- 浏览: 99628 次
- 性别:
- 来自: 杭州
文章分类
最新评论
-
wangyajin333:
棒棒的。写得很好!
深入理解HashMap -
mciiv:
赞一个~ 不过对下面所述的问题有一些不同的看法, 仅为个人观 ...
深入理解HashMap -
showgo1306:
随便说说:发现几个问题,中国的程序员果然如传闻中的那样会随意指 ...
mysql的jdbc驱动分析 -
ychen_123:
我想咨询你一个关于JMX的问题。
就是启动一个程序。
配置 ...
JMX的一些讨论 -
mzyp:
多谢分享
多线程断点续传实践
mysql的jdbc驱动分析
/**
*作者:annegu
*日期:2009-07-01
*/
Mysql是一个中小型关系型数据库管理系统,目前使用的也比较广泛。为了对开发中间dao层的问题能有更深的理解,在遇到问题的时候能够有更多的思路,于是研究了一下mysql驱动的使用,并且在这过程中也发现了一直以来关于PreparedStatement常识理解上的错误,与大家分享。
下面是个最简单的使用jdbc取得数据的应用。在例子之后我将分成4步,分别是①取得连接,②创建PreparedStatement,③设置参数,④执行查询,来分步分析这个过程。除了设置参数那一步之外,其他的我都画了时序图,如果不想看文字的话,可以对着时序图 。文中的第4步是组装mysql协议并发送数据包的关键,而且在这部分的(b)环节,我对于PreparedStatement的应用有详细的代码注释分析,建议大家关注一下。
1、首先我们看到要的到一个数据库连接,得到数据库连接这部分放在DBHelper类中的getConnection方法中实现。Class.forName("com.mysql.jdbc.Driver");用来加载mysql的jdbc驱动。
Mysql的Driver类实现了java.sql.Driver接口,任何数据库提供商的驱动类都必须实现这个接口。在DriverManager类中使用的都是接口Driver类型的驱动,也就是说驱动的使用不依赖于具体的实现,这无疑给我们的使用带来很大的方便。如果需要换用其他的数据库的话,只需要把Class.forName()中的参数换掉就可以了,可以说是非常方便的。
在com.mysql.jdbc.Driver类中,除了构造方法,就是一个static的方法体,它调用了DriverManager的registerDriver()方法,这个方法会加载所有系统提供的驱动,并把它们都假如到具体的驱动类中,当然现在就是mysql的Driver。在这里我们第一次看到了DriverManager类,这个类中提供了jdbc连接的主要操作,创建连接就是在这里完成的,可以说这是一个管理驱动的工具类。
注册驱动首先就是初始化,然后把驱动的信息封装一下放进一个叫做DriverInfo的驱动信息类中,最后放入一个驱动的集合中。初始化工作主要是完成所有驱动的加载。
至于驱动的集合writeDrivers和readDrivers,很有趣的是,无论是registerDriver还是deregisterDriver,都是先对writeDrivers中的数据进行添加或者删除,然后再把writeDrivers中的驱动都拷贝到readDrivers中,但每次取出driver却从来不从writeDrivers中取,都是通过readDrivers来获得。我认为可以这样理解,writeDrivers只负责注册driver与注销driver,而readDrivers只负责提供可用的driver,只有当writeDrivers中准备好了驱动,这些驱动才是可以使用的,所以才能被copy至readDrivers中以备使用。这样一来,对内的注册注销与对外的提供使用就分开来了。
第二步就要根据url和用户名,密码来获得数据库的连接了。url一般都是这样的格式:jdbc:protocol://host_name:port/db_name?parameter_name=param_value。开头部分的protocal是对应于不同的数据库提供商的协议,例如mysql的就是mysql。
DriverManager中有重载了四个getConnection(),因为我们有用户名和密码,就把用户和密码存放在Properties中,最后进入终极getConnection(),如下:
Initialize()简直无所不在,DriverManager中只要使用driver之前,就要检查一下有没有初始化,非常小心。然后开始遍历所有驱动,直到找到一个可用的驱动,用这个驱动来取得一个数据库连接,最后返回这个连接。当然,这是正常的情况,从上面我们可以看到,程序中对异常的处理很仔细。如果连接失败,会记录抛出的第一个异常信息,如果没有找到合适的驱动,就抛出一个08001的错误。
现在重点就是假如一切正常,就应该从driver.connect()返回一个数据库连接,所以我们来看看如何通过url提供的数据库。
很简洁的写法,就是新建了一个mysql的connection,host, port, database给它传进入,让它去连接就对了,props里面是些什么东西呢,就是把url拆解一下,什么host,什么数据库名,然后url后面的一股脑的参数,再把用户跟密码也都放进入,反正就是所有的连接数据都放进入了。
在com.mysql.jdbc.Connection的构造方法里面,会先做一些连接的初始化操作,例如创建PreparedStatement的cache,创建日志等等。然后就进入createNewIO()来建立连接了。
从时序图中可以看到,createNewIO()就是新建了一个com.mysql.jdbc.MysqlIO,利用com.mysql.jdbc.StandardSocketFactory来创建一个socket。然后就由这个mySqlIO来与MySql服务器进行握手(doHandshake()),这个doHandshake主要用来初始化与Mysql server的连接,负责登陆服务器和处理连接错误。在其中会分析所连接的mysql server的版本,根据不同的版本以及是否使用SSL加密数据都有不同的处理方式,并把要传输给数据库server的数据都放在一个叫做packet的buffer中,调用send()方法往outputStream中写入要发送的数据。
2、PreparedStatement stmt = conn.prepareStatement(sql);使用得到的connection创建一个Statement。Statement有许多种,我们常用的就是PreparedStatement,用于执行预编译好的SQL语句,CallableStatement用于调用数据库的存储过程。它们的继承关系如下图所示。
一旦有了一个statement,就可以通过执行statement.executeQuery()并通过ResultSet对象读出查询结果(如果查询有返回结果的话)。
创建statement的方法一般都有重载,我们看下面的prepareStatement:
如果没有指定resultSetType和resultSetConcurrency的话,会给它们默认设置一个值。
ResultSet中的参数常量主要有以下几种:
TYPE_FORWARD_ONLY: ResultSet的游标只能向前移动。
TYPE_SCROLL_INSENSITIVE:ResultSet的游标可以滚动,但对于resultSet下的数据改变不敏感。
TYPE_SCROLL_SENSITIVE:ResultSet的游标可以滚动,但对于resultSet下的数据改变是敏感的。
CONCUR_READ_ONLY:不可以更新的ResultSet的并发模式。
CONCUR_UPDATABLE:可以更新的ResultSet的并发模式。
FETCH_FORWARD:按正向(即从第一个到最后一个)处理结果集中的行。
FETCH_REVERSE:按反向(即从最后一个到第一个)处理结果集中的行处理。
FETCH_UNKNOWN:结果集中的行的处理顺序未知。
CLOSE_CURSORS_AT_COMMIT:调用Connection.commit方法时应该关闭 ResultSet 对
HOLD_CURSORS_OVER_COMMIT:调用Connection.commit方法时不应关闭ResultSet对象。
prepareStatement的创建如下图所示:
在new ParseInfo中,会对这个sql语句进行分析,例如看看这个sql是什么语句;有没有limit条件语句,还有一个重要的工作,如果使用的是PreparedStatement来准备sql语句的话,会在这里把sql语句进行分解。我们知道PreparedStatement对象在实例化创建时就被设置了一个sql语句,使用PreparedStatement对象执行的sql语句在首次发送到数据库时,sql语句就会被编译,这样当多次执行同一个sql语句时,mysql就不用每次都去编译sql语句了。这个sql语句如果包含参数的话,可以用问号(”?”)来为参数进行占位,而不需要立即为参数赋值,而在语句执行之前,必须通过适当的set***()来为问号处的参数赋值。New ParseInfo()中,包含了参数的sql语句就会被分解为多段,放在staticSql中,以便需要设置参数时定位参数的位置。假如sql语句为“select * from adv where id = ? and name = ?”的话,那么staticSql中的元素就是3个,staticSql[3]={ ”select * from adv where id = ”, ” and name = ” , ””}。注意数组中最后一个元素,在这个例子中是””,因为我的例子里面最后一个就是”?”,如果sql语句是这样的“select * from adv where id = ? and name = ? order by id”的话,staticSql就变成是这样的{ ”select * from adv where id = ”, ” and name = ” , ” order by id”}。
3、stmt.setInt(1, new Integer(1));
设置sql语句中的参数值。
对于参数而言,PreparedStatement中一共有四个变量来储存它们,分别是
a) byte[][] parameterValues:参数转换为byte后的值。
b) InputStream[] parameterStreams:只有在调用存储过程batch(CallableStatement)的时候才会用到它,否则它的数组中的值设置为null。
c) boolean[] isStream:是否为stream的标志,如果调用的是preparedStatement,isStream数组中的值均为false,若调用的是CallableStatement,则均设置为true。
d) boolean[] isNull:标识参数是否为空,设置为false。
这四个变量的一维数组的大小都是一样的,sql语句中有几个待set的参数(几个问号),一维的元素个数就是多大。
4、ResultSet rs = stmt.executeQuery();
一切准备就绪,开始执行查询罗!
a) 检查preparedStatement是否已关闭,如果已关闭,抛出一个SQLError.SQL_STATE_CONNECTION_NOT_OPEN的错误。
b) fillSendPacket:创建数据包,其中包含了要发送到服务器的查询。
这个sendPacket就是mysql驱动要发送给数据库服务器的协议数据。一般来说,协议的数据格式有两种,一种是二进制流的格式,还有一种是文本的格式。文本协议就是基本上人可以直接阅读的协议,一般是用ascii字符集,也有用utf8格式的,优点是便于理解,读起来方便,扩充容易,缺点就是解析的时候比较麻烦,而且占用的空间比较大,冗余的数据比较多。二进制格式话,就需要服务器与客户端协议规定固定的数据结构,哪个位置放什么数据,如果单独看协议内容的话,很难理解数据含义,优点就是数据量小,解析的时候只要根据固定位置的值就能知道具体标识什么意义。
在这里使用的是二进制流的格式,也就是说协议中的数据格式是固定的,而且都要转换成二进制。格式为第一个byte标识操作信号,后面开始就是完整的sql语句的二进制流,请看下面的代码分析。
假如sql语句为“select * from adv where id = ?”的话,这个sendPacket中第一个byte的值就是3(MysqlDefs.QUERY的int值),后面接着的就是填充了参数值的完整的sql语句字符串(例如:select * from adv where id = 1)转换成的byte格式。
于是,我们看到,好像sql语句在这里就已经不是带”?”的preparedStatement,而是在驱动里面把参数替代到”?”中,再把完整的sql语句发送给mysql server来编译,那么尽管只是参数改变,但对于mysql server来说,每次都是新的sql语句,都要进行编译的。这与我们之前一直理解的PreparedStatement完全不一样。照理来说,应该把带”?”的sql语句发送给数据库server,由mysql server来编译这个带”?”的sql语句,然后用实际的参数来替代”?”,这样才是实现了sql语句只编译一次的效果。sql语句预编译的功能取决于server端,oracle就是支持sql预编译的。
所以说,从mysql驱动的PreparedStatement里面,好像我们并没有看到mysql支持预编译功能的证据。(实际测试也表明,如果server没有预编译功能的话,PreparedStatement和Statement的效率几乎一样,甚至当使用次数不多的时候,PreparedStatement比Statement还要慢一些)。
但是并不是说PreparedStatement除了给我们带来高效率就没有其他作用了,它还有非常好的其他作用:
i. 极大的提高了sql语句的安全性,可以防止sql注入
ii. 代码结构清晰,易于理解,便于维护。
2009-07-02增加(感谢gembler):其实,在mysql5上的版本是支持预编译sql功能的。我用的驱动是5.0.6的,在com.mysql.jdbc.Connection中有一个参数useServerPreparedStmts,表明是否使用预编译功能,所以如果把useServerPreparedStmts置为true的话,mysql驱动可以通过PreparedStatement的子类ServerPreparedStatement来实现真正的PreparedStatement的功能。在这个类的serverExecute方法里面,就负责告诉server,用现在提供的参数来动态绑定到编译好的sql语句上。所以说,ServerPreparedStatement才是真正实现了所谓prepare statement。
c) 设置当前的数据库名,并把之前的数据库名记录下来,在查询完成之后还要恢复原状。
d) 检查一下之前是否有缓存的数据,如果不久之前执行过这个查询,并且缓存了数据的话,就直接从缓存中取出。
e) 如果sql查询没有限制条件的话,为其设置默认的返回行数,若preparedStatement中已经设置了maxRows的话,就使用它。
f) executeInternal:执行查询。
i. 设置当前数据库连接,并调用connection的execSQL来执行查询.然后继续把要发送的查询包,就是之间组装完毕的sendPacket传递进入MysqlIO的sqlQueryDirect()。
ii. 接下来就要往server端发送我们的查询指令啦(sendCommand),说到发送数据,不禁要问,如果这个待发送的数据包超级大,难道每次都是一次性的发送吗?当然不是,如果数据包超过规定的最大值的话,就会把它分割一下,分成几个不超过最大值的数据包来发送。
所以可以肯定,在分割的过程中,除了最后一个数据包,其他数据包的大小都是一样的。那就这样的数据包直接切割了进行发送的话,假如现在被分成了三个数据包,发送给mysql server,服务器怎么知道那个包是第一个呢,它读数据该从什么地方开始读呢,这都是问题,所以,我们要给每个数据包的前面加上一点属性标志,这个标志一共占了4个byte。从代码①处开始就是头标识位的设置。第一位表示数据包的开始位置,就是数据存放的起始位置,一般都设置为0,就是从第一个位置开始。第二和第三个字节标识了这个数据包的大小,注意的是,这个大小是出去标识的4个字节的大小,对于非最后一个数据包来说,这个大小都是一样的,就是splitSize,也就是maxThreeBytes,它的值是255 * 255 * 255。
最后一个字节中存放的就是数据包的编号了,从0开始递增。
在标识位设置完毕之后,就可以把255 * 255 * 255大小的数据从我们准备好的待发送数据包中copy出来了,注意,前4位已经是标识位了,所以应该从第五个位置开始copy数据。
在数据包都装配完毕之后,就可以往socket的outputSteam中发送数据了。接下来的事情,就是由mysql服务器接收数据并解析,执行查询了。
iii. 通过readAllResults方法读取查询结果。这个读取的过程与发送过程相反,如果接收到的数据包有多个的话,通过IO不断读取,并根据第packet第4个位置上的序号来组装这些packet。然后把读到的数据组装成resultSet中的rowData,这个结果就是我们要的查询结果了。
结合下面的executeQuery的时序图再理一下思路就更清楚了。
至此,把resultSet一步步的返回给dao,接下来的过程,就是从resultSet中取出rowData,组合成我们自己需要的对象数据了。
总结一下,经过这次对mysql驱动的探索,我发现了更多关于mysql的底层细节,对于以后分析问题解决问题有很大帮助,当然,这里面还有很多细节文中没有写。另外一个就是对于PreparedStatement有了重新的认识,有些东西往往都是想当然得出来的结论,真相还是要靠实践来发现。
恭喜您,您获得了javaeye2009年度1-6月份"水王"荣誉称号,您常年活跃于各大板块,坚持灌水为主,讨论为辅,始终保持0分的纯洁和神圣,希望您能将这种精神持之以恒的保持,即使每天一份Javaeye小测验也不动摇自己的信念。
那哥们以后能不能说句有用的话,在这浪费je的数据库空间。
1, 数据库是否必须支持事务:
这个你不要扣概念了。在中国还有百万富翁之称呢,你到津巴布韦去自称亿万富翁,笑死你。
2, MyISAM是支持事务的<====> MyISAM 只是一个数据文件的格式, 和事物没有任何关系
你怎么前边不搭后边。人们都称MyISAM存储引擎,你却在这里只说人家是数据文件格式
3, 你不会认为事务是由文件格式或者文件系统提供的吧?
看来你对MySQL事物的ACDI很了解啊,那就说说MySQL+MyISAM的文件级别的锁怎么实现 我不了解得ACDI吧。
再贴一点
By "transactional table type" I mean either InnoDB or BDB (but the latter is only included in -max versions). There are also some non-MySQL AB storage engines in the wild that might do the trick. And possibly there might be even more in the future.
MyISAM does not support transactions and probably never will.
But we are working on a feature that can rollback commands that were active when the MySQL server crashed. It will also be possible to explicitly rollback everything from the point of a LOCK TABLES as long as no UNLOCK TABLES has been done so far.
Please do not think of it as transactional. The way MyISAM stores data does not guarantee you anything when the machine (or better the operating system) crashes. You won't be able to undo nor redo MyISAM tables to a clean state.
Regards
Ingo Strüwing, Senior Software Developer - Storage Engines
MySQL AB, www.mysql.com
人家自己都说不支持,你还非得给别人脸上贴金,还有我想等会你干脆根据你的理论说,基于MyISAM存储引擎的MySQL is not 数据库。。。
ok 你说的是对的, 确实我对MySQL 不是能了解, 站在数据库的角度说, Mysql 确实有不小的差距,有很多人认为MySQL 是dabatase, 但你也说了,津巴布韦也有亿万富翁。
1, 数据库是否必须支持事务:
这个你不要扣概念了。在中国还有百万富翁之称呢,你到津巴布韦去自称亿万富翁,笑死你。
2, MyISAM是支持事务的<====> MyISAM 只是一个数据文件的格式, 和事物没有任何关系
你怎么前边不搭后边。人们都称MyISAM存储引擎,你却在这里只说人家是数据文件格式
3, 你不会认为事务是由文件格式或者文件系统提供的吧?
看来你对MySQL事物的ACDI很了解啊,那就说说MySQL+MyISAM的文件级别的锁怎么实现 我不了解得ACDI吧。
再贴一点
By "transactional table type" I mean either InnoDB or BDB (but the latter is only included in -max versions). There are also some non-MySQL AB storage engines in the wild that might do the trick. And possibly there might be even more in the future.
MyISAM does not support transactions and probably never will.
But we are working on a feature that can rollback commands that were active when the MySQL server crashed. It will also be possible to explicitly rollback everything from the point of a LOCK TABLES as long as no UNLOCK TABLES has been done so far.
Please do not think of it as transactional. The way MyISAM stores data does not guarantee you anything when the machine (or better the operating system) crashes. You won't be able to undo nor redo MyISAM tables to a clean state.
Regards
Ingo Strüwing, Senior Software Developer - Storage Engines
MySQL AB, www.mysql.com
人家自己都说不支持,你还非得给别人脸上贴金,还有我想等会你干脆根据你的理论说,基于MyISAM存储引擎的MySQL is not 数据库。。。
MyISAM真的支持事务吗?
http://dev.mysql.com/doc/refman/5.4/en/myisam-storage-engine.html
这上面Transactions写的是NO呀。
你不会认为事务是由文件格式或者文件系统提供的吧?
MyISAM 只是一个数据文件的格式, 和事物没有任何关系, 所谓的MyISAM支持本地事物是指可以使用posix文件锁在MyISAM数据文件上提供表级别的锁,MyISAM中一个表就是一个文件, 文件锁就是表锁,并发性可想而知了。 正是如此, 所以这个功能才不被建议使用!在后续的某个版本提供的行锁(排他性的)使用的posix记录锁实现的, 效果也很差。
mysql 提供lock 语句, 你去看看lock的文档,上面应该说的很清楚的。
顺便说一句, 我没用过mysql。mysql的文档我也没读过。我以前为postgresql捐献过代码, 在pgsql开发组中混的几天,所以听那帮大牛说过不少次mysql的优缺点和问题。 所以才对mysql 略微有一些了解。
不要太概念了,概念的东西都是理论的东西,具体实现和概念是有差距的。 数据库必须支持事务吗?完全可以做一个不支持事物的数据库啊(MYSQL MyISAM引擎)! 而且完全可以实现一个不抛异常的api事务操作实现啊(自己实现一个事务接口不给你抛任何异常)! 总得把每个函数的作用搞搞清楚吧---应该是吧没有具体实现搞清楚嘛(我又没反对不该搞清楚)。我的意思就是很多东西就那么回事,没有什么大不了的!
1, 数据库是否必须支持事务, 去查查关于数据库的文献, 有很多,而且都是大部头的。 再去了解一下什么是ACDI,顺便搞清楚数据库和文件系统的区别。
2 MyISAM是支持事务的, 而且支持的就是我说的本地事务。 其实现方式用的是文件锁,(后续的某个版本提供了记录锁的支持), 但是MyISAM的文档上确实说过, 不建议使用事务方式, 同时文档中也说明了理由, 原因是并发太差,而且错误太多。(给你个建议, 我觉得你对MyISAM的了解还是少了些, 最好在去看看文档,补习补习。)
3 你当然可以自己去实现一套api, j2ee 中间有个叫做jca的东西, 如果有兴趣的你可以摆弄一下,搞个不出异常的东西出来,别忘记把 jts 也重新实现一下, 否则还是要抛出异常的! 但是我这里要说的是,我们讨论的jdbc API, 不是你的个人作品。
MyISAM真的支持事务吗?
http://dev.mysql.com/doc/refman/5.4/en/myisam-storage-engine.html
这上面Transactions写的是NO呀。
恭喜您,您获得了javaeye2009年度1-6月份"水王"荣誉称号,您常年活跃于各大板块,坚持灌水为主,讨论为辅,始终保持0分的纯洁和神圣,希望您能将这种精神持之以恒的保持,即使每天一份Javaeye小测验也不动摇自己的信念。
哪里说?
昨天才做了论坛测试题,请看置顶帖子:JavaEye积分规则,博客和论坛使用规则
四、严禁无聊灌水性帖子
JavaEye的论坛不是用来灌水的,是用来交流技术的,即使是海阔天空版面,也不意味着你可以随意灌水,凡是言之无物的无聊灌水贴,将被删除,并扣除发贴者积分30分。纯灌水贴请到“JavaEye水源”圈子。请记住JavaEye的论坛是一个严肃的技术交流场所,在这里制造垃圾信息将受到惩罚。
版主,我不是灌水,是回答问题,并普及论坛规则!
这回robbin高兴死了
哪里说?
昨天才做了论坛测试题,请看置顶帖子:JavaEye积分规则,博客和论坛使用规则
四、严禁无聊灌水性帖子
JavaEye的论坛不是用来灌水的,是用来交流技术的,即使是海阔天空版面,也不意味着你可以随意灌水,凡是言之无物的无聊灌水贴,将被删除,并扣除发贴者积分30分。纯灌水贴请到“JavaEye水源”圈子。请记住JavaEye的论坛是一个严肃的技术交流场所,在这里制造垃圾信息将受到惩罚。
版主,我不是灌水,是回答问题,并普及论坛规则!
*作者:annegu
*日期:2009-07-01
*/
Mysql是一个中小型关系型数据库管理系统,目前使用的也比较广泛。为了对开发中间dao层的问题能有更深的理解,在遇到问题的时候能够有更多的思路,于是研究了一下mysql驱动的使用,并且在这过程中也发现了一直以来关于PreparedStatement常识理解上的错误,与大家分享。
下面是个最简单的使用jdbc取得数据的应用。在例子之后我将分成4步,分别是①取得连接,②创建PreparedStatement,③设置参数,④执行查询,来分步分析这个过程。除了设置参数那一步之外,其他的我都画了时序图,如果不想看文字的话,可以对着时序图 。文中的第4步是组装mysql协议并发送数据包的关键,而且在这部分的(b)环节,我对于PreparedStatement的应用有详细的代码注释分析,建议大家关注一下。
public class DBHelper { public static Connection getConnection() { Connection conn = null; try { Class.forName("com.mysql.jdbc.Driver"); conn = DriverManager.getConnection("jdbc:mysql://localhost/ad?useUnicode=true&characterEncoding=GBK&jdbcCompliantTruncation=false", "root", "root"); } catch (Exception e) { e.printStackTrace(); } return conn; } } /*dao中的方法*/ public List<Adv> getAllAdvs() { Connection conn = null; ResultSet rs = null; PreparedStatement stmt = null; String sql = "select * from adv where id = ?"; List<Adv> advs = new ArrayList<Adv>(); conn = DBHelper.getConnection(); if (conn != null) { try { stmt = conn.prepareStatement(sql); stmt.setInt(1, new Integer(1)); rs = stmt.executeQuery(); if (rs != null) { while (rs.next()) { Adv adv = new Adv(); adv.setId(rs.getLong(1)); adv.setName(rs.getString(2)); adv.setDesc(rs.getString(3)); adv.setPicUrl(rs.getString(4)); advs.add(adv); } } } catch (SQLException e) { e.printStackTrace(); } finally { try { stmt.close(); conn.close(); } catch (SQLException e) { e.printStackTrace(); } } } return advs; }
1、首先我们看到要的到一个数据库连接,得到数据库连接这部分放在DBHelper类中的getConnection方法中实现。Class.forName("com.mysql.jdbc.Driver");用来加载mysql的jdbc驱动。
public class Driver extends NonRegisteringDriver implements java.sql.Driver { static { try { java.sql.DriverManager.registerDriver(new Driver()); } catch (SQLException E) { throw new RuntimeException("Can't register driver!"); } } public Driver() throws SQLException { } }
Mysql的Driver类实现了java.sql.Driver接口,任何数据库提供商的驱动类都必须实现这个接口。在DriverManager类中使用的都是接口Driver类型的驱动,也就是说驱动的使用不依赖于具体的实现,这无疑给我们的使用带来很大的方便。如果需要换用其他的数据库的话,只需要把Class.forName()中的参数换掉就可以了,可以说是非常方便的。
在com.mysql.jdbc.Driver类中,除了构造方法,就是一个static的方法体,它调用了DriverManager的registerDriver()方法,这个方法会加载所有系统提供的驱动,并把它们都假如到具体的驱动类中,当然现在就是mysql的Driver。在这里我们第一次看到了DriverManager类,这个类中提供了jdbc连接的主要操作,创建连接就是在这里完成的,可以说这是一个管理驱动的工具类。
public static synchronized void registerDriver(java.sql.Driver driver) throws SQLException { if (!initialized) { initialize(); } DriverInfo di = new DriverInfo(); /*把driver的信息封装一下,组成一个DriverInfo对象*/ di.driver = driver; di.driverClass = driver.getClass(); di.driverClassName = di.driverClass.getName(); writeDrivers.addElement(di); println("registerDriver: " + di); readDrivers = (java.util.Vector) writeDrivers.clone(); }
注册驱动首先就是初始化,然后把驱动的信息封装一下放进一个叫做DriverInfo的驱动信息类中,最后放入一个驱动的集合中。初始化工作主要是完成所有驱动的加载。
至于驱动的集合writeDrivers和readDrivers,很有趣的是,无论是registerDriver还是deregisterDriver,都是先对writeDrivers中的数据进行添加或者删除,然后再把writeDrivers中的驱动都拷贝到readDrivers中,但每次取出driver却从来不从writeDrivers中取,都是通过readDrivers来获得。我认为可以这样理解,writeDrivers只负责注册driver与注销driver,而readDrivers只负责提供可用的driver,只有当writeDrivers中准备好了驱动,这些驱动才是可以使用的,所以才能被copy至readDrivers中以备使用。这样一来,对内的注册注销与对外的提供使用就分开来了。
第二步就要根据url和用户名,密码来获得数据库的连接了。url一般都是这样的格式:jdbc:protocol://host_name:port/db_name?parameter_name=param_value。开头部分的protocal是对应于不同的数据库提供商的协议,例如mysql的就是mysql。
DriverManager中有重载了四个getConnection(),因为我们有用户名和密码,就把用户和密码存放在Properties中,最后进入终极getConnection(),如下:
private static Connection getConnection( String url, java.util.Properties info, ClassLoader callerCL) throws SQLException { java.util.Vector drivers = null; ... if (!initialized) { initialize(); } /*取得连接使用的driver从readDrivers中取*/ synchronized (DriverManager.class){ drivers = readDrivers; } SQLException reason = null; for (int i = 0; i < drivers.size(); i++) { DriverInfo di = (DriverInfo)drivers.elementAt(i); if ( getCallerClass(callerCL, di.driverClassName ) != di.driverClass ) { continue; } try { /*找到可供使用的驱动,连接数据库server*/ Connection result = di.driver.connect(url, info); if (result != null) { return (result); } } catch (SQLException ex) { if (reason == null) { reason = ex; } } } if (reason != null) { println("getConnection failed: " + reason); throw reason; } throw new SQLException("No suitable driver found for "+ url, "08001"); }
Initialize()简直无所不在,DriverManager中只要使用driver之前,就要检查一下有没有初始化,非常小心。然后开始遍历所有驱动,直到找到一个可用的驱动,用这个驱动来取得一个数据库连接,最后返回这个连接。当然,这是正常的情况,从上面我们可以看到,程序中对异常的处理很仔细。如果连接失败,会记录抛出的第一个异常信息,如果没有找到合适的驱动,就抛出一个08001的错误。
现在重点就是假如一切正常,就应该从driver.connect()返回一个数据库连接,所以我们来看看如何通过url提供的数据库。
public java.sql.Connection connect(String url, Properties info) throws SQLException { Properties props = null; if ((props = parseURL(url, info)) == null) { return null; } try { Connection newConn = new com.mysql.jdbc.Connection(host(props), port(props), props, database(props), url); return newConn; } catch (SQLException sqlEx) { throw sqlEx; } catch (Exception ex) { throw SQLError.createSQLException(...); } }
很简洁的写法,就是新建了一个mysql的connection,host, port, database给它传进入,让它去连接就对了,props里面是些什么东西呢,就是把url拆解一下,什么host,什么数据库名,然后url后面的一股脑的参数,再把用户跟密码也都放进入,反正就是所有的连接数据都放进入了。
在com.mysql.jdbc.Connection的构造方法里面,会先做一些连接的初始化操作,例如创建PreparedStatement的cache,创建日志等等。然后就进入createNewIO()来建立连接了。
从时序图中可以看到,createNewIO()就是新建了一个com.mysql.jdbc.MysqlIO,利用com.mysql.jdbc.StandardSocketFactory来创建一个socket。然后就由这个mySqlIO来与MySql服务器进行握手(doHandshake()),这个doHandshake主要用来初始化与Mysql server的连接,负责登陆服务器和处理连接错误。在其中会分析所连接的mysql server的版本,根据不同的版本以及是否使用SSL加密数据都有不同的处理方式,并把要传输给数据库server的数据都放在一个叫做packet的buffer中,调用send()方法往outputStream中写入要发送的数据。
2、PreparedStatement stmt = conn.prepareStatement(sql);使用得到的connection创建一个Statement。Statement有许多种,我们常用的就是PreparedStatement,用于执行预编译好的SQL语句,CallableStatement用于调用数据库的存储过程。它们的继承关系如下图所示。
一旦有了一个statement,就可以通过执行statement.executeQuery()并通过ResultSet对象读出查询结果(如果查询有返回结果的话)。
创建statement的方法一般都有重载,我们看下面的prepareStatement:
public java.sql.PreparedStatement prepareStatement(String sql) throws SQLException { return prepareStatement(sql, java.sql.ResultSet.TYPE_FORWARD_ONLY, java.sql.ResultSet.CONCUR_READ_ONLY); } public java.sql.PreparedStatement prepareStatement(String sql, int resultSetType, int resultSetConcurrency) throws SQLException;
如果没有指定resultSetType和resultSetConcurrency的话,会给它们默认设置一个值。
ResultSet中的参数常量主要有以下几种:
TYPE_FORWARD_ONLY: ResultSet的游标只能向前移动。
TYPE_SCROLL_INSENSITIVE:ResultSet的游标可以滚动,但对于resultSet下的数据改变不敏感。
TYPE_SCROLL_SENSITIVE:ResultSet的游标可以滚动,但对于resultSet下的数据改变是敏感的。
CONCUR_READ_ONLY:不可以更新的ResultSet的并发模式。
CONCUR_UPDATABLE:可以更新的ResultSet的并发模式。
FETCH_FORWARD:按正向(即从第一个到最后一个)处理结果集中的行。
FETCH_REVERSE:按反向(即从最后一个到第一个)处理结果集中的行处理。
FETCH_UNKNOWN:结果集中的行的处理顺序未知。
CLOSE_CURSORS_AT_COMMIT:调用Connection.commit方法时应该关闭 ResultSet 对
HOLD_CURSORS_OVER_COMMIT:调用Connection.commit方法时不应关闭ResultSet对象。
prepareStatement的创建如下图所示:
在new ParseInfo中,会对这个sql语句进行分析,例如看看这个sql是什么语句;有没有limit条件语句,还有一个重要的工作,如果使用的是PreparedStatement来准备sql语句的话,会在这里把sql语句进行分解。我们知道PreparedStatement对象在实例化创建时就被设置了一个sql语句,使用PreparedStatement对象执行的sql语句在首次发送到数据库时,sql语句就会被编译,这样当多次执行同一个sql语句时,mysql就不用每次都去编译sql语句了。这个sql语句如果包含参数的话,可以用问号(”?”)来为参数进行占位,而不需要立即为参数赋值,而在语句执行之前,必须通过适当的set***()来为问号处的参数赋值。New ParseInfo()中,包含了参数的sql语句就会被分解为多段,放在staticSql中,以便需要设置参数时定位参数的位置。假如sql语句为“select * from adv where id = ? and name = ?”的话,那么staticSql中的元素就是3个,staticSql[3]={ ”select * from adv where id = ”, ” and name = ” , ””}。注意数组中最后一个元素,在这个例子中是””,因为我的例子里面最后一个就是”?”,如果sql语句是这样的“select * from adv where id = ? and name = ? order by id”的话,staticSql就变成是这样的{ ”select * from adv where id = ”, ” and name = ” , ” order by id”}。
3、stmt.setInt(1, new Integer(1));
设置sql语句中的参数值。
对于参数而言,PreparedStatement中一共有四个变量来储存它们,分别是
a) byte[][] parameterValues:参数转换为byte后的值。
b) InputStream[] parameterStreams:只有在调用存储过程batch(CallableStatement)的时候才会用到它,否则它的数组中的值设置为null。
c) boolean[] isStream:是否为stream的标志,如果调用的是preparedStatement,isStream数组中的值均为false,若调用的是CallableStatement,则均设置为true。
d) boolean[] isNull:标识参数是否为空,设置为false。
这四个变量的一维数组的大小都是一样的,sql语句中有几个待set的参数(几个问号),一维的元素个数就是多大。
4、ResultSet rs = stmt.executeQuery();
一切准备就绪,开始执行查询罗!
a) 检查preparedStatement是否已关闭,如果已关闭,抛出一个SQLError.SQL_STATE_CONNECTION_NOT_OPEN的错误。
b) fillSendPacket:创建数据包,其中包含了要发送到服务器的查询。
这个sendPacket就是mysql驱动要发送给数据库服务器的协议数据。一般来说,协议的数据格式有两种,一种是二进制流的格式,还有一种是文本的格式。文本协议就是基本上人可以直接阅读的协议,一般是用ascii字符集,也有用utf8格式的,优点是便于理解,读起来方便,扩充容易,缺点就是解析的时候比较麻烦,而且占用的空间比较大,冗余的数据比较多。二进制格式话,就需要服务器与客户端协议规定固定的数据结构,哪个位置放什么数据,如果单独看协议内容的话,很难理解数据含义,优点就是数据量小,解析的时候只要根据固定位置的值就能知道具体标识什么意义。
在这里使用的是二进制流的格式,也就是说协议中的数据格式是固定的,而且都要转换成二进制。格式为第一个byte标识操作信号,后面开始就是完整的sql语句的二进制流,请看下面的代码分析。
protected Buffer fillSendPacket(byte[][] batchedParameterStrings, InputStream[] batchedParameterStreams, boolean[] batchedIsStream, int[] batchedStreamLengths) throws SQLException { // 从connection的IO中得到发送数据包,首先清空其中的数据 Buffer sendPacket = this.connection.getIO().getSharedSendPacket(); sendPacket.clear(); /* 数据包的第一位为一个操作标识符(MysqlDefs.QUERY),表示驱动向服务器发送的连接的操作信号,包括有QUERY, PING, RELOAD, SHUTDOWN, PROCESS_INFO, QUIT, SLEEP等等,这个操作信号并不是针对sql语句操作而言的CRUD操作,从提供的几种参数来看,这个操作是针对服务器的一个操作。一般而言,使用到的都是MysqlDefs.QUERY,表示发送的是要执行sql语句的操作。 */ sendPacket.writeByte((byte) MysqlDefs.QUERY); boolean useStreamLengths = this.connection .getUseStreamLengthsInPrepStmts(); int ensurePacketSize = 0; for (int i = 0; i < batchedParameterStrings.length; i++) { if (batchedIsStream[i] && useStreamLengths) { ensurePacketSize += batchedStreamLengths[i]; } } /* 判断这个sendPacket的byte buffer够不够大,不够大的话,按照1.25倍来扩充buffer */ if (ensurePacketSize != 0) { sendPacket.ensureCapacity(ensurePacketSize); } /* 遍历所有的参数。在prepareStatement阶段的new ParseInfo()中,驱动曾经对sql语句进行过分割,如果含有以问号标识的参数占位符的话,就记录下这个占位符的位置,依据这个位置把sql分割成多段,放在了一个名为staticSql的字符串中。这里就开始把sql语句进行拼装,把staticSql和parameterValues进行组合,放在操作符的后面。 */ for (int i = 0; i < batchedParameterStrings.length; i++) { /* batchedParameterStrings就是parameterValues,batchedParameterStreams就是parameterStreams,如果两者都为null,说明在参数的设置过程中出了错,立即抛出错误。 */ if ((batchedParameterStrings[i] == null) && (batchedParameterStreams[i] == null)) { throw SQLError.createSQLException(Messages .getString("PreparedStatement.40") //$NON-NLS-1$ + (i + 1), SQLError.SQL_STATE_WRONG_NO_OF_PARAMETERS); } /*在sendPacket中加入staticSql数组中的元素,就是分割出来的没有”?”的sql语句,并把字符串转换成byte。 */ sendPacket.writeBytesNoNull(this.staticSqlStrings[i]); /* batchedIsStream就是isStream,如果参数是通过CallableStatement传递进来的话,batchedIsStream[i]==true,就用batchedParameterStreams中的值填充到问号占的参数位置中去。 */ if (batchedIsStream[i]) { streamToBytes(sendPacket, batchedParameterStreams[i], true, batchedStreamLengths[i], useStreamLengths); } else { /*否则的话,就用batchedParameterStrings,也就是parameterValues来填充参数位置。在循环中,这个操作是跟在staticSql后面的,因此就把第i个参数加到了第i个staticSql段中。参考前面的staticSql的例子,发现当循环结束的时候,原始sql语句最后一个”?”之前的sql语句就拼成了正确的语句了。 */ sendPacket.writeBytesNoNull(batchedParameterStrings[i]); } } /*由于在原始的包含问号的sql语句中,在最后一个”?”后面可能还有order by等语句,因此staticSql数组中的元素个数一定比参数的个数多1,所以这里把staticSqlString中的最后一段sql语句放入sendPacket中。 */ sendPacket .writeBytesNoNull(this.staticSqlStrings[batchedParameterStrings.length]); return sendPacket; }
假如sql语句为“select * from adv where id = ?”的话,这个sendPacket中第一个byte的值就是3(MysqlDefs.QUERY的int值),后面接着的就是填充了参数值的完整的sql语句字符串(例如:select * from adv where id = 1)转换成的byte格式。
于是,我们看到,好像sql语句在这里就已经不是带”?”的preparedStatement,而是在驱动里面把参数替代到”?”中,再把完整的sql语句发送给mysql server来编译,那么尽管只是参数改变,但对于mysql server来说,每次都是新的sql语句,都要进行编译的。这与我们之前一直理解的PreparedStatement完全不一样。照理来说,应该把带”?”的sql语句发送给数据库server,由mysql server来编译这个带”?”的sql语句,然后用实际的参数来替代”?”,这样才是实现了sql语句只编译一次的效果。sql语句预编译的功能取决于server端,oracle就是支持sql预编译的。
所以说,从mysql驱动的PreparedStatement里面,好像我们并没有看到mysql支持预编译功能的证据。(实际测试也表明,如果server没有预编译功能的话,PreparedStatement和Statement的效率几乎一样,甚至当使用次数不多的时候,PreparedStatement比Statement还要慢一些)。
但是并不是说PreparedStatement除了给我们带来高效率就没有其他作用了,它还有非常好的其他作用:
i. 极大的提高了sql语句的安全性,可以防止sql注入
ii. 代码结构清晰,易于理解,便于维护。
2009-07-02增加(感谢gembler):其实,在mysql5上的版本是支持预编译sql功能的。我用的驱动是5.0.6的,在com.mysql.jdbc.Connection中有一个参数useServerPreparedStmts,表明是否使用预编译功能,所以如果把useServerPreparedStmts置为true的话,mysql驱动可以通过PreparedStatement的子类ServerPreparedStatement来实现真正的PreparedStatement的功能。在这个类的serverExecute方法里面,就负责告诉server,用现在提供的参数来动态绑定到编译好的sql语句上。所以说,ServerPreparedStatement才是真正实现了所谓prepare statement。
c) 设置当前的数据库名,并把之前的数据库名记录下来,在查询完成之后还要恢复原状。
d) 检查一下之前是否有缓存的数据,如果不久之前执行过这个查询,并且缓存了数据的话,就直接从缓存中取出。
e) 如果sql查询没有限制条件的话,为其设置默认的返回行数,若preparedStatement中已经设置了maxRows的话,就使用它。
f) executeInternal:执行查询。
i. 设置当前数据库连接,并调用connection的execSQL来执行查询.然后继续把要发送的查询包,就是之间组装完毕的sendPacket传递进入MysqlIO的sqlQueryDirect()。
ii. 接下来就要往server端发送我们的查询指令啦(sendCommand),说到发送数据,不禁要问,如果这个待发送的数据包超级大,难道每次都是一次性的发送吗?当然不是,如果数据包超过规定的最大值的话,就会把它分割一下,分成几个不超过最大值的数据包来发送。
所以可以肯定,在分割的过程中,除了最后一个数据包,其他数据包的大小都是一样的。那就这样的数据包直接切割了进行发送的话,假如现在被分成了三个数据包,发送给mysql server,服务器怎么知道那个包是第一个呢,它读数据该从什么地方开始读呢,这都是问题,所以,我们要给每个数据包的前面加上一点属性标志,这个标志一共占了4个byte。从代码①处开始就是头标识位的设置。第一位表示数据包的开始位置,就是数据存放的起始位置,一般都设置为0,就是从第一个位置开始。第二和第三个字节标识了这个数据包的大小,注意的是,这个大小是出去标识的4个字节的大小,对于非最后一个数据包来说,这个大小都是一样的,就是splitSize,也就是maxThreeBytes,它的值是255 * 255 * 255。
最后一个字节中存放的就是数据包的编号了,从0开始递增。
在标识位设置完毕之后,就可以把255 * 255 * 255大小的数据从我们准备好的待发送数据包中copy出来了,注意,前4位已经是标识位了,所以应该从第五个位置开始copy数据。
在数据包都装配完毕之后,就可以往socket的outputSteam中发送数据了。接下来的事情,就是由mysql服务器接收数据并解析,执行查询了。
while (len >= this.maxThreeBytes) { this.packetSequence++; /*设置包的开始位置*/ ① headerPacket.setPosition(0); /*设置这个数据包的大小,splitSize=255 * 255 * 255*/ headerPacket.writeLongInt(splitSize); /*设置数据包的序号*/ headerPacket.writeByte(this.packetSequence); /*origPacketBytes就是sendPacket,所以这里就是把sendPacket中大小为255 * 255 * 255的数据放入headPacket中,headerPacketBytes是headPacket的byte buffer*/ System.arraycopy(origPacketBytes, originalPacketPos, headerPacketBytes, 4, splitSize); int packetLen = splitSize + HEADER_LENGTH; if (!this.useCompression) { this.mysqlOutput.write(headerPacketBytes, 0, splitSize + HEADER_LENGTH); this.mysqlOutput.flush(); } else { Buffer packetToSend; headerPacket.setPosition(0); packetToSend = compressPacket(headerPacket, HEADER_LENGTH, splitSize, HEADER_LENGTH); packetLen = packetToSend.getPosition(); /*往IO的output stream中写数据*/ this.mysqlOutput.write(packetToSend.getByteBuffer(), 0, packetLen); this.mysqlOutput.flush(); } originalPacketPos += splitSize; len -= splitSize; }
iii. 通过readAllResults方法读取查询结果。这个读取的过程与发送过程相反,如果接收到的数据包有多个的话,通过IO不断读取,并根据第packet第4个位置上的序号来组装这些packet。然后把读到的数据组装成resultSet中的rowData,这个结果就是我们要的查询结果了。
结合下面的executeQuery的时序图再理一下思路就更清楚了。
至此,把resultSet一步步的返回给dao,接下来的过程,就是从resultSet中取出rowData,组合成我们自己需要的对象数据了。
总结一下,经过这次对mysql驱动的探索,我发现了更多关于mysql的底层细节,对于以后分析问题解决问题有很大帮助,当然,这里面还有很多细节文中没有写。另外一个就是对于PreparedStatement有了重新的认识,有些东西往往都是想当然得出来的结论,真相还是要靠实践来发现。
评论
56 楼
showgo1306
2012-09-28
随便说说:
发现几个问题,中国的程序员果然如传闻中的那样会随意指责别人;还有就是现在的程序员(当然包括我自己)果然已经很没有耐心了,记得javaeye上有一篇来自中关村的大侠写的文章,说公司培养程序员一定要培养他们的耐心,果然有些道理;不,是灰常有道理;连看这么一点文字的耐心都没有,还能奢望看什么。。。
发现几个问题,中国的程序员果然如传闻中的那样会随意指责别人;还有就是现在的程序员(当然包括我自己)果然已经很没有耐心了,记得javaeye上有一篇来自中关村的大侠写的文章,说公司培养程序员一定要培养他们的耐心,果然有些道理;不,是灰常有道理;连看这么一点文字的耐心都没有,还能奢望看什么。。。
55 楼
WOSHIHOUJINXIN
2012-04-22
我表示看的有点难
54 楼
ZHH2009
2010-06-27
楼主MM现在还在研究MySQL JDBC Driver吗?
关于replication和loadbalace的问题最好把驱动的版本换到最新的5.1.12吧,
5.0.6估计太老了,从5.1.11开始, ReplicationConnection类的实现改过了,
之前对于一个master、多个slave的情形,slave之间是不会进行负载均衡的,
因为在ReplicationConnection类的构造函数中它都用了"jdbc:mysql://"这样的url前缀,
从5.1.11开始变成了"jdbc:mysql:loadbalance://"了,这样多个slave之间可以实现负载均衡,
目前MySQL JDBC Driver有两种负载均衡策略:random 和 bestResponseTime,
默认是random,当然可以通过“bestResponseTime”属性改变它。
关于replication和loadbalace的例子在MySQL JDBC Driver的文档上有:
"4.6. Using Master/Slave Replication with ReplicationConnection"
但是MySQL JDBC Driver这些有关replication和loadbalace的实现还是很弱,
比如还不能自动做到master、slave切换,
读写分离必须要用户明确调用setReadOnly,
我最近也一直在看MySQL JDBC Driver的源代码,主要是想解决一些大并发相关的问题。
P.S.1:
关于"server-side prepared statement",
首先要把"useServerPrepStmts"属性设为true,
然后MySQL Server的版本要>=4.1.0才能启用,
但是有个例外:
MySQL Server 5.0.0 到 MySQL Server 5.0.3这些版本的"server-side prepared statement"不能正常工作,
所以不支持"server-side prepared statement"。
(这个细节在ConnectionImpl类的initializePropsFromServer()方法中可以找到)
我的感觉是:
MySQL Server的不同版本之间真的很折腾,
MySQL JDBC Driver整个实现经常在干判断版本号的事。
P.S.2:
MySQL 5.1的官方文档上说,
只有InnoDB存储引擎才支持XA。
P.S.3:
MySQL JDBC Driver 5.1.12搞了160多个配置属性,
那个since版本号是MySQL JDBC Driver的版本,不是MySQL Server的版本,
这个since版本号在源代码实现中没意义的。
关于replication和loadbalace的问题最好把驱动的版本换到最新的5.1.12吧,
5.0.6估计太老了,从5.1.11开始, ReplicationConnection类的实现改过了,
之前对于一个master、多个slave的情形,slave之间是不会进行负载均衡的,
因为在ReplicationConnection类的构造函数中它都用了"jdbc:mysql://"这样的url前缀,
从5.1.11开始变成了"jdbc:mysql:loadbalance://"了,这样多个slave之间可以实现负载均衡,
目前MySQL JDBC Driver有两种负载均衡策略:random 和 bestResponseTime,
默认是random,当然可以通过“bestResponseTime”属性改变它。
关于replication和loadbalace的例子在MySQL JDBC Driver的文档上有:
"4.6. Using Master/Slave Replication with ReplicationConnection"
但是MySQL JDBC Driver这些有关replication和loadbalace的实现还是很弱,
比如还不能自动做到master、slave切换,
读写分离必须要用户明确调用setReadOnly,
我最近也一直在看MySQL JDBC Driver的源代码,主要是想解决一些大并发相关的问题。
P.S.1:
关于"server-side prepared statement",
首先要把"useServerPrepStmts"属性设为true,
然后MySQL Server的版本要>=4.1.0才能启用,
但是有个例外:
MySQL Server 5.0.0 到 MySQL Server 5.0.3这些版本的"server-side prepared statement"不能正常工作,
所以不支持"server-side prepared statement"。
(这个细节在ConnectionImpl类的initializePropsFromServer()方法中可以找到)
我的感觉是:
MySQL Server的不同版本之间真的很折腾,
MySQL JDBC Driver整个实现经常在干判断版本号的事。
P.S.2:
MySQL 5.1的官方文档上说,
只有InnoDB存储引擎才支持XA。
P.S.3:
MySQL JDBC Driver 5.1.12搞了160多个配置属性,
那个since版本号是MySQL JDBC Driver的版本,不是MySQL Server的版本,
这个since版本号在源代码实现中没意义的。
53 楼
jitabc
2010-05-16
lookdd1 写道
whaosoft 写道
看来lz要好好学习了哦!~
恭喜您,您获得了javaeye2009年度1-6月份"水王"荣誉称号,您常年活跃于各大板块,坚持灌水为主,讨论为辅,始终保持0分的纯洁和神圣,希望您能将这种精神持之以恒的保持,即使每天一份Javaeye小测验也不动摇自己的信念。
那哥们以后能不能说句有用的话,在这浪费je的数据库空间。
52 楼
coffeesweet
2010-05-14
Pre
好像是EA吧
cheney_love 写道
你的类图很漂亮啊,用什么画的
好像是EA吧
51 楼
cheney_love
2010-01-27
你的类图很漂亮啊,用什么画的
50 楼
wkbulletin
2009-07-13
少数几个人了解的知识, 不代表大家都懂. LZ的文章起到抛砖引玉的作用,支持LZ
49 楼
xlongbuilder
2009-07-06
好文
ps:
得改改了吧
引用就引少点
搞这么多烦人啊
ps:
得改改了吧
引用就引少点
搞这么多烦人啊
48 楼
applefeng_52
2009-07-05
annegu 写道
/**
*作者:annegu
*日期:2009-07-01
*/
Mysql是一个中小型关系型数据库管理系统,目前使用的也比较广泛。为了对开发中间dao层的问题能有更深的理解,在遇到问题的时候能够有更多的思路,于是研究了一下mysql驱动的使用,并且在这过程中也发现了一直以来关于PreparedStatement常识理解上的错误,与大家分享。
下面是个最简单的使用jdbc取得数据的应用。在例子之后我将分成4步,分别是①取得连接,②创建PreparedStatement,③设置参数,④执行查询,来分步分析这个过程。除了设置参数那一步之外,其他的我都画了时序图,如果不想看文字的话,可以对着时序图 。文中的第4步是组装mysql协议并发送数据包的关键,而且在这部分的(b)环节,我对于PreparedStatement的应用有详细的代码注释分析,建议大家关注一下。
1、首先我们看到要的到一个数据库连接,得到数据库连接这部分放在DBHelper类中的getConnection方法中实现。Class.forName("com.mysql.jdbc.Driver");用来加载mysql的jdbc驱动。
Mysql的Driver类实现了java.sql.Driver接口,任何数据库提供商的驱动类都必须实现这个接口。在DriverManager类中使用的都是接口Driver类型的驱动,也就是说驱动的使用不依赖于具体的实现,这无疑给我们的使用带来很大的方便。如果需要换用其他的数据库的话,只需要把Class.forName()中的参数换掉就可以了,可以说是非常方便的。
在com.mysql.jdbc.Driver类中,除了构造方法,就是一个static的方法体,它调用了DriverManager的registerDriver()方法,这个方法会加载所有系统提供的驱动,并把它们都假如到具体的驱动类中,当然现在就是mysql的Driver。在这里我们第一次看到了DriverManager类,这个类中提供了jdbc连接的主要操作,创建连接就是在这里完成的,可以说这是一个管理驱动的工具类。
注册驱动首先就是初始化,然后把驱动的信息封装一下放进一个叫做DriverInfo的驱动信息类中,最后放入一个驱动的集合中。初始化工作主要是完成所有驱动的加载。
至于驱动的集合writeDrivers和readDrivers,很有趣的是,无论是registerDriver还是deregisterDriver,都是先对writeDrivers中的数据进行添加或者删除,然后再把writeDrivers中的驱动都拷贝到readDrivers中,但每次取出driver却从来不从writeDrivers中取,都是通过readDrivers来获得。我认为可以这样理解,writeDrivers只负责注册driver与注销driver,而readDrivers只负责提供可用的driver,只有当writeDrivers中准备好了驱动,这些驱动才是可以使用的,所以才能被copy至readDrivers中以备使用。这样一来,对内的注册注销与对外的提供使用就分开来了。
第二步就要根据url和用户名,密码来获得数据库的连接了。url一般都是这样的格式:jdbc:protocol://host_name:port/db_name?parameter_name=param_value。开头部分的protocal是对应于不同的数据库提供商的协议,例如mysql的就是mysql。
DriverManager中有重载了四个getConnection(),因为我们有用户名和密码,就把用户和密码存放在Properties中,最后进入终极getConnection(),如下:
Initialize()简直无所不在,DriverManager中只要使用driver之前,就要检查一下有没有初始化,非常小心。然后开始遍历所有驱动,直到找到一个可用的驱动,用这个驱动来取得一个数据库连接,最后返回这个连接。当然,这是正常的情况,从上面我们可以看到,程序中对异常的处理很仔细。如果连接失败,会记录抛出的第一个异常信息,如果没有找到合适的驱动,就抛出一个08001的错误。
现在重点就是假如一切正常,就应该从driver.connect()返回一个数据库连接,所以我们来看看如何通过url提供的数据库。
很简洁的写法,就是新建了一个mysql的connection,host, port, database给它传进入,让它去连接就对了,props里面是些什么东西呢,就是把url拆解一下,什么host,什么数据库名,然后url后面的一股脑的参数,再把用户跟密码也都放进入,反正就是所有的连接数据都放进入了。
在com.mysql.jdbc.Connection的构造方法里面,会先做一些连接的初始化操作,例如创建PreparedStatement的cache,创建日志等等。然后就进入createNewIO()来建立连接了。
从时序图中可以看到,createNewIO()就是新建了一个com.mysql.jdbc.MysqlIO,利用com.mysql.jdbc.StandardSocketFactory来创建一个socket。然后就由这个mySqlIO来与MySql服务器进行握手(doHandshake()),这个doHandshake主要用来初始化与Mysql server的连接,负责登陆服务器和处理连接错误。在其中会分析所连接的mysql server的版本,根据不同的版本以及是否使用SSL加密数据都有不同的处理方式,并把要传输给数据库server的数据都放在一个叫做packet的buffer中,调用send()方法往outputStream中写入要发送的数据。
2、PreparedStatement stmt = conn.prepareStatement(sql);使用得到的connection创建一个Statement。Statement有许多种,我们常用的就是PreparedStatement,用于执行预编译好的SQL语句,CallableStatement用于调用数据库的存储过程。它们的继承关系如下图所示。
一旦有了一个statement,就可以通过执行statement.executeQuery()并通过ResultSet对象读出查询结果(如果查询有返回结果的话)。
创建statement的方法一般都有重载,我们看下面的prepareStatement:
如果没有指定resultSetType和resultSetConcurrency的话,会给它们默认设置一个值。
ResultSet中的参数常量主要有以下几种:
TYPE_FORWARD_ONLY: ResultSet的游标只能向前移动。
TYPE_SCROLL_INSENSITIVE:ResultSet的游标可以滚动,但对于resultSet下的数据改变不敏感。
TYPE_SCROLL_SENSITIVE:ResultSet的游标可以滚动,但对于resultSet下的数据改变是敏感的。
CONCUR_READ_ONLY:不可以更新的ResultSet的并发模式。
CONCUR_UPDATABLE:可以更新的ResultSet的并发模式。
FETCH_FORWARD:按正向(即从第一个到最后一个)处理结果集中的行。
FETCH_REVERSE:按反向(即从最后一个到第一个)处理结果集中的行处理。
FETCH_UNKNOWN:结果集中的行的处理顺序未知。
CLOSE_CURSORS_AT_COMMIT:调用Connection.commit方法时应该关闭 ResultSet 对
HOLD_CURSORS_OVER_COMMIT:调用Connection.commit方法时不应关闭ResultSet对象。
prepareStatement的创建如下图所示:
在new ParseInfo中,会对这个sql语句进行分析,例如看看这个sql是什么语句;有没有limit条件语句,还有一个重要的工作,如果使用的是PreparedStatement来准备sql语句的话,会在这里把sql语句进行分解。我们知道PreparedStatement对象在实例化创建时就被设置了一个sql语句,使用PreparedStatement对象执行的sql语句在首次发送到数据库时,sql语句就会被编译,这样当多次执行同一个sql语句时,mysql就不用每次都去编译sql语句了。这个sql语句如果包含参数的话,可以用问号(”?”)来为参数进行占位,而不需要立即为参数赋值,而在语句执行之前,必须通过适当的set***()来为问号处的参数赋值。New ParseInfo()中,包含了参数的sql语句就会被分解为多段,放在staticSql中,以便需要设置参数时定位参数的位置。假如sql语句为“select * from adv where id = ? and name = ?”的话,那么staticSql中的元素就是3个,staticSql[3]={ ”select * from adv where id = ”, ” and name = ” , ””}。注意数组中最后一个元素,在这个例子中是””,因为我的例子里面最后一个就是”?”,如果sql语句是这样的“select * from adv where id = ? and name = ? order by id”的话,staticSql就变成是这样的{ ”select * from adv where id = ”, ” and name = ” , ” order by id”}。
3、stmt.setInt(1, new Integer(1));
设置sql语句中的参数值。
对于参数而言,PreparedStatement中一共有四个变量来储存它们,分别是
a) byte[][] parameterValues:参数转换为byte后的值。
b) InputStream[] parameterStreams:只有在调用存储过程batch(CallableStatement)的时候才会用到它,否则它的数组中的值设置为null。
c) boolean[] isStream:是否为stream的标志,如果调用的是preparedStatement,isStream数组中的值均为false,若调用的是CallableStatement,则均设置为true。
d) boolean[] isNull:标识参数是否为空,设置为false。
这四个变量的一维数组的大小都是一样的,sql语句中有几个待set的参数(几个问号),一维的元素个数就是多大。
4、ResultSet rs = stmt.executeQuery();
一切准备就绪,开始执行查询罗!
a) 检查preparedStatement是否已关闭,如果已关闭,抛出一个SQLError.SQL_STATE_CONNECTION_NOT_OPEN的错误。
b) fillSendPacket:创建数据包,其中包含了要发送到服务器的查询。
这个sendPacket就是mysql驱动要发送给数据库服务器的协议数据。一般来说,协议的数据格式有两种,一种是二进制流的格式,还有一种是文本的格式。文本协议就是基本上人可以直接阅读的协议,一般是用ascii字符集,也有用utf8格式的,优点是便于理解,读起来方便,扩充容易,缺点就是解析的时候比较麻烦,而且占用的空间比较大,冗余的数据比较多。二进制格式话,就需要服务器与客户端协议规定固定的数据结构,哪个位置放什么数据,如果单独看协议内容的话,很难理解数据含义,优点就是数据量小,解析的时候只要根据固定位置的值就能知道具体标识什么意义。
在这里使用的是二进制流的格式,也就是说协议中的数据格式是固定的,而且都要转换成二进制。格式为第一个byte标识操作信号,后面开始就是完整的sql语句的二进制流,请看下面的代码分析。
假如sql语句为“select * from adv where id = ?”的话,这个sendPacket中第一个byte的值就是3(MysqlDefs.QUERY的int值),后面接着的就是填充了参数值的完整的sql语句字符串(例如:select * from adv where id = 1)转换成的byte格式。
于是,我们看到,好像sql语句在这里就已经不是带”?”的preparedStatement,而是在驱动里面把参数替代到”?”中,再把完整的sql语句发送给mysql server来编译,那么尽管只是参数改变,但对于mysql server来说,每次都是新的sql语句,都要进行编译的。这与我们之前一直理解的PreparedStatement完全不一样。照理来说,应该把带”?”的sql语句发送给数据库server,由mysql server来编译这个带”?”的sql语句,然后用实际的参数来替代”?”,这样才是实现了sql语句只编译一次的效果。sql语句预编译的功能取决于server端,oracle就是支持sql预编译的。
所以说,从mysql驱动的PreparedStatement里面,好像我们并没有看到mysql支持预编译功能的证据。(实际测试也表明,如果server没有预编译功能的话,PreparedStatement和Statement的效率几乎一样,甚至当使用次数不多的时候,PreparedStatement比Statement还要慢一些)。
但是并不是说PreparedStatement除了给我们带来高效率就没有其他作用了,它还有非常好的其他作用:
i. 极大的提高了sql语句的安全性,可以防止sql注入
ii. 代码结构清晰,易于理解,便于维护。
2009-07-02增加(感谢gembler):其实,在mysql5上的版本是支持预编译sql功能的。我用的驱动是5.0.6的,在com.mysql.jdbc.Connection中有一个参数useServerPreparedStmts,表明是否使用预编译功能,所以如果把useServerPreparedStmts置为true的话,mysql驱动可以通过PreparedStatement的子类ServerPreparedStatement来实现真正的PreparedStatement的功能。在这个类的serverExecute方法里面,就负责告诉server,用现在提供的参数来动态绑定到编译好的sql语句上。所以说,ServerPreparedStatement才是真正实现了所谓prepare statement。
c) 设置当前的数据库名,并把之前的数据库名记录下来,在查询完成之后还要恢复原状。
d) 检查一下之前是否有缓存的数据,如果不久之前执行过这个查询,并且缓存了数据的话,就直接从缓存中取出。
e) 如果sql查询没有限制条件的话,为其设置默认的返回行数,若preparedStatement中已经设置了maxRows的话,就使用它。
f) executeInternal:执行查询。
i. 设置当前数据库连接,并调用connection的execSQL来执行查询.然后继续把要发送的查询包,就是之间组装完毕的sendPacket传递进入MysqlIO的sqlQueryDirect()。
ii. 接下来就要往server端发送我们的查询指令啦(sendCommand),说到发送数据,不禁要问,如果这个待发送的数据包超级大,难道每次都是一次性的发送吗?当然不是,如果数据包超过规定的最大值的话,就会把它分割一下,分成几个不超过最大值的数据包来发送。
所以可以肯定,在分割的过程中,除了最后一个数据包,其他数据包的大小都是一样的。那就这样的数据包直接切割了进行发送的话,假如现在被分成了三个数据包,发送给mysql server,服务器怎么知道那个包是第一个呢,它读数据该从什么地方开始读呢,这都是问题,所以,我们要给每个数据包的前面加上一点属性标志,这个标志一共占了4个byte。从代码①处开始就是头标识位的设置。第一位表示数据包的开始位置,就是数据存放的起始位置,一般都设置为0,就是从第一个位置开始。第二和第三个字节标识了这个数据包的大小,注意的是,这个大小是出去标识的4个字节的大小,对于非最后一个数据包来说,这个大小都是一样的,就是splitSize,也就是maxThreeBytes,它的值是255 * 255 * 255。
最后一个字节中存放的就是数据包的编号了,从0开始递增。
在标识位设置完毕之后,就可以把255 * 255 * 255大小的数据从我们准备好的待发送数据包中copy出来了,注意,前4位已经是标识位了,所以应该从第五个位置开始copy数据。
在数据包都装配完毕之后,就可以往socket的outputSteam中发送数据了。接下来的事情,就是由mysql服务器接收数据并解析,执行查询了。
iii. 通过readAllResults方法读取查询结果。这个读取的过程与发送过程相反,如果接收到的数据包有多个的话,通过IO不断读取,并根据第packet第4个位置上的序号来组装这些packet。然后把读到的数据组装成resultSet中的rowData,这个结果就是我们要的查询结果了。
结合下面的executeQuery的时序图再理一下思路就更清楚了。
至此,把resultSet一步步的返回给dao,接下来的过程,就是从resultSet中取出rowData,组合成我们自己需要的对象数据了。
总结一下,经过这次对mysql驱动的探索,我发现了更多关于mysql的底层细节,对于以后分析问题解决问题有很大帮助,当然,这里面还有很多细节文中没有写。另外一个就是对于PreparedStatement有了重新的认识,有些东西往往都是想当然得出来的结论,真相还是要靠实践来发现。
*作者:annegu
*日期:2009-07-01
*/
Mysql是一个中小型关系型数据库管理系统,目前使用的也比较广泛。为了对开发中间dao层的问题能有更深的理解,在遇到问题的时候能够有更多的思路,于是研究了一下mysql驱动的使用,并且在这过程中也发现了一直以来关于PreparedStatement常识理解上的错误,与大家分享。
下面是个最简单的使用jdbc取得数据的应用。在例子之后我将分成4步,分别是①取得连接,②创建PreparedStatement,③设置参数,④执行查询,来分步分析这个过程。除了设置参数那一步之外,其他的我都画了时序图,如果不想看文字的话,可以对着时序图 。文中的第4步是组装mysql协议并发送数据包的关键,而且在这部分的(b)环节,我对于PreparedStatement的应用有详细的代码注释分析,建议大家关注一下。
public class DBHelper { public static Connection getConnection() { Connection conn = null; try { Class.forName("com.mysql.jdbc.Driver"); conn = DriverManager.getConnection("jdbc:mysql://localhost/ad?useUnicode=true&characterEncoding=GBK&jdbcCompliantTruncation=false", "root", "root"); } catch (Exception e) { e.printStackTrace(); } return conn; } } /*dao中的方法*/ public List<Adv> getAllAdvs() { Connection conn = null; ResultSet rs = null; PreparedStatement stmt = null; String sql = "select * from adv where id = ?"; List<Adv> advs = new ArrayList<Adv>(); conn = DBHelper.getConnection(); if (conn != null) { try { stmt = conn.prepareStatement(sql); stmt.setInt(1, new Integer(1)); rs = stmt.executeQuery(); if (rs != null) { while (rs.next()) { Adv adv = new Adv(); adv.setId(rs.getLong(1)); adv.setName(rs.getString(2)); adv.setDesc(rs.getString(3)); adv.setPicUrl(rs.getString(4)); advs.add(adv); } } } catch (SQLException e) { e.printStackTrace(); } finally { try { stmt.close(); conn.close(); } catch (SQLException e) { e.printStackTrace(); } } } return advs; }
1、首先我们看到要的到一个数据库连接,得到数据库连接这部分放在DBHelper类中的getConnection方法中实现。Class.forName("com.mysql.jdbc.Driver");用来加载mysql的jdbc驱动。
public class Driver extends NonRegisteringDriver implements java.sql.Driver { static { try { java.sql.DriverManager.registerDriver(new Driver()); } catch (SQLException E) { throw new RuntimeException("Can't register driver!"); } } public Driver() throws SQLException { } }
Mysql的Driver类实现了java.sql.Driver接口,任何数据库提供商的驱动类都必须实现这个接口。在DriverManager类中使用的都是接口Driver类型的驱动,也就是说驱动的使用不依赖于具体的实现,这无疑给我们的使用带来很大的方便。如果需要换用其他的数据库的话,只需要把Class.forName()中的参数换掉就可以了,可以说是非常方便的。
在com.mysql.jdbc.Driver类中,除了构造方法,就是一个static的方法体,它调用了DriverManager的registerDriver()方法,这个方法会加载所有系统提供的驱动,并把它们都假如到具体的驱动类中,当然现在就是mysql的Driver。在这里我们第一次看到了DriverManager类,这个类中提供了jdbc连接的主要操作,创建连接就是在这里完成的,可以说这是一个管理驱动的工具类。
public static synchronized void registerDriver(java.sql.Driver driver) throws SQLException { if (!initialized) { initialize(); } DriverInfo di = new DriverInfo(); /*把driver的信息封装一下,组成一个DriverInfo对象*/ di.driver = driver; di.driverClass = driver.getClass(); di.driverClassName = di.driverClass.getName(); writeDrivers.addElement(di); println("registerDriver: " + di); readDrivers = (java.util.Vector) writeDrivers.clone(); }
注册驱动首先就是初始化,然后把驱动的信息封装一下放进一个叫做DriverInfo的驱动信息类中,最后放入一个驱动的集合中。初始化工作主要是完成所有驱动的加载。
至于驱动的集合writeDrivers和readDrivers,很有趣的是,无论是registerDriver还是deregisterDriver,都是先对writeDrivers中的数据进行添加或者删除,然后再把writeDrivers中的驱动都拷贝到readDrivers中,但每次取出driver却从来不从writeDrivers中取,都是通过readDrivers来获得。我认为可以这样理解,writeDrivers只负责注册driver与注销driver,而readDrivers只负责提供可用的driver,只有当writeDrivers中准备好了驱动,这些驱动才是可以使用的,所以才能被copy至readDrivers中以备使用。这样一来,对内的注册注销与对外的提供使用就分开来了。
第二步就要根据url和用户名,密码来获得数据库的连接了。url一般都是这样的格式:jdbc:protocol://host_name:port/db_name?parameter_name=param_value。开头部分的protocal是对应于不同的数据库提供商的协议,例如mysql的就是mysql。
DriverManager中有重载了四个getConnection(),因为我们有用户名和密码,就把用户和密码存放在Properties中,最后进入终极getConnection(),如下:
private static Connection getConnection( String url, java.util.Properties info, ClassLoader callerCL) throws SQLException { java.util.Vector drivers = null; ... if (!initialized) { initialize(); } /*取得连接使用的driver从readDrivers中取*/ synchronized (DriverManager.class){ drivers = readDrivers; } SQLException reason = null; for (int i = 0; i < drivers.size(); i++) { DriverInfo di = (DriverInfo)drivers.elementAt(i); if ( getCallerClass(callerCL, di.driverClassName ) != di.driverClass ) { continue; } try { /*找到可供使用的驱动,连接数据库server*/ Connection result = di.driver.connect(url, info); if (result != null) { return (result); } } catch (SQLException ex) { if (reason == null) { reason = ex; } } } if (reason != null) { println("getConnection failed: " + reason); throw reason; } throw new SQLException("No suitable driver found for "+ url, "08001"); }
Initialize()简直无所不在,DriverManager中只要使用driver之前,就要检查一下有没有初始化,非常小心。然后开始遍历所有驱动,直到找到一个可用的驱动,用这个驱动来取得一个数据库连接,最后返回这个连接。当然,这是正常的情况,从上面我们可以看到,程序中对异常的处理很仔细。如果连接失败,会记录抛出的第一个异常信息,如果没有找到合适的驱动,就抛出一个08001的错误。
现在重点就是假如一切正常,就应该从driver.connect()返回一个数据库连接,所以我们来看看如何通过url提供的数据库。
public java.sql.Connection connect(String url, Properties info) throws SQLException { Properties props = null; if ((props = parseURL(url, info)) == null) { return null; } try { Connection newConn = new com.mysql.jdbc.Connection(host(props), port(props), props, database(props), url); return newConn; } catch (SQLException sqlEx) { throw sqlEx; } catch (Exception ex) { throw SQLError.createSQLException(...); } }
很简洁的写法,就是新建了一个mysql的connection,host, port, database给它传进入,让它去连接就对了,props里面是些什么东西呢,就是把url拆解一下,什么host,什么数据库名,然后url后面的一股脑的参数,再把用户跟密码也都放进入,反正就是所有的连接数据都放进入了。
在com.mysql.jdbc.Connection的构造方法里面,会先做一些连接的初始化操作,例如创建PreparedStatement的cache,创建日志等等。然后就进入createNewIO()来建立连接了。
从时序图中可以看到,createNewIO()就是新建了一个com.mysql.jdbc.MysqlIO,利用com.mysql.jdbc.StandardSocketFactory来创建一个socket。然后就由这个mySqlIO来与MySql服务器进行握手(doHandshake()),这个doHandshake主要用来初始化与Mysql server的连接,负责登陆服务器和处理连接错误。在其中会分析所连接的mysql server的版本,根据不同的版本以及是否使用SSL加密数据都有不同的处理方式,并把要传输给数据库server的数据都放在一个叫做packet的buffer中,调用send()方法往outputStream中写入要发送的数据。
2、PreparedStatement stmt = conn.prepareStatement(sql);使用得到的connection创建一个Statement。Statement有许多种,我们常用的就是PreparedStatement,用于执行预编译好的SQL语句,CallableStatement用于调用数据库的存储过程。它们的继承关系如下图所示。
一旦有了一个statement,就可以通过执行statement.executeQuery()并通过ResultSet对象读出查询结果(如果查询有返回结果的话)。
创建statement的方法一般都有重载,我们看下面的prepareStatement:
public java.sql.PreparedStatement prepareStatement(String sql) throws SQLException { return prepareStatement(sql, java.sql.ResultSet.TYPE_FORWARD_ONLY, java.sql.ResultSet.CONCUR_READ_ONLY); } public java.sql.PreparedStatement prepareStatement(String sql, int resultSetType, int resultSetConcurrency) throws SQLException;
如果没有指定resultSetType和resultSetConcurrency的话,会给它们默认设置一个值。
ResultSet中的参数常量主要有以下几种:
TYPE_FORWARD_ONLY: ResultSet的游标只能向前移动。
TYPE_SCROLL_INSENSITIVE:ResultSet的游标可以滚动,但对于resultSet下的数据改变不敏感。
TYPE_SCROLL_SENSITIVE:ResultSet的游标可以滚动,但对于resultSet下的数据改变是敏感的。
CONCUR_READ_ONLY:不可以更新的ResultSet的并发模式。
CONCUR_UPDATABLE:可以更新的ResultSet的并发模式。
FETCH_FORWARD:按正向(即从第一个到最后一个)处理结果集中的行。
FETCH_REVERSE:按反向(即从最后一个到第一个)处理结果集中的行处理。
FETCH_UNKNOWN:结果集中的行的处理顺序未知。
CLOSE_CURSORS_AT_COMMIT:调用Connection.commit方法时应该关闭 ResultSet 对
HOLD_CURSORS_OVER_COMMIT:调用Connection.commit方法时不应关闭ResultSet对象。
prepareStatement的创建如下图所示:
在new ParseInfo中,会对这个sql语句进行分析,例如看看这个sql是什么语句;有没有limit条件语句,还有一个重要的工作,如果使用的是PreparedStatement来准备sql语句的话,会在这里把sql语句进行分解。我们知道PreparedStatement对象在实例化创建时就被设置了一个sql语句,使用PreparedStatement对象执行的sql语句在首次发送到数据库时,sql语句就会被编译,这样当多次执行同一个sql语句时,mysql就不用每次都去编译sql语句了。这个sql语句如果包含参数的话,可以用问号(”?”)来为参数进行占位,而不需要立即为参数赋值,而在语句执行之前,必须通过适当的set***()来为问号处的参数赋值。New ParseInfo()中,包含了参数的sql语句就会被分解为多段,放在staticSql中,以便需要设置参数时定位参数的位置。假如sql语句为“select * from adv where id = ? and name = ?”的话,那么staticSql中的元素就是3个,staticSql[3]={ ”select * from adv where id = ”, ” and name = ” , ””}。注意数组中最后一个元素,在这个例子中是””,因为我的例子里面最后一个就是”?”,如果sql语句是这样的“select * from adv where id = ? and name = ? order by id”的话,staticSql就变成是这样的{ ”select * from adv where id = ”, ” and name = ” , ” order by id”}。
3、stmt.setInt(1, new Integer(1));
设置sql语句中的参数值。
对于参数而言,PreparedStatement中一共有四个变量来储存它们,分别是
a) byte[][] parameterValues:参数转换为byte后的值。
b) InputStream[] parameterStreams:只有在调用存储过程batch(CallableStatement)的时候才会用到它,否则它的数组中的值设置为null。
c) boolean[] isStream:是否为stream的标志,如果调用的是preparedStatement,isStream数组中的值均为false,若调用的是CallableStatement,则均设置为true。
d) boolean[] isNull:标识参数是否为空,设置为false。
这四个变量的一维数组的大小都是一样的,sql语句中有几个待set的参数(几个问号),一维的元素个数就是多大。
4、ResultSet rs = stmt.executeQuery();
一切准备就绪,开始执行查询罗!
a) 检查preparedStatement是否已关闭,如果已关闭,抛出一个SQLError.SQL_STATE_CONNECTION_NOT_OPEN的错误。
b) fillSendPacket:创建数据包,其中包含了要发送到服务器的查询。
这个sendPacket就是mysql驱动要发送给数据库服务器的协议数据。一般来说,协议的数据格式有两种,一种是二进制流的格式,还有一种是文本的格式。文本协议就是基本上人可以直接阅读的协议,一般是用ascii字符集,也有用utf8格式的,优点是便于理解,读起来方便,扩充容易,缺点就是解析的时候比较麻烦,而且占用的空间比较大,冗余的数据比较多。二进制格式话,就需要服务器与客户端协议规定固定的数据结构,哪个位置放什么数据,如果单独看协议内容的话,很难理解数据含义,优点就是数据量小,解析的时候只要根据固定位置的值就能知道具体标识什么意义。
在这里使用的是二进制流的格式,也就是说协议中的数据格式是固定的,而且都要转换成二进制。格式为第一个byte标识操作信号,后面开始就是完整的sql语句的二进制流,请看下面的代码分析。
protected Buffer fillSendPacket(byte[][] batchedParameterStrings, InputStream[] batchedParameterStreams, boolean[] batchedIsStream, int[] batchedStreamLengths) throws SQLException { // 从connection的IO中得到发送数据包,首先清空其中的数据 Buffer sendPacket = this.connection.getIO().getSharedSendPacket(); sendPacket.clear(); /* 数据包的第一位为一个操作标识符(MysqlDefs.QUERY),表示驱动向服务器发送的连接的操作信号,包括有QUERY, PING, RELOAD, SHUTDOWN, PROCESS_INFO, QUIT, SLEEP等等,这个操作信号并不是针对sql语句操作而言的CRUD操作,从提供的几种参数来看,这个操作是针对服务器的一个操作。一般而言,使用到的都是MysqlDefs.QUERY,表示发送的是要执行sql语句的操作。 */ sendPacket.writeByte((byte) MysqlDefs.QUERY); boolean useStreamLengths = this.connection .getUseStreamLengthsInPrepStmts(); int ensurePacketSize = 0; for (int i = 0; i < batchedParameterStrings.length; i++) { if (batchedIsStream[i] && useStreamLengths) { ensurePacketSize += batchedStreamLengths[i]; } } /* 判断这个sendPacket的byte buffer够不够大,不够大的话,按照1.25倍来扩充buffer */ if (ensurePacketSize != 0) { sendPacket.ensureCapacity(ensurePacketSize); } /* 遍历所有的参数。在prepareStatement阶段的new ParseInfo()中,驱动曾经对sql语句进行过分割,如果含有以问号标识的参数占位符的话,就记录下这个占位符的位置,依据这个位置把sql分割成多段,放在了一个名为staticSql的字符串中。这里就开始把sql语句进行拼装,把staticSql和parameterValues进行组合,放在操作符的后面。 */ for (int i = 0; i < batchedParameterStrings.length; i++) { /* batchedParameterStrings就是parameterValues,batchedParameterStreams就是parameterStreams,如果两者都为null,说明在参数的设置过程中出了错,立即抛出错误。 */ if ((batchedParameterStrings[i] == null) && (batchedParameterStreams[i] == null)) { throw SQLError.createSQLException(Messages .getString("PreparedStatement.40") //$NON-NLS-1$ + (i + 1), SQLError.SQL_STATE_WRONG_NO_OF_PARAMETERS); } /*在sendPacket中加入staticSql数组中的元素,就是分割出来的没有”?”的sql语句,并把字符串转换成byte。 */ sendPacket.writeBytesNoNull(this.staticSqlStrings[i]); /* batchedIsStream就是isStream,如果参数是通过CallableStatement传递进来的话,batchedIsStream[i]==true,就用batchedParameterStreams中的值填充到问号占的参数位置中去。 */ if (batchedIsStream[i]) { streamToBytes(sendPacket, batchedParameterStreams[i], true, batchedStreamLengths[i], useStreamLengths); } else { /*否则的话,就用batchedParameterStrings,也就是parameterValues来填充参数位置。在循环中,这个操作是跟在staticSql后面的,因此就把第i个参数加到了第i个staticSql段中。参考前面的staticSql的例子,发现当循环结束的时候,原始sql语句最后一个”?”之前的sql语句就拼成了正确的语句了。 */ sendPacket.writeBytesNoNull(batchedParameterStrings[i]); } } /*由于在原始的包含问号的sql语句中,在最后一个”?”后面可能还有order by等语句,因此staticSql数组中的元素个数一定比参数的个数多1,所以这里把staticSqlString中的最后一段sql语句放入sendPacket中。 */ sendPacket .writeBytesNoNull(this.staticSqlStrings[batchedParameterStrings.length]); return sendPacket; }
假如sql语句为“select * from adv where id = ?”的话,这个sendPacket中第一个byte的值就是3(MysqlDefs.QUERY的int值),后面接着的就是填充了参数值的完整的sql语句字符串(例如:select * from adv where id = 1)转换成的byte格式。
于是,我们看到,好像sql语句在这里就已经不是带”?”的preparedStatement,而是在驱动里面把参数替代到”?”中,再把完整的sql语句发送给mysql server来编译,那么尽管只是参数改变,但对于mysql server来说,每次都是新的sql语句,都要进行编译的。这与我们之前一直理解的PreparedStatement完全不一样。照理来说,应该把带”?”的sql语句发送给数据库server,由mysql server来编译这个带”?”的sql语句,然后用实际的参数来替代”?”,这样才是实现了sql语句只编译一次的效果。sql语句预编译的功能取决于server端,oracle就是支持sql预编译的。
所以说,从mysql驱动的PreparedStatement里面,好像我们并没有看到mysql支持预编译功能的证据。(实际测试也表明,如果server没有预编译功能的话,PreparedStatement和Statement的效率几乎一样,甚至当使用次数不多的时候,PreparedStatement比Statement还要慢一些)。
但是并不是说PreparedStatement除了给我们带来高效率就没有其他作用了,它还有非常好的其他作用:
i. 极大的提高了sql语句的安全性,可以防止sql注入
ii. 代码结构清晰,易于理解,便于维护。
2009-07-02增加(感谢gembler):其实,在mysql5上的版本是支持预编译sql功能的。我用的驱动是5.0.6的,在com.mysql.jdbc.Connection中有一个参数useServerPreparedStmts,表明是否使用预编译功能,所以如果把useServerPreparedStmts置为true的话,mysql驱动可以通过PreparedStatement的子类ServerPreparedStatement来实现真正的PreparedStatement的功能。在这个类的serverExecute方法里面,就负责告诉server,用现在提供的参数来动态绑定到编译好的sql语句上。所以说,ServerPreparedStatement才是真正实现了所谓prepare statement。
c) 设置当前的数据库名,并把之前的数据库名记录下来,在查询完成之后还要恢复原状。
d) 检查一下之前是否有缓存的数据,如果不久之前执行过这个查询,并且缓存了数据的话,就直接从缓存中取出。
e) 如果sql查询没有限制条件的话,为其设置默认的返回行数,若preparedStatement中已经设置了maxRows的话,就使用它。
f) executeInternal:执行查询。
i. 设置当前数据库连接,并调用connection的execSQL来执行查询.然后继续把要发送的查询包,就是之间组装完毕的sendPacket传递进入MysqlIO的sqlQueryDirect()。
ii. 接下来就要往server端发送我们的查询指令啦(sendCommand),说到发送数据,不禁要问,如果这个待发送的数据包超级大,难道每次都是一次性的发送吗?当然不是,如果数据包超过规定的最大值的话,就会把它分割一下,分成几个不超过最大值的数据包来发送。
所以可以肯定,在分割的过程中,除了最后一个数据包,其他数据包的大小都是一样的。那就这样的数据包直接切割了进行发送的话,假如现在被分成了三个数据包,发送给mysql server,服务器怎么知道那个包是第一个呢,它读数据该从什么地方开始读呢,这都是问题,所以,我们要给每个数据包的前面加上一点属性标志,这个标志一共占了4个byte。从代码①处开始就是头标识位的设置。第一位表示数据包的开始位置,就是数据存放的起始位置,一般都设置为0,就是从第一个位置开始。第二和第三个字节标识了这个数据包的大小,注意的是,这个大小是出去标识的4个字节的大小,对于非最后一个数据包来说,这个大小都是一样的,就是splitSize,也就是maxThreeBytes,它的值是255 * 255 * 255。
最后一个字节中存放的就是数据包的编号了,从0开始递增。
在标识位设置完毕之后,就可以把255 * 255 * 255大小的数据从我们准备好的待发送数据包中copy出来了,注意,前4位已经是标识位了,所以应该从第五个位置开始copy数据。
在数据包都装配完毕之后,就可以往socket的outputSteam中发送数据了。接下来的事情,就是由mysql服务器接收数据并解析,执行查询了。
while (len >= this.maxThreeBytes) { this.packetSequence++; /*设置包的开始位置*/ ① headerPacket.setPosition(0); /*设置这个数据包的大小,splitSize=255 * 255 * 255*/ headerPacket.writeLongInt(splitSize); /*设置数据包的序号*/ headerPacket.writeByte(this.packetSequence); /*origPacketBytes就是sendPacket,所以这里就是把sendPacket中大小为255 * 255 * 255的数据放入headPacket中,headerPacketBytes是headPacket的byte buffer*/ System.arraycopy(origPacketBytes, originalPacketPos, headerPacketBytes, 4, splitSize); int packetLen = splitSize + HEADER_LENGTH; if (!this.useCompression) { this.mysqlOutput.write(headerPacketBytes, 0, splitSize + HEADER_LENGTH); this.mysqlOutput.flush(); } else { Buffer packetToSend; headerPacket.setPosition(0); packetToSend = compressPacket(headerPacket, HEADER_LENGTH, splitSize, HEADER_LENGTH); packetLen = packetToSend.getPosition(); /*往IO的output stream中写数据*/ this.mysqlOutput.write(packetToSend.getByteBuffer(), 0, packetLen); this.mysqlOutput.flush(); } originalPacketPos += splitSize; len -= splitSize; }
iii. 通过readAllResults方法读取查询结果。这个读取的过程与发送过程相反,如果接收到的数据包有多个的话,通过IO不断读取,并根据第packet第4个位置上的序号来组装这些packet。然后把读到的数据组装成resultSet中的rowData,这个结果就是我们要的查询结果了。
结合下面的executeQuery的时序图再理一下思路就更清楚了。
至此,把resultSet一步步的返回给dao,接下来的过程,就是从resultSet中取出rowData,组合成我们自己需要的对象数据了。
总结一下,经过这次对mysql驱动的探索,我发现了更多关于mysql的底层细节,对于以后分析问题解决问题有很大帮助,当然,这里面还有很多细节文中没有写。另外一个就是对于PreparedStatement有了重新的认识,有些东西往往都是想当然得出来的结论,真相还是要靠实践来发现。
47 楼
lixinso
2009-07-04
好帖,一定要顶
46 楼
mikewang
2009-07-04
kaipingk 写道
mikewang 写道
1, 数据库是否必须支持事务:
这个你不要扣概念了。在中国还有百万富翁之称呢,你到津巴布韦去自称亿万富翁,笑死你。
mikewang 写道
2, MyISAM是支持事务的<====> MyISAM 只是一个数据文件的格式, 和事物没有任何关系
你怎么前边不搭后边。人们都称MyISAM存储引擎,你却在这里只说人家是数据文件格式
mikewang 写道
3, 你不会认为事务是由文件格式或者文件系统提供的吧?
看来你对MySQL事物的ACDI很了解啊,那就说说MySQL+MyISAM的文件级别的锁怎么实现 我不了解得ACDI吧。
再贴一点
引用
By "transactional table type" I mean either InnoDB or BDB (but the latter is only included in -max versions). There are also some non-MySQL AB storage engines in the wild that might do the trick. And possibly there might be even more in the future.
MyISAM does not support transactions and probably never will.
But we are working on a feature that can rollback commands that were active when the MySQL server crashed. It will also be possible to explicitly rollback everything from the point of a LOCK TABLES as long as no UNLOCK TABLES has been done so far.
Please do not think of it as transactional. The way MyISAM stores data does not guarantee you anything when the machine (or better the operating system) crashes. You won't be able to undo nor redo MyISAM tables to a clean state.
Regards
Ingo Strüwing, Senior Software Developer - Storage Engines
MySQL AB, www.mysql.com
人家自己都说不支持,你还非得给别人脸上贴金,还有我想等会你干脆根据你的理论说,基于MyISAM存储引擎的MySQL is not 数据库。。。
ok 你说的是对的, 确实我对MySQL 不是能了解, 站在数据库的角度说, Mysql 确实有不小的差距,有很多人认为MySQL 是dabatase, 但你也说了,津巴布韦也有亿万富翁。
45 楼
kaipingk
2009-07-03
mikewang 写道
1, 数据库是否必须支持事务:
这个你不要扣概念了。在中国还有百万富翁之称呢,你到津巴布韦去自称亿万富翁,笑死你。
mikewang 写道
2, MyISAM是支持事务的<====> MyISAM 只是一个数据文件的格式, 和事物没有任何关系
你怎么前边不搭后边。人们都称MyISAM存储引擎,你却在这里只说人家是数据文件格式
mikewang 写道
3, 你不会认为事务是由文件格式或者文件系统提供的吧?
看来你对MySQL事物的ACDI很了解啊,那就说说MySQL+MyISAM的文件级别的锁怎么实现 我不了解得ACDI吧。
再贴一点
引用
By "transactional table type" I mean either InnoDB or BDB (but the latter is only included in -max versions). There are also some non-MySQL AB storage engines in the wild that might do the trick. And possibly there might be even more in the future.
MyISAM does not support transactions and probably never will.
But we are working on a feature that can rollback commands that were active when the MySQL server crashed. It will also be possible to explicitly rollback everything from the point of a LOCK TABLES as long as no UNLOCK TABLES has been done so far.
Please do not think of it as transactional. The way MyISAM stores data does not guarantee you anything when the machine (or better the operating system) crashes. You won't be able to undo nor redo MyISAM tables to a clean state.
Regards
Ingo Strüwing, Senior Software Developer - Storage Engines
MySQL AB, www.mysql.com
人家自己都说不支持,你还非得给别人脸上贴金,还有我想等会你干脆根据你的理论说,基于MyISAM存储引擎的MySQL is not 数据库。。。
44 楼
bsq519
2009-07-03
很经典,以后慢慢消化
43 楼
mikewang
2009-07-03
rxgp02a 写道
MyISAM真的支持事务吗?
http://dev.mysql.com/doc/refman/5.4/en/myisam-storage-engine.html
这上面Transactions写的是NO呀。
你不会认为事务是由文件格式或者文件系统提供的吧?
MyISAM 只是一个数据文件的格式, 和事物没有任何关系, 所谓的MyISAM支持本地事物是指可以使用posix文件锁在MyISAM数据文件上提供表级别的锁,MyISAM中一个表就是一个文件, 文件锁就是表锁,并发性可想而知了。 正是如此, 所以这个功能才不被建议使用!在后续的某个版本提供的行锁(排他性的)使用的posix记录锁实现的, 效果也很差。
mysql 提供lock 语句, 你去看看lock的文档,上面应该说的很清楚的。
顺便说一句, 我没用过mysql。mysql的文档我也没读过。我以前为postgresql捐献过代码, 在pgsql开发组中混的几天,所以听那帮大牛说过不少次mysql的优缺点和问题。 所以才对mysql 略微有一些了解。
42 楼
rxgp02a
2009-07-03
mikewang 写道
kaipingk 写道
不要太概念了,概念的东西都是理论的东西,具体实现和概念是有差距的。 数据库必须支持事务吗?完全可以做一个不支持事物的数据库啊(MYSQL MyISAM引擎)! 而且完全可以实现一个不抛异常的api事务操作实现啊(自己实现一个事务接口不给你抛任何异常)! 总得把每个函数的作用搞搞清楚吧---应该是吧没有具体实现搞清楚嘛(我又没反对不该搞清楚)。我的意思就是很多东西就那么回事,没有什么大不了的!
1, 数据库是否必须支持事务, 去查查关于数据库的文献, 有很多,而且都是大部头的。 再去了解一下什么是ACDI,顺便搞清楚数据库和文件系统的区别。
2 MyISAM是支持事务的, 而且支持的就是我说的本地事务。 其实现方式用的是文件锁,(后续的某个版本提供了记录锁的支持), 但是MyISAM的文档上确实说过, 不建议使用事务方式, 同时文档中也说明了理由, 原因是并发太差,而且错误太多。(给你个建议, 我觉得你对MyISAM的了解还是少了些, 最好在去看看文档,补习补习。)
3 你当然可以自己去实现一套api, j2ee 中间有个叫做jca的东西, 如果有兴趣的你可以摆弄一下,搞个不出异常的东西出来,别忘记把 jts 也重新实现一下, 否则还是要抛出异常的! 但是我这里要说的是,我们讨论的jdbc API, 不是你的个人作品。
MyISAM真的支持事务吗?
http://dev.mysql.com/doc/refman/5.4/en/myisam-storage-engine.html
这上面Transactions写的是NO呀。
41 楼
kejianjun01
2009-07-02
如果能讲上述文字写在注释中!我下个效果会更好些!
40 楼
lookdd1
2009-07-02
whaosoft 写道
看来lz要好好学习了哦!~
恭喜您,您获得了javaeye2009年度1-6月份"水王"荣誉称号,您常年活跃于各大板块,坚持灌水为主,讨论为辅,始终保持0分的纯洁和神圣,希望您能将这种精神持之以恒的保持,即使每天一份Javaeye小测验也不动摇自己的信念。
39 楼
lucky16
2009-07-02
呵呵 大闹咯!
38 楼
gembler
2009-07-02
annegu 写道
gembler 写道
annegu 写道
PS:javaeye说严禁讨论与技术无关的问题。
哪里说?
昨天才做了论坛测试题,请看置顶帖子:JavaEye积分规则,博客和论坛使用规则
四、严禁无聊灌水性帖子
JavaEye的论坛不是用来灌水的,是用来交流技术的,即使是海阔天空版面,也不意味着你可以随意灌水,凡是言之无物的无聊灌水贴,将被删除,并扣除发贴者积分30分。纯灌水贴请到“JavaEye水源”圈子。请记住JavaEye的论坛是一个严肃的技术交流场所,在这里制造垃圾信息将受到惩罚。
版主,我不是灌水,是回答问题,并普及论坛规则!
这回robbin高兴死了
37 楼
annegu
2009-07-02
gembler 写道
annegu 写道
PS:javaeye说严禁讨论与技术无关的问题。
哪里说?
昨天才做了论坛测试题,请看置顶帖子:JavaEye积分规则,博客和论坛使用规则
四、严禁无聊灌水性帖子
JavaEye的论坛不是用来灌水的,是用来交流技术的,即使是海阔天空版面,也不意味着你可以随意灌水,凡是言之无物的无聊灌水贴,将被删除,并扣除发贴者积分30分。纯灌水贴请到“JavaEye水源”圈子。请记住JavaEye的论坛是一个严肃的技术交流场所,在这里制造垃圾信息将受到惩罚。
版主,我不是灌水,是回答问题,并普及论坛规则!
相关推荐
mysql jdbc 驱动 适用于5.7及以上版本数据库 mysql jdbc 驱动 适用于5.7及以上版本数据库 mysql jdbc 驱动 适用于5.7及以上版本数据库 mysql jdbc 驱动 适用于5.7及以上版本数据库 mysql jdbc 驱动 适用于5.7及以上...
mysql jdbc 驱动 适用于5.6版本及以下数据库 mysql jdbc 驱动 适用于5.6版本及以下数据库 mysql jdbc 驱动 适用于5.6版本及以下数据库 mysql jdbc 驱动 适用于5.6版本及以下数据库 mysql jdbc 驱动 适用于5.6版本及...
在这个"mysql jdbc 驱动包"中,包含了两个不同版本的驱动:MysqlJDBC驱动包5.1.7和mysql-connector-java-5.1.5。 首先,让我们深入了解一下JDBC。JDBC是Java平台上的一个标准接口,由Sun Microsystems开发,用于...
MySQL JDBC驱动包,全称Java Database Connectivity Driver,是Java编程语言与MySQL数据库之间通信的桥梁。这个驱动包允许Java开发者使用标准的Java SQL API来访问MySQL数据库,进行数据的读写、查询和更新操作。...
MySQL 8.0 JDBC驱动是Java应用程序连接MySQL数据库的关键组件,它允许程序通过Java Database Connectivity (JDBC) API与MySQL数据库进行交互。JDBC是一种Java编程接口,它为开发人员提供了一种标准的方法来访问各种...
MySQL JDBC驱动,全称为MySQL Connector/J,是MySQL官方提供的用于Java应用程序与MySQL数据库进行交互的Java Database Connectivity (JDBC) 驱动程序。这个驱动允许Java开发者通过编写Java代码来执行SQL语句,实现对...
windows平台下的Mysql JDBC驱动程序,经验证可以配合eclipse平台的Sql explore使用。
本文将深入探讨Mysql5.6和Mysql5.7的JDBC驱动,以及如何使用`mysql-connector-java-5.1.34.jar`这个驱动包进行数据库连接。 首先,JDBC驱动是Java程序连接MySQL数据库的关键,它允许Java应用程序通过执行SQL语句来...
MySQL JDBC驱动包8.0.11是MySQL官方发布的用于Java应用程序与MySQL数据库进行通信的驱动程序。JDBC(Java Database Connectivity)是Java平台中的一种标准API,它允许Java程序员使用SQL语句来访问和操作数据库。这个...
本文将详细探讨“mysql5.7.20的jdbc驱动”这一主题,包括其重要性、功能、版本兼容性以及如何在Java项目中使用。 首先,MySQL 5.7.20 JDBC驱动程序是Java应用程序连接到MySQL数据库的关键组件。它提供了Java API,...
Oracle MySQL JDBC驱动是连接Oracle数据库和Java应用程序的重要组件,它允许Java程序通过Java Database Connectivity (JDBC) API与MySQL数据库进行交互。Oracle公司为MySQL提供官方的JDBC驱动,确保了高效、稳定的...
MySQL JDBC驱动,全称为MySQL Connector/J,是MySQL官方提供的用于Java应用程序与MySQL数据库进行交互的Java Database Connectivity (JDBC)驱动。JDBC是Java平台的标准API,它允许Java程序员使用SQL语言来连接、查询...
mysql8的jdbc驱动,jdbc.driver=com.mysql.cj.jdbc.Driver jdbc.url=jdbc:mysql://localhost:3306/test?useSSL=false&useUnicode=true&characterEncoding=UTF8&serverTimezone=GMT jdbc.username=root jdbc.password...
MySQL JDBC驱动,全称为Java Database Connectivity driver for MySQL,是Java应用程序连接MySQL数据库的重要桥梁。它遵循JDBC API规范,使得Java开发者能够在Java平台上方便地进行MySQL数据库的操作。MySQL JDBC...
MySQL JDBC驱动,全称为MySQL Connector/J,是MySQL官方提供的用于Java应用程序与MySQL数据库进行交互的驱动程序。它实现了Java Database Connectivity (JDBC) API,使得Java开发者能够在Java平台上方便地进行MySQL...
MySQL JDBC驱动是Java开发者在与MySQL数据库交互时不可或缺的一个组件。JDBC(Java Database Connectivity)是Java编程语言的标准API,用于连接Java应用程序和各种类型的数据库,包括MySQL。MySQL JDBC驱动,也称为...
MySQL 8.0 JDBC驱动是Java应用程序连接到MySQL数据库的关键组件。JDBC(Java Database Connectivity)是Java平台的标准,允许Java程序与各种数据库系统交互,包括MySQL。在MySQL 8.0版本中,JDBC驱动提供了高效、...
MySQL JDBC驱动包是Java应用程序与MySQL数据库进行交互的重要组件,它实现了Java Database Connectivity (JDBC) API,使得Java开发者能够方便地在Java程序中执行SQL语句,进行数据的存取、更新和删除等操作。...
MySQL JDBC驱动 下载之后解压缩,解压zip文件,随便解压到一个地方就好,当然作为程序员,需要有好习惯的 创建java项目,导入mysql-connector-java-8.0.13.jar包