疯狂Java讲义(十三)----第四部分

1.执行SQL语句的方式

        前面介绍了JDBC执行查询等示例程序,实际上,JDBC不仅可以执行查询,也可以执行 DDL、DML等SQL语句,从而允许通过JDBC最大限度地控制数据库。

  (1)使用Java 8新增的executeLargeUpdate方法执行DDL和DML语句

        Statement提供了三个方法来执行SQL语句,前面已经介绍了使用executeQuery()来执行查询语句,下面将介绍使用executeLargeUpdate()(或executeUpdate())来执行DDL和 DML语句。使用Statement执行DDL和 DML语句的步骤与执行普通查询语句的步骤基本相似,区别在于执行了DDL语句后返回值为0,执行了DML语句后返回值为受影响的记录条数。
        下面程序示范了使用executeUpdate()方法(此处暂未使用executeLargeUpdate()方法是因为MySQL驱动暂不支持)创建数据表。该示例并没有直接把数据库连接信息写在程序里,而是使用一个mysql.ini文件(就是一个properties 文件)来保存数据库连接信息,这是比较成熟的做法——当需要把应用程序从开发环境移植到生产环境时,无须修改源代码,只需要修改mysql.ini配置文件即可。

        运行上面程序,执行成功后会看到select_test 数据库中添加了一个jdbc_test数据表,这表明JDBC执行DDL语句成功。
        使用executeUpdate()执行DML语句与执行DDL语句基本相似,区别是 executeUpdate()执行DDL语句后返回0,而执行DML语句后返回受影响的记录条数。下面程序将会执行一条insert 语句,这条insert 语句会向刚刚建立的jdbc_test 数据表中插入几条记录。因为使用了带子查询的 insert语句,所以可以一次插入多条语句。

        运行上面程序,执行成功将会看到jdbc_test 数据表中多了几条记录,而且在程序控制台会看到输出有几条记录受影响的信息。

  (2) 使用execute方法执行SQL语句

        Statement的 execute()方法几乎可以执行任何SQL 语句,但它执行SQL语句时比较麻烦,通常没有必要使用execute()方法来执行SQL语句,使用executeQuery()或executeUpdate()方法更简单。但如果不清楚SQL语句的类型,则只能使用execute()方法来执行该SQL语句了。
        使用execute()方法执行SQL语句的返回值只是 boolean值,它表明执行该SQL 语句是否返回了ResultSet对象。那么如何来获取执行SQL语句后得到的ResultSet对象呢?Statement 提供了如下两个方法来获取执行结果。

  • getResultSet():获取该Statement 执行查询语句所返回的ResultSet对象。
  • getUpdateCount():获取该Statement()执行 DML语句所影响的记录行数。

        下面程序示范了使用Statement 的execute()方法来执行任意的SQL语句,执行不同的SQL语句时产生不同的输出。

        运行上面程序,会看到使用Statement 的不同方法执行不同SQL语句的效果。执行DDL语句显示受影响的记录条数为0;执行DML语句显示插入、修改或删除的记录条数;执行查询语句则可以输出查询结果。

  (3) 使用PreparedStatement 执行SQL语句

        如果经常需要反复执行一条结构相似的SQL语句,例如如下两条SQL语句:

        对于这两条SQL语句而言,它们的结构基本相似,只是执行插入时插入的值不同而已。对于这种情况,可以使用带占位符(?)参数的SQL语句来代替它:

        但Statement 执行SQL语句时不允许使用问号占位符参数,而且这个问号占位符参数必须获得值后才可以执行。为了满足这种功能,JDBC提供了PreparedStatement 接口,它是Statement 接口的子接口,它可以预编译SQL语句,预编译后的SQL语句被存储在PreparedStatement对象中,然后可以使用该对象多次高效地执行该语句。简而言之,使用 PreparedStatement 比使用Statement 的效率要高。
        创建PreparedStatement对象使用Connection 的 prepareStatement()方法,该方法需要传入一个SQL字符串,该SQL字符串可以包含占位符参数。如下代码所示:

        PreparedStatement也提供了execute()、executeUpdate()、executeQuery()三个方法来执行SQL语句,不过这三个方法无须参数,因为PreparedStatement已存储了预编译的SQL语句。
        使用PreparedStatement预编译SQL语句时,该SQL语句可以带占位符参数,因此在执行SQL语句之前必须为这些参数传入参数值, PreparedStatement 提供了一系列的setXxx(int index , Xxx value)方法来传入参数值。

        下面程序示范了使用Statement 和 PreparedStatement分别插入100条记录的对比。使用Statement需要传入100条SQL语句,但使用PreparedStatement则只需要传入1条预编译的SQL语句,然后100次为该PreparedStatement的参数设值即可。

        多次运行上面程序,可以发现使用PreparedStatement插入100条记录所用的时间比使用Statement插入100条记录所用的时间少,这表明PreparedStatement 的执行效率比Statement的执行效率高。
        除此之外,使用PreparedStatement还有一个优势——当SQL语句中要使用参数时,无须“拼接”SQL字符串。而使用Statement则要“拼接”SQL字符串,如上程序中粗体字代码所示,这是相当容易出现错误的——注意粗体字代码中的单引号,这是因为SQL 语句中的字符串必须用单引号引起来。尤其是当SQL语句中有多个字符串参数时,“拼接”这条SQL 语句时就更容易出错了。使用PreparedStatement则只需要使用问号占位符来代替这些参数即可,降低了编程复杂度。
        使用PreparedStatement还有一个很好的作用——用于防止SQL注入。

        下面以一个简单的登录窗口为例来介绍这种SQL注入的结果。下面登录窗口包含两个文本框,一个用于输入用户名,一个用于输入密码,系统根据用户输入与 jdbc_test表里的记录进行匹配,如果找到相应记录则提示登录成功。

 

        运行上面程序,如果用户正常输入其用户名、密码当然没有问题,输入正确可以正常登录,输入错误将提示输入失败。但如果这个用户是一个Cracker,他可以按图13.19所示来输入。

        看到这条SQL语句,读者应该不难明白为什么这样输入也可以显示“正常登录”对话框了,因为Cracker直接输入了true,而SQL把这个true当成了直接量。

        如果把上面的 validate()方法换成使用PreparedStatement 来执行验证,而不是直接使用Statement.程序如下:

        将上面的validate()方法改为使用PrepareStatement 来执行SQL语句之后,即使用户按图13.19所示输入,系统一样会显示“登录失败”对话框。
        总体来看,使用PreparedStatement 比使用Statement多了如下三个好处。

  • PreparedStatement预编译SQL语句,性能更好。
  • PreparedStatement无须“拼接”SQL语句,编程更简单。
  • PreparedStatement可以防止SQL注入,安全性更好。

        基于以上三点,通常推荐避免使用Statement 来执行SQL 语句,改为使用PreparedStatement 执行sQL语句。

  (4) 使用CallableStatement调用存储过程

