jdbc获取数据具体过程

下面是个最简单的使用jdbc取得数据的应用。在例子之后我将分成4步,分别是①取得连接,②创建PreparedStatement,③设置参数,④执行查询,来分步分析这个过程。除了设置参数那一步之外,其他的我都画了时序图,如果不想看文字的话,可以对着时序图 。文中的第4步是组装MySQL协议并发送数据包的关键,而且在这部分的(b)环节,我对于PreparedStatement的应用有详细的代码注释分析,建议大家关注一下。


  1. Java代码
  2. public class DBHelper {
  3. public static Connection getConnection() {
  4. Connection conn = null;
  5. try {
  6. Class.forName("com.mysql.jdbc.Driver");
  7. conn = DriverManager.getConnection("jdbc:mysql://localhost/ad?useUnicode=true&characterEncoding=GBK&jdbcCompliantTruncation=false",
  8. "root", "root");
  9. catch (Exception e) {
  10. e.printStackTrace();
  11. }
  12. return conn;
  13. }
  14. }
  15. /*dao中的方法*/
  16. public List getAllAdvs() {
  17. Connection conn = null;
  18. ResultSet rs = null;
  19. PreparedStatement stmt = null;
  20. String sql = "select * from adv where id = ?";
  21. List advs = new ArrayList();
  22. conn = DBHelper.getConnection();
  23. if (conn != null) {
  24. try {
  25. stmt = conn.prepareStatement(sql);
  26. stmt.setInt(1, new Integer(1));
  27. rs = stmt.executeQuery();
  28. if (rs != null) {
  29. while (rs.next()) {
  30. Adv adv = new Adv();
  31. adv.setId(rs.getLong(1));
  32. adv.setName(rs.getString(2));
  33. adv.setDesc(rs.getString(3));
  34. adv.setPicUrl(rs.getString(4));
  35. advs.add(adv);
  36. }
  37. }
  38. catch (SQLException e) {
  39. e.printStackTrace();
  40. finally {
  41. try {
  42. stmt.close();
  43. conn.close();
  44. catch (SQLException e) {
  45. e.printStackTrace();
  46. }
  47. }
  48. }
  49. return advs;
  50. }
  51. public class DBHelper {
  52. public static Connection getConnection() {
  53. Connection conn = null;
  54. try {
  55. Class.forName("com.mysql.jdbc.Driver");
  56. conn = DriverManager.getConnection("jdbc:mysql://localhost/ad?useUnicode=true&characterEncoding=GBK&jdbcCompliantTruncation=false",
  57. "root", "root");
  58. catch (Exception e) {
  59. e.printStackTrace();
  60. }
  61. return conn;
  62. }
  63. }
  64. /*dao中的方法*/
  65. public List getAllAdvs() {
  66. Connection conn = null;
  67. ResultSet rs = null;
  68. PreparedStatement stmt = null;
  69. String sql = "select * from adv where id = ?";
  70. List advs = new ArrayList();
  71. conn = DBHelper.getConnection();
  72. if (conn != null) {
  73. try {
  74. stmt = conn.prepareStatement(sql);
  75. stmt.setInt(1, new Integer(1));
  76. rs = stmt.executeQuery();
  77. if (rs != null) {
  78. while (rs.next()) {
  79. Adv adv = new Adv();
  80. adv.setId(rs.getLong(1));
  81. adv.setName(rs.getString(2));
  82. adv.setDesc(rs.getString(3));
  83. adv.setPicUrl(rs.getString(4));
  84. advs.add(adv);
  85. }
  86. }
  87. catch (SQLException e) {
  88. e.printStackTrace();
  89. finally {
  90. try {
  91. stmt.close();
  92. conn.close();
  93. catch (SQLException e) {
  94. e.printStackTrace();
  95. }
  96. }
  97. }
  98. return advs;
  99. }

1、首先我们看到要的到一个数据库连接,得到数据库连接这部分放在DBHelper类中的getConnection方法中实现。Class.forName("com.mysql.jdbc.Driver");用来加载mysql的jdbc驱动。


  1. Java代码
  2. public class Driver extends NonRegisteringDriver implements java.sql.Driver {
  3. static {
  4. try {
  5. java.sql.DriverManager.registerDriver(new Driver());
  6. catch (SQLException E) {
  7. throw new RuntimeException("Can‘t register driver!");
  8. }
  9. }
  10. public Driver() throws SQLException {
  11. }
  12. }
  13. public class Driver extends NonRegisteringDriver implements java.sql.Driver {
  14. static {
  15. try {
  16. java.sql.DriverManager.registerDriver(new Driver());
  17. catch (SQLException E) {
  18. throw new RuntimeException("Can‘t register driver!");
  19. }
  20. }
  21. public Driver() throws SQLException {
  22. }
  23. }

Mysql的Driver类实现了java.sql.Driver接口,任何数据库提供商的驱动类都必须实现这个接口。在DriverManager类中使用的都是接口Driver类型的驱动,也就是说驱动的使用不依赖于具体的实现,这无疑给我们的使用带来很大的方便。如果需要换用其他的数据库的话,只需要把Class.forName()中的参数换掉就可以了,可以说是非常方便的。

在com.mysql.jdbc.Driver类中,除了构造方法,就是一个static的方法体,它调用了DriverManager的registerDriver()方法,这个方法会加载所有系统提供的驱动,并把它们都假如到具体的驱动类中,当然现在就是mysql的Driver。在这里我们第一次看到了DriverManager类,这个类中提供了jdbc连接的主要操作,创建连接就是在这里完成的,可以说这是一个管理驱动的工具类。


  1. Java代码
  2. public static synchronized void registerDriver(java.sql.Driver driver)
  3. throws SQLException {
  4. if (!initialized) {
  5. initialize();
  6. }
  7. DriverInfo di = new DriverInfo();
  8. /*把driver的信息封装一下,组成一个DriverInfo对象*/
  9. di.driver = driver;
  10. di.driverClass = driver.getClass();
  11. di.driverClassName = di.driverClass.getName();
  12. writeDrivers.addElement(di);
  13. println("registerDriver: " + di);
  14. readDrivers = (java.util.Vector) writeDrivers.clone();
  15. }
  16. public static synchronized void registerDriver(java.sql.Driver driver)
  17. throws SQLException {
  18. if (!initialized) {
  19. initialize();
  20. }
  21. DriverInfo di = new DriverInfo();
  22. /*把driver的信息封装一下,组成一个DriverInfo对象*/
  23. di.driver = driver;
  24. di.driverClass = driver.getClass();
  25. di.driverClassName = di.driverClass.getName();
  26. writeDrivers.addElement(di);
  27. println("registerDriver: " + di);
  28. readDrivers = (java.util.Vector) writeDrivers.clone();
  29. }

注册驱动首先就是初始化,然后把驱动的信息封装一下放进一个叫做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(),如下:


  1. Java代码
  2. private static Connection getConnection(
  3. String url, java.util.Properties info, ClassLoader callerCL) throws SQLException {
  4. java.util.Vector drivers = null;
  5. ...
  6. if (!initialized) {
  7. initialize();
  8. }
  9. /*取得连接使用的driver从readDrivers中取*/
  10. synchronized (DriverManager.class){
  11. drivers = readDrivers;
  12. }
  13. SQLException reason = null;
  14. for (int i = 0; i < drivers.size(); i++) {
  15. DriverInfo di = (DriverInfo)drivers.elementAt(i);
  16. if ( getCallerClass(callerCL, di.driverClassName ) != di.driverClass ) {
  17. continue;
  18. }
  19. try {
  20. /*找到可供使用的驱动,连接数据库server*/
  21. Connection result = di.driver.connect(url, info);
  22. if (result != null) {
  23. return (result);
  24. }
  25. catch (SQLException ex) {
  26. if (reason == null) {
  27. reason = ex;
  28. }
  29. }
  30. }
  31. if (reason != null)    {
  32. println("getConnection failed: " + reason);
  33. throw reason;
  34. }
  35. throw new SQLException("No suitable driver found for "+ url, "08001");
  36. }
  37. private static Connection getConnection(
  38. String url, java.util.Properties info, ClassLoader callerCL) throws SQLException {
  39. java.util.Vector drivers = null;
  40. ...
  41. if (!initialized) {
  42. initialize();
  43. }
  44. /*取得连接使用的driver从readDrivers中取*/
  45. synchronized (DriverManager.class){
  46. drivers = readDrivers;
  47. }
  48. SQLException reason = null;
  49. for (int i = 0; i < drivers.size(); i++) {
  50. DriverInfo di = (DriverInfo)drivers.elementAt(i);
  51. if ( getCallerClass(callerCL, di.driverClassName ) != di.driverClass ) {
  52. continue;
  53. }
  54. try {
  55. /*找到可供使用的驱动,连接数据库server*/
  56. Connection result = di.driver.connect(url, info);
  57. if (result != null) {
  58. return (result);
  59. }
  60. catch (SQLException ex) {
  61. if (reason == null) {
  62. reason = ex;
  63. }
  64. }
  65. }
  66. if (reason != null)    {
  67. println("getConnection failed: " + reason);
  68. throw reason;
  69. }
  70. throw new SQLException("No suitable driver found for "+ url, "08001");
  71. }

Initialize()简直无所不在,DriverManager中只要使用driver之前,就要检查一下有没有初始化,非常小心。然后开始遍历所有驱动,直到找到一个可用的驱动,用这个驱动来取得一个数据库连接,最后返回这个连接。当然,这是正常的情况,从上面我们可以看到,程序中对异常的处理很仔细。如果连接失败,会记录抛出的第一个异常信息,如果没有找到合适的驱动,就抛出一个08001的错误。

现在重点就是假如一切正常,就应该从driver.connect()返回一个数据库连接,所以我们来看看如何通过url提供的数据库。


  1. Java代码
  2. public java.sql.Connection connect(String url, Properties info)
  3. throws SQLException {
  4. Properties props = null;
  5. if ((props = parseURL(url, info)) == null) {
  6. return null;
  7. }
  8. try {
  9. Connection newConn = new com.mysql.jdbc.Connection(host(props),
  10. port(props), props, database(props), url);
  11. return newConn;
  12. catch (SQLException sqlEx) {
  13. throw sqlEx;
  14. catch (Exception ex) {
  15. throw SQLError.createSQLException(...);
  16. }
  17. }
  18. public java.sql.Connection connect(String url, Properties info)
  19. throws SQLException {
  20. Properties props = null;
  21. if ((props = parseURL(url, info)) == null) {
  22. return null;
  23. }
  24. try {
  25. Connection newConn = new com.mysql.jdbc.Connection(host(props),
  26. port(props), props, database(props), url);
  27. return newConn;
  28. catch (SQLException sqlEx) {
  29. throw sqlEx;
  30. catch (Exception ex) {
  31. throw SQLError.createSQLException(...);
  32. }
  33. }

很简洁的写法,就是新建了一个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:

Java代码  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;   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中的参数常量主要有以下几种:


  1. TYPE_FORWARD_ONLY: ResultSet的游标只能向前移动。
  2. TYPE_SCROLL_INSENSITIVE:ResultSet的游标可以滚动,但对于resultSet下的数据改变不敏感。
  3. TYPE_SCROLL_SENSITIVE:ResultSet的游标可以滚动,但对于resultSet下的数据改变是敏感的。
  4. CONCUR_READ_ONLY:不可以更新的ResultSet的并发模式。
  5. CONCUR_UPDATABLE:可以更新的ResultSet的并发模式。
  6. FETCH_FORWARD:按正向(即从第一个到最后一个)处理结果集中的行。
  7. FETCH_REVERSE:按反向(即从最后一个到第一个)处理结果集中的行处理。
  8. FETCH_UNKNOWN:结果集中的行的处理顺序未知。
  9. CLOSE_CURSORS_AT_COMMIT:调用Connection.commit方法时应该关闭 ResultSet 对
  10. 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语句的二进制流,请看下面的代码分析。


  1. Java代码
  2. protected Buffer fillSendPacket(byte[][] batchedParameterStrings,
  3. InputStream[] batchedParameterStreams, boolean[] batchedIsStream,
  4. int[] batchedStreamLengths) throws SQLException {
  5. // 从connection的IO中得到发送数据包,首先清空其中的数据
  6. Buffer sendPacket = this.connection.getIO().getSharedSendPacket();
  7. sendPacket.clear();
  8. /* 数据包的第一位为一个操作标识符(MysqlDefs.QUERY),表示驱动向服务器发送的连接的操作信号,包括有QUERY, PING, RELOAD, SHUTDOWN, PROCESS_INFO, QUIT, SLEEP等等,这个操作信号并不是针对sql语句操作而言的CRUD操作,从提供的几种参数来看,这个操作是针对服务器的一个操作。一般而言,使用到的都是MysqlDefs.QUERY,表示发送的是要执行sql语句的操作。
  9. */
  10. sendPacket.writeByte((byte) MysqlDefs.QUERY);
  11. boolean useStreamLengths = this.connection
  12. .getUseStreamLengthsInPrepStmts();
  13. int ensurePacketSize = 0;
  14. for (int i = 0; i < batchedParameterStrings.length; i++) {
  15. if (batchedIsStream[i] && useStreamLengths) {
  16. ensurePacketSize += batchedStreamLengths[i];
  17. }
  18. }
  19. /* 判断这个sendPacket的byte buffer够不够大,不够大的话,按照1.25倍来扩充buffer
  20. */
  21. if (ensurePacketSize != 0) {
  22. sendPacket.ensureCapacity(ensurePacketSize);
  23. }
  24. /* 遍历所有的参数。在prepareStatement阶段的new ParseInfo()中,驱动曾经对sql语句进行过分割,如果含有以问号标识的参数占位符的话,就记录下这个占位符的位置,依据这个位置把sql分割成多段,放在了一个名为staticSql的字符串中。这里就开始把sql语句进行拼装,把staticSql和parameterValues进行组合,放在操作符的后面。
  25. */
  26. for (int i = 0; i < batchedParameterStrings.length; i++) {
  27. /* batchedParameterStrings就是parameterValues,batchedParameterStreams就是parameterStreams,如果两者都为null,说明在参数的设置过程中出了错,立即抛出错误。
  28. */
  29. if ((batchedParameterStrings[i] == null)
  30. && (batchedParameterStreams[i] == null)) {
  31. throw SQLError.createSQLException(Messages
  32. .getString("PreparedStatement.40") //$NON-NLS-1$
  33. + (i + 1), SQLError.SQL_STATE_WRONG_NO_OF_PARAMETERS);
  34. }
  35. /*在sendPacket中加入staticSql数组中的元素,就是分割出来的没有”?”的sql语句,并把字符串转换成byte。
  36. */
  37. sendPacket.writeBytesNoNull(this.staticSqlStrings[i]);
  38. /* batchedIsStream就是isStream,如果参数是通过CallableStatement传递进来的话,batchedIsStream[i]==true,就用batchedParameterStreams中的值填充到问号占的参数位置中去。
  39. */
  40. if (batchedIsStream[i]) {
  41. streamToBytes(sendPacket, batchedParameterStreams[i], true,
  42. batchedStreamLengths[i], useStreamLengths);
  43. else {
  44. /*否则的话,就用batchedParameterStrings,也就是parameterValues来填充参数位置。在循环中,这个操作是跟在staticSql后面的,因此就把第i个参数加到了第i个staticSql段中。参考前面的staticSql的例子,发现当循环结束的时候,原始sql语句最后一个”?”之前的sql语句就拼成了正确的语句了。
  45. */
  46. sendPacket.writeBytesNoNull(batchedParameterStrings[i]);
  47. }
  48. }
  49. /*由于在原始的包含问号的sql语句中,在最后一个”?”后面可能还有order by等语句,因此staticSql数组中的元素个数一定比参数的个数多1,所以这里把staticSqlString中的最后一段sql语句放入sendPacket中。
  50. */
  51. sendPacket
  52. .writeBytesNoNull(this.staticSqlStrings[batchedParameterStrings.length]);
  53. return sendPacket;
  54. }
  55. protected Buffer fillSendPacket(byte[][] batchedParameterStrings,
  56. InputStream[] batchedParameterStreams, boolean[] batchedIsStream,
  57. int[] batchedStreamLengths) throws SQLException {
  58. // 从connection的IO中得到发送数据包,首先清空其中的数据
  59. Buffer sendPacket = this.connection.getIO().getSharedSendPacket();
  60. sendPacket.clear();
  61. /* 数据包的第一位为一个操作标识符(MysqlDefs.QUERY),表示驱动向服务器发送的连接的操作信号,包括有QUERY, PING, RELOAD, SHUTDOWN, PROCESS_INFO, QUIT, SLEEP等等,这个操作信号并不是针对sql语句操作而言的CRUD操作,从提供的几种参数来看,这个操作是针对服务器的一个操作。一般而言,使用到的都是MysqlDefs.QUERY,表示发送的是要执行sql语句的操作。
  62. */
  63. sendPacket.writeByte((byte) MysqlDefs.QUERY);
  64. boolean useStreamLengths = this.connection
  65. .getUseStreamLengthsInPrepStmts();
  66. int ensurePacketSize = 0;
  67. for (int i = 0; i < batchedParameterStrings.length; i++) {
  68. if (batchedIsStream[i] && useStreamLengths) {
  69. ensurePacketSize += batchedStreamLengths[i];
  70. }
  71. }
  72. /* 判断这个sendPacket的byte buffer够不够大,不够大的话,按照1.25倍来扩充buffer
  73. */
  74. if (ensurePacketSize != 0) {
  75. sendPacket.ensureCapacity(ensurePacketSize);
  76. }
  77. /* 遍历所有的参数。在prepareStatement阶段的new ParseInfo()中,驱动曾经对sql语句进行过分割,如果含有以问号标识的参数占位符的话,就记录下这个占位符的位置,依据这个位置把sql分割成多段,放在了一个名为staticSql的字符串中。这里就开始把sql语句进行拼装,把staticSql和parameterValues进行组合,放在操作符的后面。
  78. */
  79. for (int i = 0; i < batchedParameterStrings.length; i++) {
  80. /* batchedParameterStrings就是parameterValues,batchedParameterStreams就是parameterStreams,如果两者都为null,说明在参数的设置过程中出了错,立即抛出错误。
  81. */
  82. if ((batchedParameterStrings[i] == null)
  83. && (batchedParameterStreams[i] == null)) {
  84. throw SQLError.createSQLException(Messages
  85. .getString("PreparedStatement.40") //$NON-NLS-1$
  86. + (i + 1), SQLError.SQL_STATE_WRONG_NO_OF_PARAMETERS);
  87. }
  88. /*在sendPacket中加入staticSql数组中的元素,就是分割出来的没有”?”的sql语句,并把字符串转换成byte。
  89. */
  90. sendPacket.writeBytesNoNull(this.staticSqlStrings[i]);
  91. /* batchedIsStream就是isStream,如果参数是通过CallableStatement传递进来的话,batchedIsStream[i]==true,就用batchedParameterStreams中的值填充到问号占的参数位置中去。
  92. */
  93. if (batchedIsStream[i]) {
  94. streamToBytes(sendPacket, batchedParameterStreams[i], true,
  95. batchedStreamLengths[i], useStreamLengths);
  96. else {
  97. /*否则的话,就用batchedParameterStrings,也就是parameterValues来填充参数位置。在循环中,这个操作是跟在staticSql后面的,因此就把第i个参数加到了第i个staticSql段中。参考前面的staticSql的例子,发现当循环结束的时候,原始sql语句最后一个”?”之前的sql语句就拼成了正确的语句了。
  98. */
  99. sendPacket.writeBytesNoNull(batchedParameterStrings[i]);
  100. }
  101. }
  102. /*由于在原始的包含问号的sql语句中,在最后一个”?”后面可能还有order by等语句,因此staticSql数组中的元素个数一定比参数的个数多1,所以这里把staticSqlString中的最后一段sql语句放入sendPacket中。
  103. */
  104. sendPacket
  105. .writeBytesNoNull(this.staticSqlStrings[batchedParameterStrings.length]);
  106. return sendPacket;
  107. }

假如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服务器接收数据并解析,执行查询了。


  1. Java代码
  2. while (len >= this.maxThreeBytes) {
  3. this.packetSequence++;
  4. /*设置包的开始位置*/
  5. headerPacket.setPosition(0);
  6. /*设置这个数据包的大小,splitSize=255 * 255 * 255*/
  7. headerPacket.writeLongInt(splitSize);
  8. /*设置数据包的序号*/
  9. headerPacket.writeByte(this.packetSequence);
  10. /*origPacketBytes就是sendPacket,所以这里就是把sendPacket中大小为255 * 255 * 255的数据放入headPacket中,headerPacketBytes是headPacket的byte buffer*/
  11. System.arraycopy(origPacketBytes, originalPacketPos,
  12. headerPacketBytes, 4, splitSize);
  13. int packetLen = splitSize + HEADER_LENGTH;
  14. if (!this.useCompression) {
  15. this.mysqlOutput.write(headerPacketBytes, 0,
  16. splitSize + HEADER_LENGTH);
  17. this.mysqlOutput.flush();
  18. else {
  19. Buffer packetToSend;
  20. headerPacket.setPosition(0);
  21. packetToSend = compressPacket(headerPacket, HEADER_LENGTH,
  22. splitSize, HEADER_LENGTH);
  23. packetLen = packetToSend.getPosition();
  24. /*往IO的output stream中写数据*/
  25. this.mysqlOutput.write(packetToSend.getByteBuffer(), 0,
  26. packetLen);
  27. this.mysqlOutput.flush();
  28. }
  29. originalPacketPos += splitSize;
  30. len -= splitSize;
  31. }
  32. while (len >= this.maxThreeBytes) {
  33. this.packetSequence++;
  34. /*设置包的开始位置*/
  35. ①          headerPacket.setPosition(0);
  36. /*设置这个数据包的大小,splitSize=255 * 255 * 255*/
  37. headerPacket.writeLongInt(splitSize);
  38. /*设置数据包的序号*/
  39. headerPacket.writeByte(this.packetSequence);
  40. /*origPacketBytes就是sendPacket,所以这里就是把sendPacket中大小为255 * 255 * 255的数据放入headPacket中,headerPacketBytes是headPacket的byte buffer*/
  41. System.arraycopy(origPacketBytes, originalPacketPos,
  42. headerPacketBytes, 4, splitSize);
  43. int packetLen = splitSize + HEADER_LENGTH;
  44. if (!this.useCompression) {
  45. this.mysqlOutput.write(headerPacketBytes, 0,
  46. splitSize + HEADER_LENGTH);
  47. this.mysqlOutput.flush();
  48. else {
  49. Buffer packetToSend;
  50. headerPacket.setPosition(0);
  51. packetToSend = compressPacket(headerPacket, HEADER_LENGTH,
  52. splitSize, HEADER_LENGTH);
  53. packetLen = packetToSend.getPosition();
  54. /*往IO的output stream中写数据*/
  55. this.mysqlOutput.write(packetToSend.getByteBuffer(), 0,
  56. packetLen);
  57. this.mysqlOutput.flush();
  58. }
  59. originalPacketPos += splitSize;
  60. len -= splitSize;
  61. }

iii. 通过readAllResults方法读取查询结果。这个读取的过程与发送过程相反,如果接收到的数据包有多个的话,通过IO不断读取,并根据第packet第4个位置上的序号来组装这些packet。然后把读到的数据组装成resultSet中的rowData,这个结果就是我们要的查询结果了。

结合下面的executeQuery的时序图再理一下思路就更清楚了。

至此,把resultSet一步步的返回给dao,接下来的过程,就是从resultSet中取出rowData,组合成我们自己需要的对象数据了。

总结一下,经过这次对mysql驱动的探索,我发现了更多关于mysql的底层细节,对于以后分析问题解决问题有很大帮助,当然,这里面还有很多细节文中没有写。另外一个就是对于PreparedStatement有了重新的认识,有些东西往往都是想当然得出来的结论,真相还是要靠实践来发现。

时间: 2024-10-12 13:02:22

jdbc获取数据具体过程的相关文章

volley源码解析(四)--CacheDispatcher从缓存中获取数据

从上一篇文章我们已经知道,现在要处理的问题就是CacheDispatcher和NetworkDispatcher怎么分别去缓存和网络获取数据的问题,这两个问题我分开来讲. 但是首先说明的是,这两个问题其实是有联系的,当CacheDispatcher获取不到缓存的时候,会将request放入网络请求队列,从而让NetworkDispatcher去处理它: 而当NetworkDispatcher获得数据以后,又会将数据缓存,下次CacheDispatcher就可以从缓存中获得数据了. 这篇文章,就让

第90讲,Spark streaming基于kafka 以Receiver方式获取数据 原理和案例实战

1:SparkSteaming基于kafka获取数据的方式,主要有俩种,即Receiver和Derict,基于Receiver的方式,是sparkStreaming给我们提供了kafka访问的高层api的封装,而基于Direct的方式,就是直接访问,在sparkSteaming中直接去操作kafka中的数据,不需要前面的高层api的封装.而Direct的方式,可以对kafka进行更好的控制!同时性能也更好. 2:实际上做kafka receiver的时候,通过receiver来获取数据,这个时候

是用JDBC从数据库中获取数据并以java对象返回

/** * * @param c * for example Person.class * @param primaryKeys * primaryKeys为主键,参数顺序和表中保持一致 如果id, name 为主键 类名为Person 则 getEntity(Person.class,1,"name") * @return */ public static Object getEntity(Class c, Object... primaryKeys) { PreparedState

HBase 高性能获取数据 - 多线程批量式解决办法

在前篇博客里已经讲述了通过一个自定义 HBase Filter来获取数据的办法,在末尾指出此办法的性能是不能满足应用要求的,很显然对于如此成熟的HBase来说,高性能获取数据应该不是问题.下面首先简单介绍了搜索引擎的性能,然后详细说明了HBase与MySQL的性能对比,这里的数据都是经过实际的测试获得的.最后,给出了采用多线程批量从HBase中取数据的方案,此方案经过测试要比通过自定义Filter的方式性能高出很多. Solr和HBase专辑 1.“关于Solr的使用总结的心得体会”(http:

DHCP获取IP地址过程中捕获的报文—三级网络总结(二)

上一篇文章主要说了一下知识点中的IP地址的考点,这一篇我打算说说DHCP获取IP地址过程中捕获的报文的这个考点,都是自己的理解,有错误欢迎指正. DHCP是应用层协议,UDP是传输层协议,IP是网络层协议,以太网是链路层协议.数据在网络上传输的时候要自顶向下逐层封装的,典型的DHCP过程是这样的: 1:客户机向服务器发送DHCP_DISCOVER报文,申请IP. 2:服务器向客户机返会DHCP_OFFER报文,指定一个将要分配的IP. 3:客户机向服务器发送DHCP_REQUEST报文,请求这个

黑客获取数据信息的目的和进攻手段

进入微软.亚马逊,谷歌等美国IT企业工作人才项目,起薪40万,百度搜索(MUMCS) 黑客使用进攻取证获取凭证,如用户名和密码.这些都允许他们访问敏感数据同时能够隐瞒自己的身份,以拖延攻击时被发现的时间并避免暴露自己的行踪.黑客寻找这种以半永久记忆的形式获取存在如 RAM 内存或交换文件中的动态/非静态数据.一旦黑客获得暂时存储在明文中的用户 ID 和密码,他们就可以进入下一个等级的访问,进一步获取资源,如内部网站.文档管理系统和 SharePoint 站点,本文来自网届网. 以下为原文: "一

通过jdbc获取数据库中的表结构

通过jdbc获取数据库中的表结构 主键 各个表字段类型及应用生成实体类 1.JDBC中通过MetaData来获取具体的表的相关信息.可以查询数据库中的有哪些表,表有哪些字段,字段的属性等等.MetaData中通过一系列getXXX函数,将这些信息存放到ResultSet里面,然后返回给用户.关于MetaData的说明网上也有不少,这里我只是从我自身学习的角度来记录一下简单使用JDBC以及获取数据表相关信息的方法. DatabaseMetaData dbmd = con.getMetaData()

网络爬虫模拟登陆获取数据并解析实战(二)

目录 分析要获取的数据 程序的结构 构建封装数据的model 模拟登陆程序并解析数据 结果展示 分析要获取的数据 下面继续实战,写一个模拟登陆获取汽车之家,用户信息的程序.如果大家对模拟登陆获取数据不太了解,建议看完http://blog.csdn.net/qy20115549/article/details/52249232,我写的这篇含有抓包获取人人网数据的案例程序,研究透之后,再来看这个要轻松很多. 首先,大家打开汽车之家这个网站(http://i.autohome.com.cn/7741

从SQLite获取数据完成一个产品信息展示

在ios实际开发当中,我们常常用到Core Data做为数据储存首选.但在处理一些大量复杂的数据值且数据之间相互关联的时候,这就不得不使用关系型数据库来实现.例如一个导航程序,自身应该包含大量的地图自身数据并且数据需要在app启动的时候就开始读取加载.而且数据本身变动不是特别频繁.重复向服务器发送请求获取信息是一件十分浪费的事情.因此我们可以用一个本地数据文件来直接配置.做为轻量级关系型数据库的sqlite是ios开发首选.而xcode本身包含了sqlite库,因此在ios使用的时候不需要额外配