2012年12月17日星期一

殊途同归,ado.net快速实现MySql的CRUD


殊途同归,ado.net快速实现MySql的CRUD

正在学习MySql编程,安装了官方的C#客户端,在自己的机器上写了个demo,将可能常见且容易出现的问题或错误温故知新一遍,写下来以备日后再用。

一、MySql的连接字符串

连接字符串无比简单,但是这是必须的。我们需要关心的是,通过.net客户端程序,mysql的连接字符串可以像sql server一样,利用连接池高效管理连接。简单配置如下:
?
1
2
3
<connectionStrings>
  <add name="MySqlConnString" connectionString="Server =localhost; Database =test; Uid =***; Pwd =***;Pooling=true; Max Pool Size=20;Min Pool Size=10;Allow Batch=true;" providerName="MySql.Data.MySqlClient"/>
</connectionStrings>
更多参数,请参考:

二、实现简单的CRUD

毫无疑问,快速上手CRUD的前提是必须熟悉基础的sql,然后再对照mysql的特定语法做特定操作。不要小看了这简单的crud,普通应用系统中这部分的工作占了很大一部分,笔者才疏学浅,为这部分工作也花过不少时间。还好我们现在有了各种优秀的orm,相对而言实现起来比较轻松。
1、插入记录
最简单的方式,当然是传入参数一条一条插入:
复制代码
CREATE DEFINER=`root`@`localhost` PROCEDURE `AddSimplePerson`(
FirstName varchar(16),
LastName varchar(16),
Age int,
CreateDate datetime,
UpdateDate datetime,
State int,
Remark varchar(256)
)
BEGIN
INSERT INTO test.Person(FirstName,LastName,Age,CreateDate,UpdateDate,State,Remark)
VALUES(FirstName,LastName,Age,CreateDate,UpdateDate,State,Remark);

SELECT @@IDENTITY;

END
复制代码

经测试,插入数据的存储过程里的变量定义可以和列名一模一样。基于mysql的特殊语法,建议参数变量不要和列名一模一样,最好给个别名,这里不是介绍的重点,略过。
和插入记录比较类似的一个功能就是复制表定义及复制数据行,曾经简单总结过,不再赘述。
插入记录还有一个比较常用的功能,就是批量插入
记得在向Sql Server批量插入的时候,可以通过SqlBulkCopy实现批量快速插入。先使用SqlBulkCopy测试了下MySql的批量插入,执行的结果就是直接如下抛出异常:
“在与 SQL Server 建立连接时出现与网络相关的或特定于实例的错误。未找到或无法访问服务器。请验证实例名称是否正确并且 SQL Server 已配置为允许远程连接。 (provider: Named Pipes Provider, error: 40 - 无法打开到 SQL Server 的连接)”
在配置文件中将连接字符串加上providerName(providerName="MySql.Data.MySqlClient")依然如故。看到这个结果忍不住哀号一句,老子tmd是要向mysql插入数据啊。
无奈打开msdn,看到SqlBulkCopy类的功能介绍直接崩溃:“lets you efficiently bulk load a SQL Server table with data from another source.”。原来SqlBulkCopy只对SQL Server有效啊,这点从它的命名空间(System.Data.SqlClient)大致可以猜到,MS真不厚道。
那么到底怎么实现批量插入的功能呢?
利用ILSpy查看了一下C#客户端的源码,发现有个MySqlBulkLoader,查看示例竟然是从文件中批量操作数据的,这个明显不满足我们的开发要求。话说不就是一次多插入几条数据吗?至于真的要先写入文件吗?
最后在网上搜索了一下,看到iteye上的一篇Java 批量插入数据库(MySQL)数据,仔细分析这篇文章,个人认为Java下实现批量插入无非就是通过JDBC API或者PreparedStatement批处理方式,实现思想还是比较简单的。其实.net也有自己的api实现批量插入(SqlBulkCopy),只是不支持mysql而已;而批处理命令(PreparedStatement)我怎么看上去都像是拼接字符串呢?得知mysql和db2支持下面这种多行插入(multi row insert):
insert2persons

我立刻想到实现批量插入的一种简单方式(也就是他的文章里提到的伪批处理命令),需要拼接sql字符串。贴一下有sql注入风险的示例代码:
batchinsert1

调用如下:
testbatchinsert1

通过上面代码可以看到,拼接字符串的地方没有进行特殊字符串处理,很容易造成sql注入。虽然实际开发的时候可以通过一个字符串处理函数进行sql敏感字符串处理,但显然这不是最优的方式。我们完全可以参数化传参再插入,重新改进代码如下:
batchinsert2

调用方式如下:
testbatchinsert2

还是免不了拼接,而且可读性较差,参数较多,调用不够简洁。
那如何更好地批量插入呢?老实说,直到现在我也还是不知道。实际开发中,批量插入的方式通常都是按照实体列表进行插入(或者使用datatable批量插入)的。于是,我又重写了一个泛型方法,用来实现参数化的批量插入,当然,原理还是拼接字符串,对于常见数据库批量插入操作,我推荐你使用下面这种方式:
BATCHINSERT
调用方式也比较简洁,如下所示:
TestBatchInsertModels