下面的SQL语句可以在 MySQL数据库中创建一个简单的存储过程。

        上面的SQL语句将MySQL的语句结束符改为双斜线(//),这样就可以在创建存储过程中使用分号作为分隔符(MySQL默认使用分号作为语句结束符)。上面程序创建了名为add_pro 的存储过程,该存储过程包含三个参数: a、b是传入参数,而sum使用out修饰,是传出参数。

        调用存储过程使用 CallableStatement,可以通过 Connection的 prepareCall()方法来创建CallableStatement对象,创建该对象时需要传入调用存储过程的SQL语句。调用存储过程的SQL语句总是这种格式: {call过程名(?,?,?.….)},其中的问号作为存储过程参数的占位符。例如,如下代码就创建了调用上面存储过程的CallableStatement对象。

        存储过程的参数既有传入参数,也有传出参数。所谓传入参数就是Java程序必须为这些参数传入值,可以通过CallableStatement的setXxx()方法为传入参数设置值;所谓传出参数就是Java程序可以通过该参数获取存储过程里的值,CallableStatement需要调用registerOutParameter()方法来注册该参数。如下代码所示:

        经过上面步骤之后,就可以调用CallableStatement的 execute()方法来执行存储过程了,执行结束后通过CallableStatement对象的getXxx(int index)方法来获取指定传出参数的值。下面程序示范了如何来调用该存储过程。

        上面程序中的粗体字代码就是执行存储过程的关键代码,运行上面程序将会看到这个简单存储过程的执行结果,传入参数分别是4、5,执行加法后传出总和9。

5. 管理结果集

        JDBC使用ResultSet来封装执行查询得到的查询结果,然后通过移动ResultSet 的记录指针来取出结果集的内容。除此之外,JDBC还允许通过ResultSet 来更新记录,并提供了ResultSetMetaData来获得ResultSet 对象的相关信息。

  (1) 可滚动、可更新的结果集

        前面提到,ResultSet 定位记录指针的方法有absolute()、previous()等方法,但前面程序自始至终都只用了next()方法来移动记录指针,实际上也可以使用absolute()、previous()、last()等方法来移动记录指针。可以使用absolute()、previous()、afterLast()等方法自由移动记录指针的ResultSet被称为可滚动的结果集。

        以默认方式打开的 ResultSet是不可更新的,如果希望创建可更新的ResultSet,则必须在创建Statement或 PreparedStatement时传入额外的参数。Connection在创建Statement或PreparedStatement时还可额外传入如下两个参数。

  1. resultSetType:控制 ResultSet的类型,该参数可以取如下三个值。
  • ResultSet.TYPE_FORWARD_ONLY:该常量控制记录指针只能向前移动。这是 JDK 1.4以前的默认值。
  • ResultSet.TYPE_SCROLL_INSENSITIVE:该常量控制记录指针可以自由移动(可滚动结果集),但底层数据的改变不会影响ResultSet的内容。
  • ResultSet.TYPE_SCROLL_SENSITIVE:该常量控制记录指针可以自由移动(可滚动结果集),而且底层数据的改变会影响ResultSet的内容。

  1.  resultSetConcurrency:控制ResultSet的并发类型,该参数可以接收如下两个值。
  • ResultSet.CONCUR_READ_ONLY:该常量指示ResultSet 是只读的并发模式(默认)。
  • ResultSet.CONCUR_UPDATABLE:该常量指示ResultSet是可更新的并发模式。

        下面代码通过这两个参数创建了一个PreparedStatement对象,由该对象生成的ResultSet对象将是可滚动、可更新的结果集。

        需要指出的是,可更新的结果集还需要满足如下两个条件。

  • 所有数据都应该来自一个表。
  • 选出的数据集必须包含主键列。

        通过该PreparedStatement 创建的 ResultSet 就是可滚动、可更新的,程序可调用ResultSet 的updateXxx(int columnIndex , Xxx value)方法来修改记录指针所指记录、特定列的值,最后调用ResultSet的updateRow()方法来提交修改。
        Java 8为 ResultSet添加了updateObject(String columnLabel,Object x,SQLType targetSqIType)和updateObject(int columnIndex,Object x, SQLType targetSqlType)两个默认方法,这两个方法可以直接用Object 来修改记录指针所指记录、特定列的值,其中 SQLType用于指定该数据列的类型。但目前最新的MySQL驱动暂不支持该方法。
        下面程序示范了这种创建可滚动、可更新的结果集的方法。

        上面程序中的粗体字代码示范了如何自由移动记录指针并更新记录指针所指的记录。运行上面程序,将会看到student_table表中的记录被倒过来输出了,因为是从最大记录行开始输出的。而且当程序运行结束后,student_table表中所有记录的student_name列的值都被修改了。

  (2)  处理Blob类型数据

        Blob (Binary Long Object〉是二进制长对象的意思,Blob列通常用于存储大文件,典型的Blob 内容是一张图片或一个声音文件,由于它们的特殊性,必须使用特殊的方式来存储。使用Blob列可以把图片、声音等文件的二进制数据保存在数据库里,并可以从数据库里恢复指定文件。
        如果需要将图片插入数据库,显然不能直接通过普通的SQL 语句来完成,因为有一个关键的问题——Blob 常量无法表示。所以将Blob数据插入数据库需要使用PreparedStatement,该对象有一个方法:setBinaryStream(int parameterIndex,InputStream x),该方法可以为指定参数传入二进制输入流,从而可以实现将Blob 数据保存到数据库的功能。
        当需要从ResultSet里取出 Blob数据时,可以调用ResultSet 的getBlob(int columnIndex)方法,该方法将返回一个Blob对象,Blob对象提供了getBinaryStream()方法来获取该Blob数据的输入流,也可以使用Blob对象提供的getBytes()方法直接取出该Blob对象封装的二进制数据。
        为了把图片放入数据库,本程序先使用如下SQL语句来建立一个数据表。

        下面程序可以实现图片“上传”——实际上就是将图片保存到数据库,并在右边的列表框中显示图片的名字,当用户双击列表框中的图片名时,左边窗口将显示该图片—一实质就是根据选中的ID从数据库里查找图片,并将其显示出来。

 

 

 

 

 

        上面程序中的第一段粗体字代码用于控制将一个图片文件保存到数据库,第二段粗体字代码用于控制将数据库里的图片数据显示出来。运行上面程序,并上传一些图片,会看到如图13.20所示的界面。

  (3) 使用ResultSetMetaData分析结果集

        当执行SQL查询后可以通过移动记录指针来遍历 ResultSet 的每条记录,但程序可能不清楚该ResultSet里包含哪些数据列,以及每个数据列的数据类型,那么可以通过ResultSetMetaData来获取关于ResultSet的描述信息。

        ResultSet里包含一个getMetaData()方法,该方法返回该ResultSet对应的ResultSetMetaData对象。一旦获得了ResultSetMetaData对象,就可通过ResultSetMetaData提供的大量方法来返回ResultSet 的描述信息。常用的方法有如下三个。

  • int getColumnCount():返回该ResultSet的列数量。
  • String getColumnName(int column):返回指定索引的列名。
  • int getColumnType(int column):返回指定索引的列类型。

        下面是一个简单的查询执行器,当用户在文本框内输入合法的查询语句并执行成功后,下面的表格将会显示查询结果。

         上面程序中的粗体字代码就是根据 ResultSetMetaData分析 ResultSet的关键代码,使用ResultSetMetaData查询ResultSet包含多少列,并把所有数据列的列名添加到一个Vector里,然后把ResultSet里的所有数据添加到 Vector里,并使用这两个Vector来创建新的TableModel,再利用该TableModel 生成一个新的JTable,最后将该JTable显示出来。运行上面程序,会看到如图13.21所示的窗口。

猜你喜欢

转载自blog.csdn.net/indeedes/article/details/121223479