4.11.1 如何通过JDBC访问数据库
Java数据库连接(Java DataBase Connectivity,JDBC)用于在Java程序中实现数据库操作功能,它提供了执行SQL语句、访问各种数据库的方法,并为各种不同的数据库提供统一的操作接口,java.sql包中包含了JDBC操作数据库的所有类。通过JDBC访问数据库一般有如下几个步骤:
1>加载JDBC驱动器。将数据库的JDBC驱动加载到classpath中,在基于JavaEE的Web应用开发过程中,通常要把目标数据库产品的JDBC驱动复制到WEB-INF/lib下。
2>加载JDBC驱动,并将其注册到DriverManager中。一般使用反射Class.forName(String driveName)。
3>建立数据库连接,取得Connection对象。一般通过DriverManager.getConnection(url,username,password)方法实现,其中,url表示连接数据库的字符串,username表示连接数据库的用户名,password表示连接数据库的密码。
4>建立Statement对象或是PreparedStatement对象。
5>执行SQL语句。
6>访问结果集ResultSet对象。
7>依次将ResultSet、Statement、PreparedStatement、Connection对象关闭,释放掉所占用的资源,例如rs.close(),con.close()等。为什么要这么做呢?原因在于JDBC驱动在底层通常是通过网络IO实现SQL命令与数据传输的。
以下是一个用JDBC访问MySql的例子:
首先在MySql中创建Employee表:
creat table Employee{
id int primary key;
name varchar(20);
age int
};
其次,创建一个Java实例程序,如下所示:
import java.sql.*;
public class Test{
public static void main(String[] args) throws Exception{
String user = "user1";
String password = "password1";
String url = "jdbc:mysql//localhost:3306/Test";
String driver = "com.mysql.jdbc.Driver";
Connection con = null;
Statement stmt = null;
ResultSet rs = null;
try{
Class.forName(driver);
con = DriverMenager.getConnection(url,user,password);
stmt = con.creatStatement();
stmt.execute("insert into Employee values(1,'James1',25)");
stmt.execute("insert into Employee values(2,'James2',26)");
rs = stmt.executeQuery("select * from Employee");
while(rs.next()){
System.out.println(rs.getInt(1)+""+rs .getString(2)+""+rs.getInt(3));
}
}catch(SQLException el){
el.printStackTrace();
}finally{
try{
if(rs != null) rs.close();
if(stmt != null) stme.close();
if(con != null) con.close();
}catch(SQLException e){
System.out.println(e.getMessage());
}
}
}
}
4.11.2 JDBC处理事务采用什么办法
一个事务是由一条或多条对数据库操作的SQL语句所组成的一个不可分割的工作单元,只有当事务中的所有操作方法都正常执行完了,整个事务才会被提交给数据库。在JDBC 中,一般是通过commit()方法完成或rollback()方法来结束事务的操作。其中commit()方法表示完成对事务的提交,rollback()方法表示完成事务回滚,多用于在处理事务的过程中出现了异常的情况,这两种方法都位于java.sql.Connection类中。一般而言,事务默认操作是自动提交的,即操作成功后,系统将自动调用commit()方法,否则将调用rollback()方法。
当然,在JDBC中,也可以通过调用setAutoCommit(false)方法来禁止自动提交,然后就可以把多个数据库操作的表达式作为一个事务,在操作完成后调用commit()方法来实现整体提交,如果其中一个表达式操作失败,就会抛出异常而不会调用commit()方法。在这种情况下,就可以在异常捕获的代码块中调用rollback()方法进行事务回滚。通过此种方法可以保持对数据库的多次操作后,数据仍然保持一致性。
引申:JDBC有哪些事务隔离级别?
为了解决与”多个线程请求相同数据”相关的问题,事务之间通常会用锁相互隔离开。如今,大多数主流的数据库支持不同类型的锁。因此,JDBC API支持不同类型的事务,他们由Connection对象指派或确定。在JDBC中,定义了一下5种事务隔离级别:
1>TRANSACTION_NONE.JDB。不支持事务。
2>TRANSACTION_READ_UNCOMMITTED。未提交读。说明在提交前一个事务可以看到另一个事务的变化。这样读”脏”数据、不可重复读和虚读都是允许的。
3>TRANSACTION_READ_COMMITTED。已提交读。说明读取未提交的数据是不允许的。这个级别仍然允许不可重复读和虚读产生。
4>TRANSACTION_REPEATABLE_READ。可重复读。说明事务保证能够再次读取相同的数据而不会失败,但虚读仍然会出现。
5>TRANSACTION_SERIALIZABLE。可序列化。是最高的事务级别,它防止读”脏”数据、不可重复读和虚读。
(*备注:
1)读”脏”数据。一个事务读取了另一个事务尚未提交的数据,例如,当事务A与事务B并发执行时,当事务A更新后,事务B查询读取到A尚未提交的数据,此时事务A回滚,则事务B读取到的数据是无效的”脏”数据。
2)不可重复读。一个事务的操作导致另一个事务前后两次读取到不同的数据,例如,当事务A与事务B并发执行时,当事务B查询读取数据后,事务A更新操作更改事务B查询到的数据,此时事务B再次读取该数据,发现前后两次数据不一样。
3)虚读(幻读)。一个事务的操作导致另一个事务前后两次查询的结果数据量不同,例如,当事务A与事务B并发执行时,当事务B查询读取数据后,事务A新增或删除了一条满足事务A的查询条件的记录,此时,事务B再次查询,发现查询到前次不存在的记录,或者前次额某个记录不见了。
*)
4.11.3 Class.forName的作用是什么
在Java语言中,任何类只有被装载到JVM上才能运行。Class.forName()方法的作用就是把类加载到JVM中,它会返回一个与带有给定字符串名的类或接口相关联的Class对象,并且JVM会加载到这个类,同时JVM会执行该类的静态代码段。
在使用JDBC连接数据库前,一般都会调用Class.forName(“com.mysql.jdbc.Driver”)方法来加载JDBC驱动,那么是否一定需要调用这个方法呢?如果是,那为什么要调用这个方法呢?其实,并不一定非要调用这种方法,例如Test t = (Test)Class.forName(“Test”).newInstance()语句和Test t = new Test()语句就具有相同的效果,所以使用new也可以,但二者的区别也非常明显:创建对象的方式不同。前者使用类加载机制,后者是创建了一个新的类。使用第一种方法往往能提高软件的可扩展性,例如,一个软件项目开发后会被多家公司来使用,每家公司的处理流程大致相同,只有个别公司的业务逻辑不同,在开发过程中,完全可以把不通用的地方抽取出来定义成一个接口BussinessInterface,针对每个公司不同的业务流程定义不同的实现类sub1、sub2、sub3等,通过创建不同的子类完成不同公司的业务需求。为了达到良好的可扩展性,可以把子类采用配置文件的方式放到xml文件中,在程序部署时,只需要从读配置文件中读取类名className,然后采用BussinessInterface b = (BussinessInterface)Class.forName(className).newInstance()创建实例即可提高开发人员的开发效率。当以后再有新需求时,即使开发了新的子类,也不需要修改创建实例的代码,只需要修改配置文件即可,从而使得程序具有很好的可扩展性。
JDBC规范中要求Driver类在使用前必须向DriverManager注册自己,所以,当执行Class.forName(“com.mysql.jdbc.Driver”)时,JVM会加载名字为”com.mysql.jdbc.Driver”对应的Driver类,而com.mysql.Driver类的实现如下所示:
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!");
}
}
}
在调用Class.forName()方法时,这个Driver类被加载了,由于静态部分被执行,因此Driver也被注册到了DriverManager中。
4.11.4 Statement、PreparedStatement和CallableStatement有什么区别
Statement用于执行不带参数的简单SQL语句,并返回它所生成结果的对象,每次执行SQL语句时,数据库都要编译该SQL语句。以下是一个最简单的SQL语句:
Statement stmt = conn.getStatement();
stmt.executeUpdate(“insert into client values(‘aa’,’aaaa’)”);
PreparedStatement表示预编译的SQL语句的对象,用于执行带参数的预编译SQL语句。
CallableStatement则提供了用来调用数据库中存储过程的接口,如果有输出参数要注册,说明是输出参数。下面给出一个使用PreparedStatement的例子:
import java.sql.*;
public class Test {
public static void main(String[] args) throws Exception{
String user = "user1";
String password = "pswrd1";
String url = "jdbc:mysql://localhost:3306/Test";
String driver = "com.mysql.jdbc.Driver";
Connection conn = null;
PreparedStatement stmt = null;
ResultSet rs = null;
try{
Class.forName(driver);
conn = DriverManager.getConnection(url,user,password);
stmt = conn.prepareStatement("select * from Employee where id=?");
stmt.setInt(1, 1); //传递参数(第一个问号,传递的值)
rs = stmt.executeQuery();
while(rs.next()){
System.out.print(rs.getInt(1)+""+rs.getString(2)+""+rs.getInt(3));
}
}catch(SQLException el){
el.printStackTrace();
}finally{
try{
if(rs != null) rs.close();
if(stmt != null) stmt.close();
if(conn != null) conn.close();
}catch(SQLException e){
System.out.print(e.getMessage());
}
}
}
}
运行结果:
1 James1 25
虽然Statement对象与PreparedStatement对象能够完成相同的功能,但相比之下,PreparedStatement具有以下优点:
1>效率更高。在使用PreparedStatement对象执行SQL命令时,命令会被数据库进行编译和解析,并放到命令缓冲区。然后,每当执行同一个PreparedStatement对象时,由于在缓冲区中可以发现预编译的命令,虽然它会被再解析一次,但是不会被再次编译,是可以重复使用的,能够有效提高系统性能,因此,如果要执行插入、更新、删除等操作,最好使用PreparedStatement。鉴于此,PreparedStatement适用于存在大量用户的企业级应用软件中。
2>代码可读性和可维护性更好。一下两种方法分别使用Statement与PreparedStatement来执行SQL语句,虽然方法2具有更好的可读性。
方法一:
stmt.executeUpdate("insert into t(col1,clo2) values('"+var1+"','"+var2+"')");
方法二:
perstmt = conn.preparedStatement("insert into tb_name (col1,clo2) values(?,?)");
perstmt.setString(1,var1);
perstmt.setString(2,var2);
3>安全性更好。使用PreparedStatement能够预防SQL注入攻击、所谓SQL注入,指的是通过把SQL命令插入到Web表单递交或输入域名或页面请求的查询字符串,最终达到欺骗服务器,达到执行恶意SQL命令的目的。注入只对SQL语句的编译过程有破坏作用,而执行阶段只是把输入串作为数据处理,不再需要对SQL语句进行解析,因此也就避免了类似select * from Employee where id=1的SQL注入问题的发生。
CallableStatement由preparedCall()方法所创建,它为所有DBMS(Database Management System,数据库管理系统)提供了一种以标准形式调用已存储过程的方法。它从PreparedStatement中继承了用于处理输入参数的方法,而且还增加了调用数据库中的存储过程和函数以及设置输出类型参数的功能。
4.11.5 getString()方法与getObjet()方法有什么区别
JDBC提供了getString()、getInt()和getData()等方法从ResultSet中获取数据,当查询结果集中的数据量较小时,不用考虑性能,使用这些方法完全能够满足需求,但是当查询结果集中的数据量非常大时,则会抛出异常;OrecleException未处理;ORA-01000:maximun open cuesors exceeded(以访问Oracle数据库为例)。通常情况下,使用getObject()方法就可以解决这个问题。
getString()或getInt()等方法在被调用时,程序会一次性地把数据都放到内存中,然后通过调用ResultSet的next()和getString()等方法来获取数据。当数据量大到内存放不下的时候就会抛出异常,而使用getObject()方法就不会出现这种问题,因为数据不会一次性被读到内存中,每次调用时会直接从数据库获取数据,因此使用这种方法不会因为数据量过大而出错。
4.11.6 使用JDBC时需要注意哪些问题
在使用JDBC编程时,首先需要建立与数据库的连接,才能完成对数据库的访问,由于与数据库的连接是非常重要的资源,JDBC连接池提供了JDBC连接定义和数目有限的连接,如果连接数量不够,就需要长时间的等待。不正常关闭JDBC连接会导致等待回收无效的JDBC连接。只有正常的关闭和释放JDBC连接,JDBC资源才可以被快速的重用,从而使得系统性能得到改善。因此在编程时,一定要保证释放不再使用的连接。
一般来讲,在使用JDBC访问数据库时,createStatement和prepareStatement最好放在循环外面,而且使用了这些Statement后,需要及时关闭。最好是在执行了一次executeQuery、executeUpdate等之后,如果不需要使用结果集(ResultSet)的数据,就马上将Statement关闭。因为每次执行conn.createStatement()或conn.prepareStatement(),实际上都相当于在数据库中打开了一个cusor(游标),如果把这两个方法的调用放到循环里,会一直不停地打开cursor。如果不能及时的关闭,会导致程序抛出异常。
4.11.7 什么是JDO
Java数据对象(Java Data Object,JDO)是一个用于存取某种数据仓库中的对象的标准化API,它使开发人员能够间接地访问数据库。
JDO是JDBC的一个补充,它提供了透明的对象存储,因此对开发人员来说,存储数据对象完全不需要额外的代码(例如JDBC API的使用)。这些繁琐的工作已经转移到JDO产品提供商身上,使开发人员解脱出来,从而集中时间和精力在业务逻辑上。另外,相较于JDBC,JDO更灵活、更通用,它提供了到任何数据底层的存储功能,例如关系数据库、文件、xml以及对象数据库管理系统(Object Database Menagement System,OOBMS)等,使得应用可移植性更强。
4.11.8 JDBC与Hibernate有什么区别
Hibernate是JDBC的封装,采用配置文件的形式将数据库的连接参数写到xml文件中,至于对数据库的访问还是通过JDBC来完成的。
Hibernate是一个持久层框架,它将表的信息映射到XML文件中,再从XML文件映射到相应的持久化类中,这样可以使用Hibernate独特的Hibernate查询语言(Hibernate Query Language,HQL)了。Hibernate的HQL查询语句返回的是List