7 JDBC
JDBC是Java的一种扩展。
警告:学习此章节需要对数据库知识有一定了解。
JDBC(Java Data Base Connectivity)是Java设计者为数据库编程提供的一组接口。这组接口对于开发者来说,是访问数据库的工具;对于数据库提供商来说,是驱动程序的编写规范,从而保证Java可以访问他们的数据库产品。因此使用JDBC后,开发者可以更加专注于业务的开发,而不必为特定的数据库编写程序。
JDBC面向的是两个方向:开发者和数据库提供商。对于开发者来说,只要使用数据库提供商提供的驱动程序,就可以方便地访问数据库了;对于数据库提供商来说,他们的职责就是提供JDBC的规范,编写正确的驱动程序。
JDBC的架构:用户即开发者通过使用JDBC,运行相应数据库的驱动,然后获取连接,驱动就自动连接相应数据库。(此处的驱动是封装的链接器,主要用于Java和数据库的链接)JDBC使用可插拔的方式,让用户以同一种方式访问数据库。
JDBC的生命周期:
- 加载数据库驱动。
- 注册数据库驱动。
- 获取连接会话。
- 进行数据库操作。
- 关闭并释放连接。
在JDBC的生命周期中,需要用到几个重要的类或接口,例如Connection、Driver、DriverManager以及一些具体的驱动类。JDBC使用了接口Driver和接口Connection,JDBC设计者并未对其进行实现,具体的实现留给了数据库提供商,正是这种面向接口的编程,使得JDBC的扩展更加灵活健壮,使得我们可以对JDBC进行插拔操作。
7.1 JDBC的核心类和接口
7.1.1 java.sql.Connection
该接口的实现类负责维护Java开发者与数据库之间的会话。特定的数据库需要实现该接口,以便开发者能正确地操作数据库。开发者拥有该类的实例后,就可以访问数据库了,并可以执行特定的操作。下面列出了该接口中一些重要且常用的方法:
- Statement createStatement() throws SQLException:该方法返回一个用于指定静态SQL的Statement对象,开发者通过该方法获得Statement实例后,即可通过该实例执行静态SQL语句并获取返回结果。
- PreparedStatement prepareStatement(String sql) throws SQLException:该方法返回一个SQL命令执行器PreparedStatement,PreparedStatement与Statement不同的是,该类在初始化时需要传入一个SQL,SQL需要的条件值,可通过参数的方法设置(例如setInt(int index, int value)、setString(int index, String value)),该类会预编译SQL命令,因此在执行效率上高于Statement,但是它只能执行特定的SQL语句。
- void commit() throws SQLException:提交数据库操作进行的数据库操作,默认情况下connection会自动提交,即执行每条SQL语句后都会自动提交,如果取消了自动提交,则必须使用此方法进行提交,否则对数据库的操作将无效。
- void rollback() throws SQLException:取消当前事务的所有数据库操作,将已经修改的数据还原成初始状态。
7.1.2 java.sql.Driver接口
数据库提供商提供的驱动类必须实现此接口,才能接入到JDBC中。此接口的实现代码如下:
package java.sql;
public interface Driver {
Connection connect(String url, java.util.Properies info) throws SQLException;
boolean acceptsURL(String url) throws SQLException;
DriverProperiyInfo[] getPropertyInfo(String url, Properties info) throws SQLException;
int getMajorVersion();
int getMinorVersion();
boolean jdbcCompliant();
}
这就是Jvaa驱动程序的规格,所有的数据库提供商提供的驱动都必须实现这个接口,这个接口中包含6个方法:
-
Connection connect(String url, java.util.Properies info) throws SQLException:该方法负责创建与指定数据库的会话,通过会话来进行数据库操作。该方法拥有两个参数,其中参数url表示数据库的连接地址,例如:
jdbc:mysql://localhost:3306/student
参数info表示创建会话所需的参数,参数用Properties类来进行描述,常见的参数为用户名、密码等。如果连接失败,该方法会抛出SQLException。
-
boolean acceptsURL(String url) throws SQLException:该方法通常负责检查url是否符合特定数据的连接协议,如果url格式合法,则返回true,否则返回false。
-
DriverProperiyInfo[] getPropertyInfo(String url, Properties info) throws SQLException:可以通过该方法获取驱动程序提供的信息。该方法返回信息由驱动开发者根据自身的情况决定。该方法需要传入两个参数:
- url即数据库的连接地址。
- info连接数据库所必须的验证等辅助信息。
-
int getMajorVersion():该方法返回驱动程序的主版本号( 例如,版本1.5.1254.0中的1表示主版本)。
-
int getMinorVersion():该方法返回驱动程序的此版本号( 例如,版本1.5.1254.0中的5表示次版本)。
-
boolean jdbcCompliant():检测驱动程序是否符合JDBC标准,以及支持的SQL是不是标准的SQL或其扩展集。
7.1.3 java.sql.DriverManager
如果说数据库提供商开发的驱动是插头,那么DriverManager类就是插座。用户需要将数据库驱动注册到DriverManager中,这样才能访问和操作数据库。DriverManager是JDBC的核心和管理者,DriverManager含有两个内部类,分别是DriverInfo和DriverService(注意:这两个都比较重要)。
DriverInfo类的源代码如下:
class DriverInfo {
Driver driver;
Class driverClass;
String driverClassName;
public String toString() {
return ("driver[className=" + driverClassName + "," + driver + "]");
}
}
DriverInfo类用于表示驱动类信息,其中包含三个属性,分别是driver、driverClass、driverClassName:
- driver的类型是接口Driver,因此它可以被赋予所有具体实现类的实例。
- driverClass表示驱动类的具体类型,比如com.mysql.jdbc.Driver(这个老版本,新版本为:com.mysql.cj.jdbc.Driver)。
- driverClassName表示具体驱动类的类名。
DriverService类通过Service的配置文件,加载java.sql.Driver的实现类(即具体的数据库驱动类)以便这些类能够被实例化。
DriverService类的源代码如下:
class DriverService implements java.security.PrivilegedAction {
Iterator ps = null;
public DriverService() {...};
public Object run() {
ps = Service.providers(java.sql.Driver.class);
try {
while (ps.hasNext()) {
ps.next();
}
} catch(Throwalble t) {}
return null;
}
}
在DriverService类中,首先定义了一个迭代器实例,在run方法中通过Service类的providers获取java.sql.Driver的实现类,这些实现类被配置在service文件中,可以在META-INFO的service文件夹下找到这些配置文件,获取驱动类的迭代器后,逐一遍历迭代器中的驱动。
在DriverManager中除了这两个重要的内部类外,还有很多功能方法,下面列出了DriverManager中常用的方法。
1. getConnection()
在该方法中DriverManager选择合适的驱动程序与数据库建立连接,然后把该连接返回给用户。
方法原型:public static Connection getConnection(String url, String user, String password) throws SQLException
方法源代码:
public static Connection getConnection(
String url, String user, String password) throws SQLException {
java.util.Properties info = new java.util.Properties();
ClassLoarder callerCL = DriverManager.getCallerClassLoarder();
if (user != null) {
info.put("user", user);
}
if (password != nul) {
info.put("password", password);
}
return (getConnetion(url, info, callerCL));
}
private static Connection getConnetcion(
String url, java.util.Properites info, ClassLoarder callerCL) throws SQLException {
java.util.Vector drivers = null;
synchronized (DriverManager.class) {
if (callerCL == null) {
callerCl = Thread.currentThread().getContextClassLoarder();
}
}
if (!initialized) {
initialize();
}
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 {
Connection result = di.driver.connect(url, info);
if (result != null)
return (result);
} catch (SQLException ex) {
if (reason == null)
reason = ex;
}
}
}
大致思路:用户先调用DriverManager的getConnection(String url, String user, Stirng password)方法获取与数据库的会话连接,在该方法内部DriverManager首先遍历已经注册的数据库驱动,然后通过驱动程序的acceptsURL(String url)方法找到能过够解析url的合适驱动,接着通过该驱动去连接数据库,建立会话,最后把会话反馈给用户。这个方法都是以接口的形式访问数据库驱动,这样可以确保它不依赖于具体的驱动。
代码解读:代码中包括两个静态方法,一个是公有的静态方法getConnection(String url, String user, String password),另一个是私有的静态方法getConnection(String url, java.util.Properties info, ClassLoarder callerCL)。
- 在公有的getConnection静态方法中,首先获取了当前驱动程序的类加载器,然后将连接数据库所需要的参数封装成Properties类的实例,然后将类加载器、参数信息,以及连接字符串作为参数传递给私有的getConnection静态方法。
- 在私有的getConnection静态方法中,首先获取当前类的类加载器,然后遍历注册的所有数据库驱动。在遍历算法内部,首先判断驱动类是不是当前类加载器加载的,如果不是则跳过,如果是则尝试着进行连接,如果连接通过就会返回一个Connection接口的实现类,终止遍历,建立会话结束。
在这个方法的实现中,使用驱动类型为Driver接口,也就是说,它是针对Driver接口进行编程,这样做的好处是,任何实现Driver接口的驱动类都可以对其进行赋值,从而实现插拔。
2. registerDriver()
数据库提供商编写的驱动程序,必须通过注册,才能在驱动管理器中进行应用。
方法原型:public static synchronized void registerDriver(java.sql.Driver driver) throws SQlException
方法源代码:
public static synchronized void registerDriver(
java.sql.Driver driver) throws SQLException {
if (!initialized) {
initialize();
}
DriverInfo di = new DriverInfo();
di.driver = driver;
di.driverClass = driver.getClass();
di.driverClassName = di.driverClass.getName();
writeDrivers.addElement(di);
readDrivers = (java.util.Vector) writeDrivers.clone();
}
这是一个共有的静态方法,方法内部首先判断DriverManager有没有进行初始化,如果没有则进行初始化。然后创建一个驱动信息类,并对其进行赋值,然后再驱动集中添加驱动,最后更新驱动集。该方法一般都有驱动程序进行调用注册,具体情况将在介绍具体驱动类时详述。
3. getDriver()
通过该方法可以获取能解析url的合适驱动。
方法原型:public static Driver getDriver(String url) throws SQLException
方法源代码:
public static Driver getDriver(String url) throws SQLException {
java.util.Vector drivers = null;
if (!initialized) {
initialize();
}
synchronized (DriverManager.class) {
drivers = readDrivers;
}
ClasssLoarder callerCL = DriverManager.getCallerClassLoarder();
for (int i = 0; i < drivers.size(); i++) {
DriverInfo di = (DriverInfo) drivers.elementAt(i);
if (getCallerClass(callerCL, di.driverClassName) != di,driverClass) {
continue;
}
try {
if (di.driver.acceptsURL(url)) {
return (di.driver);
}
} catch (SQLException ex) { }
}
throw new SQLException("No suitable driver", "08001");
}
代码解读:该方法首先定义了一个驱动信息集合,然后判断DriverManager是否初始化,如果没有则进行初始化,接着读取驱动信息集合。接着遍历驱动信息集合。在遍历代码块中,首先判断有没有权限访问驱动类,如果没有则跳过,如果有则调用驱动类的acceptsURL(String url)方法解析数据库连接串。如果能解析数据库连接串,则返回该驱动。遍历结束后,如果仍没找到合适的驱动,则抛出没有合适的驱动类异常。
7.2 驱动的实现
介绍了JDBC的核心类和接口后,下面介绍驱动的具体实现。以MySQL的驱动类作为例子,下面为com.mysql.jdbc.Driver(老版本,新版本为com.mysql.cj.jdbc.Driver)驱动类的源代码:
public class MySqlDriver 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 {
/*...*/
}
}
该类继承了NonRegisteringDriver类并实现了java.sql.Driver接口,具体的数据库连接与处理细节都放到NonRegisteringDriver中进行了实现,在com.mysql.jdbc.Driver中只实现了注册功能。在介绍java.sql.DriverManager中的组成方法时,我们曾说过驱动的注册多由驱动自身完成,那么我们看看驱动是如何注册到DriverManager中的。在Driver类中,有一段静态代码域,可以看到在这段代码域中,调用了DriverManager的registerDriver()方法,对驱动进行了注册,代码域会在类加载的时候执行,这就是为什么在使用驱动之前都需要调用Class类的forName方法加载驱动的原因。
NonRegisteringDriver也继承了接口java.sql.Driver,在其内部可以看出数据库连接与处理的详细逻辑:属性块、父类方法块、工具与解析方法块。
7.2.1 属性块
1. 协议匹配常量
private static final String REPLICATION_URL_PREFIX = "jdbc:mysql:replication://";
private static final String URL_PREFIX = "jdbc:mysql://";
private static final String MXJ_URL_PREFIX = "jdbc:mysql:mxj://";
private static final String LOADBALANCE_URL_PREFIX = "jdbc:musql:loadbalance://";
这些常量定义了MySQL的数据库驱动所能理解和接受的协议。通过解析,若发现连接字符串不是使用这些协议,MySQL驱动将禁止数据库连接。
2. 参数名称常量
private static final String HOST_PROPERTY_KET = "HOST";
private static final String PASSWORD_PROPERTY_KEY = "password";
private static final String DBENAM_PROPERTY_KEY = "DBNAME";
private static final String USER_PROPERTY_KEY = "user";
private static final String PROTOCOL_PROPERTY_KEY = "PROTOCOL";
private static final String PATH_PROPERTY_KEY = "PATH";
上面列出了部分参数名称常量在类NonRegisteringDriver内部。
3. 其他常量
/** Should the driver generate debugging output? */
public static final boolean DEBUG = false;
/** Should the driver generate method-call traces? */
public static final boolean TRACE = false;
这些常量用于控制调试以及输出信息。
7.2.1 父类方法块
1. connection(String url, Properties info)
该方法根据url辨别出用户使用的协议,根据用户的协议创建合适的连接,然后将连接返回给用户。参数info表示建立连接时所需要的参数,比如用户名和密码等,该方法的代码如下:
public java.sql.Connection connect(
String url, Properites info) throws SQLException {
if (url != null) {
if (StirngUtils.startsWithIgnoreCase(url, LOADBALANCE_URL_PREFIX)) {
return connectLoadBalanced(url, info);
} else if (StringUtils.startsWithIgnoreCase(url, REPLICATION_URL_PREFIX)) {
return connectReplicationConnection(url, info);
}
}
Properites props = null;
if ((props = parseURL(url, info)) == null) {
return null;
}
if(!"1".equals(props.getProperty(NUM_HOSTS_PROPERTY_KEY))) {
return connectFailover(url, info);
}
try {
Connection newConn = com.mysql.jdbc.ConnectionImpl.getInstance(
host(props), port(props), props, database(props), url);
return newConn;
} catch (SQLException sqlEx) {
throw sqlEx;
} catch (Exception ex) {
SQLException sqlEx = SQLError.createSQLException(
Messages.getString("NonRegisteringDriver.17") +
ex.toString() +
Messagex.getString("NonRegisteringDriver.18")),
SQLError.SQL_STATE_UNABLE_TO_CONNECT_TO_DATASOURCE, null);
sqlEx.initCause(ex);
throw sqlEx;
}
}
方法解读:在该方法中,首先判断url是否为空,如果不为空则判断url符合哪种协议,然后根据具体的协议创建合适的连接,此处的协议包括以下两个:
-
jdbc:mysql:loadbalance://
该协议主要用于集群环境,可以时服务器负载平衡,它采用负载平衡算法随机地从服务器列表中选择一个服务器创建连接。
-
jdbc:mysql:replication://
该协议主要用于备份赋值数据,一般服务器分为主服务器和备份服务器,在一部的赋值操作下,实现数据从一台服务器到另一台服务器的备份。
如果url为空或者不是这两个协议中的任何一个,则判断能否解析这个url字符串,如果不能则返回null,表示创建失败,否则继续,接着判断服务器的数量。如果是1,则建立故障转移连接,如果不是则继续尝试连接,并返回一个ConnectionImpl实例。
2. acceptsURL(String url)
该方法通过解析URL来判断连接字符串是否合法。其代码实现如下:
public boolean acceptsURL(String url) throws SQLException {
return (parseURL(url, null) != null);
}
该方法调用了parseURL方法来解析连接字符串,如果返回为空,则表示来连接字符串不合法,否则合法。
3. parseURL(String url, Properties defaults)
该方法用于解析来连接字符串和连接参数,并将解析后的结果放入到一个Properties实例中,其代码如下:
if (!StringUtils.startsWithIgnoreCase(url, URL_PREFIX) &&
!StringUtils.startsWithIgnoreCase(url, MXJ_URL_PREFIX) &&
!StringUtils.startsWithIgnoreCase(url, LOADBALANCE_URL_PREFIX) &&
!StringUtils.startsWithIgnoreCase(url, REPLICATION_URL_PREFIX)) {
return null;
}
该代码块的功能是:如果url与指定的协议不匹配,则返回null,表示连接字符串不合法,解析终止。
下面列出的为连接字符串中的参数解析代码块:
int index = url.indexOf("?");
if (index != -1) {
String paramString = url.substring(index+1, url.length());
url = url.substring(0, index);
StringTokenizer queryParams = new StringTokenizer(paramString, "&");
while (queryParam.hasMoreTokens()) {
String parameterValuePair = queryParam.nextToken();
int indexOfEquals = StringUtils.indexOfIgnoreCase(0, parameterValuePair, "=");
String parameter = null;
String value = null;
if (indexOfEquals != -1) {
parameter = parameterValuePair.substring(0, indexOfEquals);
if (indexOfEquals+1 < parameterValuePair.length()) {
value = parameterValuePair.substring(indexOfEquals+1);
}
}
if ((value != null && value.length() > 0)) &&
(parametet != null && parameter.length() > 0)) {
try {
urlProps.put(parameter, URLDecoder.decode(value, "UTF-8"));
} catch (UnsupporedEncodingException badEncoding) {
urlProps.put(parameter, URLDecoder.decode(value));
} catch (NoSuchMethodError nsme) {
urlProps.put(parameter, URLDecoder.decode(value));
}
}
}
}
方法解读:该方法首先找到“?”的位置,其后为连接参数。然后使用“&”符号把参数对分开,参数对以键和值的方式存在,其形式是key=value。因此通过对“=”可以把键和值分离开来,放入到Properties实例中,记录下来。
4. database(Properites props)
该方法用于返回数据库的名称,其代码实现如下:
public String dataBase(Properites props) {
return props.getProperty(DBNAME_PROPERTY_KEY);
}
该方法的实现很简单,从配置实例中读取数据库的名称,类似这样的方法还有很多,比如读取连接端口、读取用户名等。
7.3 ODBC
开放数据库互连(Open Database Connectivity, ODBC)是微软公司开发服务结构(Windows Open Services Architechture, WOSA)中有关数据库的一个组成部分,它建立了一组规范,提供了一组对数据库访问的标准API(应用程序编程接口),这些API利用SQL来完成其大部分任务。ODBC本身也提供了对SQL的支持,用户可以直接将SQL语句送给ODBC,然ODBC不适合直接在Java中使用,因为它使用C语言接口。从JAva调用本地C代码在安全性、实现、坚固性和程序的自动移植性方面都有许多缺点,因此JDBC的开发人员创建了JDBC-ODBC的桥接,以ODBC为基础进行数据库连接。JDBC API对于基本SQL抽象和概念是一种自然的Java接口,它建立在ODBC上,保留了ODBC的基本设计特征。两种接口都基于X/Open SQL CLI(调用级接口),他们直接最大的区别在于:JDBC以Java的风格与有点为基础并进行优化,因此更加易于使用。
7.3.1 建立连接
注意:书籍已经过时,此部分为笔者更新。
想要进行数据库的连接,首先需要建立与数据库之间的连接会话,所有的操作都是基于这个会话基础上进行的。建立连接的代码如下:
public class MySqlDAO {
public static Connection getConnection() throws Exception {
String driverName = "com.mysql.cj.jdbc.Driver";
String url = "jdbc:mysql://localhost:3306/student";
String userName = "root";
String password = "123456";
Class.forName(driverName);
Connection con = DriverManager.getConnection(url, userName, password);
return con;
}
}
这段代码的主要功能是建立于数据库之间的连接会话。下面来逐一分析这段代码,首先是连接参数定义:
String driverName = "com.mysql.cj.jdbc.Driver";
String url = "jdbc:mysql://localhost:3306/student";
String userName = "root";
String password = "123456";
这段代码块首先定义了几个属性:驱动名称、数据库连接字符串、数据库用户名以及数据库密码。定义这些属性请注意以下几个点:
- 驱动名称必须为全名,而且保证路径正确。
- 连接字符串分为三部分:
- 连接协议:jdbc:mysql://。
- 数据库地址:localhost:3306/,又包含主机地址和数据库端口号。
- 数据库名称:student。
- 确保用户名和密码都正确,否则会拒绝连接。
接着是加载数据库驱动:
Class.forName(driverName);
前面介绍过,加载数据库驱动的时候,驱动程序会自动调用DriverManager中的registerDrivera(Driver driver)方法,将自身注册到管理器中。
接下来是创建并获取连接:
Connection con = DriverManager.getCOnnection(url, userName, password);
此处调用了驱动管理器的getConnection()的三参数方法,驱动管理器对该方法有多个重载。该方法会根据传入的参数选择合适的连接会话返回给用户。至此已经获取了数据库的连接会话,可以在此会话基础上进行数据库操作了。注:执行这段代码前,请确保将数据库驱动的JAR包加入到项目中。
7.3.2 通过Statement执行SQL语句
下面将介绍获取连接会话后,通过Statement对数据进行增删改查。在对数据库进行操作前,需要获取Statement对象,对于执行数据的命令。因此需要在MySqlDAO中添加如下方法:
public static Statement getStatement() throws Exception {
Statement stmt = getConnection().createStatement();
return stmt;
}
1. 创建表
代码如下:
public class DBTest {
public static void mian(String[] args) throws Exception {
Statement stmt = MySqlDAO.getStatement();
String sql = "create table student(no int primary key, name char(20))";
stmt.execute(sql);
stmt.close();
}
}
这段代码首先通过MySqlDAO获取了数据库命令执行对象Statement实例,然后定义了一条数据库语句。该语句的作用是,创建一张student表,表中有两个字段:一个整数型的no(学号),另一个是字符类型的name(姓名)。接着调用Statement对象的execute(String sql)方法,执行这条数据库语句,执行后,数据库中即可发现多了一张student表。
2. 插入数据
代码如下:
public class DBTest1 {
public static void main(Stringp[] args) throws Exception {
Statement stmt = MySqlDAO.getStatement();
String sql = "insert into student values(1, 'jim')";
stmt.execute(sql);
stmt.close();
}
}
插入操作的代码与创建表的代码很相似,不一样的是执行的SQL语句不同,修改和删除也与插入操作类似,在此就不进行介绍。
3. 读取数据
public class DBTest2 {
public static void mian(String[] args) throws Exception {
Statement stmt = MySqlDAO.getStatement();
String sql = "select * from student where no=1";
ResultSet rs = stmt.executeQuery(sql);
while (rs.next()) {
System.out.println("no: " + rs.getInt("no"));
System.out.println("name: " + rs.getString(2));
}
}
}
这段代码的功能是读取表student中no等于1的数据并打印出来。首先仍是先获取数据库命令执行对象Statement实例,接着定义一条查询SQL语句,执行这条语句会返回一个结果集,结果集中包含student表中no等于1的学生信息,调用结果集ResultSet的next()方法,逐行读取学生信息,并将学生信息打印在命令行上。rs提供了很多方法用于获取属性字段,可以通过索引也可以通过字段名称。需要注意一点,字段的属性必须正确,不能通过getInt()方法获取字符串字段的值。可以发现,对于字段较少的表,这种获取信息的方法还可以接受,表中的字段有急事甚至上百的时候让人难以接受。
7.3.3 通过PreparedStatement执行SQL语句
本章的前面曾介绍过Statement与PreparedStatement的区别,下面将介绍如何使用PreparedStatement。首先需要获取PreparedStatement实例,因此向MySqlDAO中添加下面方法:
public static PreparedStatement preparedStatement(
String sql) throws Exception {
return getConnection().prepareStatement(sql);
}
该方法获取一个PreparedStatement对象,并为其指定SQL语句,获取PreparedStatement实例后,下面展示如何使用:
public class DBTest3 {
public static void main(Stirng[] args) throws Exception {
String sql = "select * from student where no=?";
PreparedStatement ps = MySqlDAO.preparedStatement(sql);
ps.setInt(1, 1);
ResultSet rs = ps.executeQuery();
while (rs,next()) {
System.out.println("no: " + rs.getInt("no"));
System.out.println("name: " + rs.getString(2));
}
}
}
这段代码仍然是先指定一条SQL语句,但这条语句不同的是它采用“?”代替了具体的值,PreparedStatement对象允许在执行SQL语句是才进行参数指定。PreparedStatement对象有多个设置参数值的方法,在设置参数时,需要知道参数的值类型,比如int类型可以使用setInt(int paramIndex, int value)进行参数设定,前一个参数表示参数的索引,即它代表着第几个问好,后面的参数为属性值。PreparedStatement对象会预先编译SQL语句,因此它的执行效率高于Statement,但它只能执行预先设定的SQL,因此多用于指定特定SQL的场景中。PreparedStatement的其他数据库操作于此类似,因此不再赘述。
7.4 JDBC进阶
本节将对JDBC的高级操作进行进一步的讨论,这些操作包括:事务操作、存储过程操作以及数据库连接池。
7.4.1 事务
事务是指一组操作的组合,这组操作作为一个逻辑单元来执行。因此,它在执行的过程中,要么不执行,要么全部执行。如果执行过程中出现错误,必须退回到原来的初始状态。
JDBC对事务的操作提供了支持,主要通过COnnection类的rollback()、commit()以及setAutoCommit(boolean autoCommit)等方法进行事务回滚、提交操作。
下面演示了事务的实例:
public class DBTest4 {
public static void main(String[] args) {
Connection con = null;
try {
con = MySqlDAO.getConnection();
con.setAutoCommit(false);
Statement stmt = con.createStatement();
String sql1 = "select max(no) from student";
ResultSet rs = stmt.executeQuery(sql1);
int no = 0;
while (rs.next()) {
no = rs.getInt(1) + 1;
}
String sql2 = "insert into student values("+ no +", 'wahaha')";
stmt.execute(sql2);
con.commit();
} catch (Exception e) {
try {
con.rollback();
} catch (SQLException el) {
el.printStackTrace();
}
}
}
}
这个例子仍然使用以前定义的MySqlDAO工具类来获取连接。获取连接后,首先将连接会话Connection的自动提交取消,再通过连接会话创建了数据库命令执行对象Statement;接着执行了一条查询语句,当前最大的学生的学号no,获取这个no值后将其增加1,获取新的学号值,并将新的学生作为此值插入到数据库中;最后提交事务。在这个事务操作的过程中,如果出现了错误,会在异常捕获后,对事物进行回滚。
7.4.2 存储过程
存储过程相当于存在再数据库服务器中的函数,它接受输入并返回输出。该函数会在数据库服务器上进行编译,供用户多次使用,因此可以大大提高数据操作的效率。MySQL中创建一个存储过程的语句如下:
Delimiter $$
Create procedure insertNewStudent (in newName varchar(20), out nowNo int)
begin
declare maxid int;
select max(no)+1 into maxid from student;
insert into student values(maxid, newName);
select max(no) int newNo form student;
end
$$
这个存储过程的作用是向Student表中插入一条新纪录,输入参数是新记录学生的姓名,输出参数是新学生的学号。存储过程的内部首先获取当前最大的学生号的值,然后将其加1作为新学生的学生号,并把胜场的学生号和姓名插入到数据库中。下面对这一SQL语句进行说明。首先需要定义一个接受符,因为在存储过程中,“;”被用做一句语句结束,因此不能作为整个SQL接受标志,我们定义的结束符是“$$”,定义方法为:
Delimiter $$
接着创建存储过程,存储过程名称是必需的,输入参数和输出参数是可选的。
Create procedure insertNewStudent (in newName varchar(20), out nowNo int)
下面看看存储过程的主题:
begin
declare maxid int;
select max(no)+1 into maxid from student;
insert into student values(maxid, newName);
select max(no) int newNo form student;
end
$$
首先以begin作为开始,再定义一个整体变量maxid为新生的学生号,接着从学生表中找出当前最大的学生号并将其加1赋给maxid。将新的学生号以及姓名插入到学生白表中,然后在此查询最大的学生号,赋值给输出参数。以end表示存储过程体结束,最后以$$表示存储过程声明结束。我们心在数据库中执行这条SQL语句。
下面演示了JDBC调用方法:
public class DBTest {
public static void mian(String[] args) throws Exception {
Connection con = MySqlDAO.getConnection();
String sql = "call insertNewStudent('coco',?);";
CallableStatement stmt = con.prepareCall(sql);
stmt.registerOutParameter(1, Types.INTEGER);
stmt.execute();
System.out.println("new student no: " + stmt.getInt(1));
stmt.close();
con.close();
}
}
这段代码调用了上面创建的存储过程insertNewStudent。首先仍是获取连接,然后定义一条访问存储过程的语句,注意输出参数需要以“?”代替。接着创建一个可以执行存储过程的命令对象,并设置其对象的存储过程的输出类型,接着执行存储过程命令对象并将存储过程的输出参数打印在控制台上,最后关闭命令对象和连接。
将部分逻辑放到数据库服务器上进行运行,这种提前编译的SQL可以大大提高效率,但也给服务器带来了性能上的压力。除此之外,存储过程在当前的数据库产品中移植性交叉,因此,如果大量使用存储过程会给程序的移植带来一定的困难。
7.4.3 数据库连接池
数据库创建连接是一项非常耗费资源的操作,因此节省创建和释放联系的性能消耗,人们提出了池的概念。池的概念被广泛应用在服务器端软件的开发上。使用池结构可以明显提高应用程序的速度,改善效率和降低系统资源开销。所以在现在的应用服务器端的开发中,池的设计和实现是开发工作中的重要一环。
池可以想象成一个容器,保存着各种需要的对象。对这些对象进行复用,从而提高系统性能。从结构上,它应该具有容器对象和具体的元素对象。从使用方法来看,可以直接取得池中的元素来用,也可以把要做的任务交给它处理,所以从目的上看,池应该有两种类型:
- 用于处理客户提交的任务的,通常用Thread Pool(线程池)来描述它。
- 客户从池中获取有关的对象进行使用,通常用Resource Pool(资源池)来描述它。
它们可以分别解决不同的问题。数据库连接池就是一种资源池。为了更好的管理数据库的连接,人们设计了数据库连接池。
下面演示了一个简单的数据库连接池:
1. MyCon类
该类表示一个有状态的数据连接。
public class MyCon {
public static final int FREE = 100;
public static final int BUZY = 101;
public static final int CLOSED = 102;
private Connection con;
private int State = FREE;
public MyCon(Connection con) {
this.con = con;
}
public Connection getCon() {
return con;
}
public int getState() {
return state;
}
public void setState(int state) {
this.state = state;
}
}
2. ConPool类
该类为数据库连接池实例。
该类使用单例模式(设计模式之一)。
public class ConPool {
private List<MyCon> freeCons = new ArrayList<>();
private List<MyCon> buzyCons = new ArrayList<>();
private int max = 10;
private int min = 2;
private int current = 0;
private static ConPool instance;
private ConPool() {
while (this.min > this. current) {
this.freeCons.add(this.createCon());
}
}
public static ConPool getInstance() {
if (instance = null)
instance = new ConPool();
return instance;
}
public MyCon getCon() {
MyCon myCon = this.getFreeCon();
if (myCon != null) {
return myCon;
} else {
return this.getNewCon();
}
}
private MyCon getFreeCon() {
if(freeCons.size() > 0) {
MyCon con = freeCons.remove(0);
con.setState(MyCon.BUZY);
this.buzyCons.add(con);
return con;
} else {
return null;
}
}
private MyCon getNewCon() {
if (this.current < this.max) {
MyCon myCon = this.createCon();
myCon.setState(MyCon.BUZY);
this.buzyCons.add(myCon);
return myCon;
} else {
return null;
}
}
private MyCon createCon() {
try {
Connection con = MySqlDAO.getConnection();
MyCon myCon = new MyCon(con);
this.current++;
return myCon;
} catch (Exception e) {}
return null;
}
public void setFree(MyCon con) {
this.buzyCons.remove(con);
con.setState(MyCon.FREE);
this.freeCons.add(con);
}
public String toString() {
return "Current connections: " + this.current +
"Free connections: " + this.freeCons.size() +
"Buzy connections: " + this.buzyCons.size();
}
}
3. MySqlDAO类
该类用于数据库连接的获取与操作。
public class MySqlDAO {
public static Connection getConnetion() throws Exception {
String driverName = "com.mysql.cj.jdbc.Driver";
String url = "jdbc:mysql://localhost:3306/student";
String userName = "root";
String password = "123456";
Class.forName(driverName);
Connection con = DriverManager.getConnection(url, userName, password);
return con;
}
}
4. DBTest类
该类为测试类,输出池中连接情况。
public class DBTest {
public static void main(String[] args) throws Exception {
System.out.println(ConPool.getInstance().toString());
MyCon con = null;
for (int i = 1; i <= 5; i++) {
con = ConPool.getInstance().getCon();
}
System.out.println(ConPool.getInstance());
ConPool.getInstance().setFree(con);
System.out.println(ConPool.getInstance());
}
}
运行结果:
Current connections: 2 Free connections: 2 Buzy connections: 0
Current connections: 5 Free connections: 0 Buzy connections: 5
Current connections: 5 Free connections: 1 Buzy connections: 4