经测试,构造插入字符串的地方,参数前面必须加个“@”符号,如果不加,则抛出异常,这一点很奇怪。
必须指出,sql拼接执行插入的长度是有限制的。在我的电脑上测试插入3000个实体一点问题没有,当插入1万条记录的时候也还行,当插入10万条的时候……虽然我没有测试过可以执行的sql长度最长是多少,但是根据经验,这个sql总长度应该不会超过8k+的长度限制吧(存疑??),所以开发中可以按照策略对需要批量操作的数据再分批量插入。
如果您有好的批量插入数据的思路和解决方案,请不吝赐教。

2、查询
数据库的两大作用,一个是存储,另一个就是各种查询了。所以必须重视查询,重视查询的形式当然是从熟悉查询语法开始,这里不做过分说明,大家可以参考官方说明文档。
简单查询如利用组合条件或者id主键查询记录,很简单:
querypersons

在程序中调用非常简单,正确传参即可:
querypersons

注:mysql的字符串函数不同于sql server,最简单的字符串连接,mysql通常都是通过CONCAT函数,而sql server则用+号即可。
CONCAT函数在mysql编程中出现频率极高,它的用法如下:
?
1
CONCAT(str1,str2,…)
连接的字符串参数个数看上去原则上是无限制的,返回结果为连接参数产生的字符串。
它还有两个特性,记录一下:
(1)、如有任何一个参数为NULL ,则返回值为 NULL。
?
1
2
3
mysql> SELECT CONCAT('My', NULL, 'Sql');
 
-> NULL
(2)、CONCAT一个或多个参数,如果所有参数均为非二进制字符串,则结果为非二进制字符串;如果自变量中含有任一二进制字符串,则结果为一个二进制字符串。一个数字参数被转化为与之相等的二进制字符串格式。
?
1
2
3
mysql> SELECT CONCAT('My', 'Sql',2011);
 
-> BLOB
下面重点讨论一下分页查询的实现:
mysql的取前top n条记录有自己的语法,即limit n,这个在这一篇里曾经提及。limit还有一个偏移区间的写法:limit offset,n,第一个参数offset指定第一个返回记录行的偏移量,第二个参数n指定返回记录行的最大数目,举例来说:
?
1
mysql> SELECT * FROM table LIMIT 5,10;  // 检索记录行 6-15
还有一种方式就是指定第二个数字n为-1,那么它表示的意思就是查询从某一个偏移量到记录集结束的所有记录数:
?
1
mysql> SELECT * FROM table LIMIT 5,-1; // 检索记录行 6-last.
这种特性相当适合分页。关于分页,技术社区和论坛里讨论的非常多,下面就再简单说说利用mysql的limit的特性按照主键ID逆序分页取记录的功能实现。常见的分页处理方式是通过如下这种分页存储过程:
getpagerperson1

我们稍微分析一下就可以发现,对于数据量较大的查询,越往后面效率应该越低,因为越往后检索的记录行越多,这一点在高效的MySql分页介绍里已经得到验证。
和传入当前页码和每页记录数类似的是传入开始记录和结束记录这种方式,本质上和上面是没有区别的:
getpagerperson2

真正可以高效的查询,当然是记住上一次查询结果的最小(或者最大)主键,这取决于查询是逆序还是升序排序。像下面这样实现的查询越往后翻页则可以省却查询很多记录行:
quickpagergetpersons

也就是说对于>1以后的查询都需要记住上次查询的一个“临界值”(通常情况下:如按Id逆序,最小Id;按照Id降序,最大Id)。园子里流传甚广的一篇博客高效的MySql分页讲的也是这个原理。记得早就有很多人总结过sql server下的分页方式,其中经典的二次top我感觉和limit的方式非常相似,只不过limit的效率可能更高一些。
注:在存储过程中LIMIT后面只能是常量而不能是变量,所以只好用字符串拼接生成SQL语句,然后动态执行即可
【UPDATE】:经同事提醒,是MySql版本问题,本地的测试环境是MySql5.0,换成MySql5.5,MySql引擎已支持LIMIT后面使用变量
同时还要切记,分页查询尽量按照主键正序或者逆序查询,否则按其他有可能重复的字段分页,查询的结果可能不准确(想想为什么?),实际开发经验中这一点已经得到了验证。
最后不能免俗,重新发布一个mysql通用分页存储过程,类似于sql server下的通用分页存储过程,核心思想当然是动态拼接字符串:
sp_pager

调用方式如下:
?
1
CALL test.sp_Pager(2,10,'Id,FirstName,LastName,CreateDate,UpdateDate,State','test.Person','ORDER BY ID DESC');
3、修改
通常情况下,按照主键修改一条记录信息相对而言比较简单,只要sql正确、传参无误,功能实现基本和sql server一模一样。
有时候我们需要进行跨表多数据量更新,比如sql server下可以通过update from的方式进行两张表或多张表数据更新,在mysql下如何实现update from方式的更新呢?
举例:有两张表Person和People,表结构完全一样。现在要实现sql server如下的功能:
updatefrom

对于sql server再简单不过的功能,mysql下需要稍微绕点弯,
mysqlupdatefrom

虽然看上去也还算简单,可是毕竟需要join查询。而对于数据量较大的表,我曾经得到过的一个重大教训就是减少连接查询。
关于修改的功能,还有一个地方需要注意,就是在原来对应列的数据基础上更新数据,比如将所有人的Age都更新为原来的数据加1,备注修改为全名:
?
1
UPDATE test.Person SET Age=Age+1,Remark=CONCAT(FirstName,LastName);
上面的sql语句执行是没有问题的。除非需要更新的列指定不明,执行的时候,sql编辑器会给出警告: Column '***' in field list is ambiguous。这就说明某一列指代不明。

4、删除
删除的功能和sql server非常相似,一条一条删除(DELETE FROM *** WHERE ***)无疑是最基础最常用的,这里不做示例展示。
和删除相关的就是清空表数据,sql server下的TRUNCATE TABLE *** 对mysql同样适用。
下面着重介绍删除重复记录的实现。
在sql server下,我们需要删除某列的重复记录(示例是FirstName相同,保留的列是ID最小的记录)很简单:
deleteduplicate
ps:要找出表中某个字段的重复值,记得曾经在这篇随笔里提及过。原理就是count+分组:
 select 字段名,count(字段名) from 表名 group by 字段名 having count(字段名)>1
如果将上面的sql语句放在mysql下面执行,会给出错误警告:Error Code: 1093. You can't specify target table 'Person' for update in FROM clause。
同样的功能,在mysql里的实现如下:
mysqldeleteduplicate

感觉mysql的这种写法比较迂回一点,好坏不做评价。

三、需要注意的几点

1、插入的记录不合法
在插入的时候,比如插入FirstName的值超过了varchar(16)的长度,则抛出mysqlexception,并提示:Data too long for column 'FirstName' at row 1。这点类似于sql server下的二进制字节流截断的异常。

2、mysql语句的参数传递
经测试,在mysql的存储过程中传递一个和列名Id完全一样的叫Id的参数是相当危险的。在我的测试中,删除存储过程DeletePerson如下:
DeletePerson

获取一条的存储过程如下:
GetPersonByID

更新一条记录的存储过程如下:
updateperson

但是执行的效果远不是期望的那样,真正的效果依次是删除所有记录、选取出所有记录和更新所有记录。这样很容易造成误删除或者误读取或者错误更新。解决的方法相当简单,给参数起个不是Id的别名即可。
那么通过sql语句而不是存储过程实现增删改查的参数传递如何呢?
我们不妨以update方法一试:
updateperson

实际效果果然非常诡异。
这里我大胆推测,官方提供的MySql 的C#客户端沿袭了Sql Server下的某些写法,所以,直接执行sql语句的时候,建议传递的参数必须带上“@”符号(经本地测试,增删改查都可以这么写;虽然@在存储过程里是个敏感字符,但是c#程序里调用存储过程传参也是可以使用@符号的),这个和前面拼接sql批量插入的地方有某种程度的巧合,而实际上人家可能就是这么设计传参的。
PS:我的一个牛人同事告诉我mysql存储过程传参的一些基本用法,其中着重强调的就是参数传递尽量按照mysql的标准来写,而不要沿袭sql server的写法,否则可能会造成不小困扰。他有非常丰富的开发经历,虽为经验之谈,经测试验证果然非常正确,今后必须汲取。

3、mysql的特殊语法和函数
我们知道,主流关系型数据库有多种,它们都有各自的特点和适用环境。所以如果一个人哪怕是多么了不起的dba,他也不太可能轻易地将各个平台的数据库知识都了然如胸。从这个层面来讲,学习通用而基础的sql原理和知识显得尤为重要,CSDN和博客园有很多优秀文章值得细细品读和学习。
对于像我一样的新手,我认为学好基础的sql,再深入熟练使用不同厂商的特殊语法和内置函数方能游刃有余,mysql的特殊语法和函数当然是一个重要的补充。这几天学到了不少mysql的“独特”写法,这里说它独特,主要还是先入为主,毕竟我个人使用sql server的开发经验远远高于mysql。准备将mysql和sql server的一些常见用法做个对比,这里不再一一列举。

四、打造自己的简易ORM

在之前介绍ado.net的几篇文章中,自己动手实现了一些类似orm的帮助类库(曾经改进了几次,但是不太令人满意),针对sql server的实现相对比较简单,而对mysql和oracle的一直没有动手。在文章最后的下载demo中,我参考了sql server的风格封又重新实现封装了个MySqlHelper(不是官方的那个MySqlHelper),感觉还不错。精力有限,有时间我可能也会把mysql的功能和sqlserver的实现整合在一起,当然还是等用熟了MySql再动手。
最后,求推荐一款简单易用的针对MySQL的ORM。 

demo下载:demo

参考:
O’REILLY <<SqlCookBook>>