什么是JDBC
- java数据库连接(Java DataBase Connectivity)
- JDBC可以让java通过程序操作关系型数据库
- JDBC基于驱动程序实现与数据库的连接与操作
什么是驱动程序?
驱动程序是数据库厂商按照java所提供的JDBC接口实现的操作数据库的程序。每个厂商内部实现不一,但是接口总是一致,因此我们可以使用JDBC搭配不同的驱动来操作厂商的数据库产品。
JDBC的优点
- 统一的API,提供一致的开发过程
- 易于学习,容易上手,代码结构稳定
- 功能强大,执行效率高,可以处理海量数据
JDBC开发流程
JDBC开发流程主要为以下五个步骤
- 加载并注册JDBC驱动
- 创建数据库连接
- 创建Statement对象并执行sql语句
- (遍历)查询SQL执行结果
- 关闭连接,释放资源
接下来将详细介绍这五个步骤
加载并注册JDBC驱动
这里使用的是mysql数据库,因此加载的是mysql数据库对应的驱动程序
Class.forName("com.mysql.cj.jdbc.Driver");
Class.forName方法会加载参数指定的类,并返回参数指定的类或接口关联的Class对象,这里我们只需要这个类被加载,所以不需要获取返回值。
这里贴一下forName的详细实现,注意一下这个Reflection.getCallerClass()方法,等会在创建数据库连接的时候会有用到,这里我们可以知道className所指定的类是由caller的classloader加载的。
@CallerSensitive
public static Class<?> forName(String className)
throws ClassNotFoundException {
Class<?> caller = Reflection.getCallerClass();
return forName0(className, true, ClassLoader.getClassLoader(caller), caller);
}
类被加载过程中,会执行static参数以及static域的初始化,这里进入com.mysql.cj.jdbc.Driver方法,会看到Driver中有个static域,这个域会在Class.forName执行时被初始化
package com.mysql.cj.jdbc;
import java.sql.DriverManager;
import java.sql.SQLException;
public class Driver extends NonRegisteringDriver implements java.sql.Driver {
public Driver() throws SQLException {
}
static {
try {
DriverManager.registerDriver(new Driver());
} catch (SQLException var1) {
throw new RuntimeException("Can't register driver!");
}
}
}
代码中可以看到,com.mysql.cj.jdbc.Driver实现了java.sql.Driver接口,并且继承了NonRegisteringDriver类,这个NonRegisteringDriver类其实就是mysql驱动具体的针对java.sql.Driver的具体实现类。里面主要方法是connect方法,可以java创建mysql数据库连接。
另外,代码中主要执行的方法为DriverManager.registerDriver(),这里会将数据库驱动程序注册到JDBC的DriverManager,后续使用将会通过DriverManager调度。registerDriver具体实现如下,registeredDrivers这个变量是一个CopyOnWriteArrayList类型,里面存储了所有注册到DriverManager的驱动程序信息。
public static synchronized void registerDriver(java.sql.Driver driver,
DriverAction da)
throws SQLException {
/* Register the driver if it has not already been added to our list */
if(driver != null) {
registeredDrivers.addIfAbsent(new DriverInfo(driver, da));
} else {
// This is for compatibility with the original DriverManager
throw new NullPointerException();
}
println("registerDriver: " + driver);
}
创建数据库连接
getConnection方法需要三个参数,分别是数据库url,以及用户名和密码。具体信息可以查看相关官网
Connection conn = DriverManager.getConnection(DBurl, DBname, DBpwd);
这里列举常见的几种数据库url连接格式
RDBMS | URL格式 | 默认端口 | 举例 |
---|---|---|---|
oracle | jdbc:oracle:thin:@dbip:port:databasename | 1521 | jdbc:oracle:thin:@10.10.10.10:1521:dataBase |
mysql | jdbc:mysql://dbip:port/databasename | 3306 | jdbc:mysql://10.10.10.10:3306/dataBase |
SQLServer | jdbc:microsoft:sqlserver://dbip:port;DatabaseName=databasename | 1433 | jdbc:sqlserver://10.10.10.10:1433;DatabaseName=dataBase |
getConnection方法内部可见详情如下,没有什么激动人心的高级操作,只是继续调用了一个同名的方法。不过这里有一个熟悉的操作,那就是同名方法的一个参数值是Reflection.getCallerClass()。我们刚刚在使用Class.forName()方法加载驱动类的时候有见到过,这里会保证我们获取到的驱动程序是和现在执行的程序是在同一个classloader下,保证程序正确运行。
@CallerSensitive
public static Connection getConnection(String url,
String user, String password) throws SQLException {
java.util.Properties info = new java.util.Properties();
if (user != null) {
info.put("user", user);
}
if (password != null) {
info.put("password", password);
}
return (getConnection(url, info, Reflection.getCallerClass()));
}
同名方法中我删掉了部分抛出异常的操作,不过不影响阅读。循环中会遍历所有已注册的驱动信息,并通过callerCL或者引导类加载器(若callerCL为null)来定位和加载驱动程序,并且在获取驱动类成功的情况下,调用driver.connect方法创建数据库连接。
private static Connection getConnection(
String url, java.util.Properties info, Class<?> caller) throws SQLException {
/*
* When callerCl is null, we should check the application's
* (which is invoking this class indirectly)
* classloader, so that the JDBC driver class outside rt.jar
* can be loaded from here.
*/
ClassLoader callerCL = caller != null ? caller.getClassLoader() : null;
synchronized(DriverManager.class) {
// synchronize loading of the correct classloader.
if (callerCL == null) {
callerCL = Thread.currentThread().getContextClassLoader();
}
}
if(url == null) {
throw new SQLException("The url cannot be null", "08001");
}
SQLException reason = null;
for(DriverInfo aDriver : registeredDrivers) {
if(isDriverAllowed(aDriver.driver, callerCL)) {
try {
println(" trying " + aDriver.driver.getClass().getName());
Connection con = aDriver.driver.connect(url, info);
if (con != null) {
// Success!
println("getConnection returning " + aDriver.driver.getClass().getName());
return (con);
}
} catch (SQLException ex) {
if (reason == null) {
reason = ex;
}
}
} else {
println(" skipping: " + aDriver.getClass().getName());
}
}
}
private static boolean isDriverAllowed(Driver driver, ClassLoader classLoader) {
boolean result = false;
if(driver != null) {
Class<?> aClass = null;
try {
aClass = Class.forName(driver.getClass().getName(), true, classLoader);
} catch (Exception ex) {
result = false;
}
result = ( aClass == driver.getClass() ) ? true : false;
}
return result;
}
创建Statement对象并执行sql语句
常用的Statement对象有两种,Statement和PreparedStatement
Statement
Statement创建和执行sql都由驱动程序完成,没啥可说的(当然我也没细看XD)
Statement stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery("select * from employee");
PreparedStatement
相比之Statement,PreparedStatement因为会对sql语句预编译,可以有效防止SQL注入攻击,并且执行效率比之Statement要高一些
String sql = "select * from employee where dname =? and eno > ?";
PreparedStatement pstmt = conn.prepareStatement(sql);
pstmt.setString(1, qdname);
pstmt.setInt(2, 1002);
ResultSet rs = pstmt.executeQuery();
除了单条执行sql,PreparedStatement也支持批处理操作,会比循环单条执行快一些
String sql = "insert into employee(eno,ename,salary,dname) values(?,?,?,?)";
PreparedStatement pstmt = conn.prepareStatement(sql);
for (int i = 200000; i < 300000; i++) {
pstmt.setInt(1, i);
pstmt.setString(2, "员工" + i);
pstmt.setFloat(3, 5000f);
pstmt.setString(4, "市场部");
// pstmt.executeUpdate();
pstmt.addBatch();
}
pstmt.executeBatch();
(遍历)查询SQL执行结果
遍历查询结果如下,当然有些update操作会只返回一个int,具体情况具体分析,这里主要展示ResultSet的遍历,rs.getXXX()方法中的数字参数表示字段在数据库表中的位置,从1开始,每个数字都表示数据库表中的第几列元素
while (rs.next()) {
Integer eno = rs.getInt(1);
String ename = rs.getString(2);
Float sal = rs.getFloat(3);
String dname = rs.getString(4);
Date hiredate = rs.getDate(5);
System.out.println(dname + "-" + eno + "-" + ename + "-" + sal + "-" + hiredate);
}
不过不怎么推荐上面的获取数据方式,一般来讲,可以直接使用数据库表的列名作为参数获取数据行对应值
while (rs.next()) {
Integer eno = rs.getInt("eno");
String ename = rs.getString("ename");
Float salary = rs.getFloat("salary");
String dname = rs.getString("dname");
Date hiredate = rs.getDate("hiredate");
Employee employee = new Employee(eno, ename, salary, dname, hiredate);
list.add(employee);
}
关闭连接,释放资源
使用完数据库,记得关闭数据库连接,释放相关资源
try {
if (conn != null && !conn.isClosed()) {
conn.close();
}
} catch (Exception e) {
e.printStackTrace();
}
JDBC的事务
JDBC为了满足对数据库的事务操作,也是有它自己的事务逻辑支持,JDBC支持两种事务模式
- 自动提交事务模式
- 手动提交事务模式
自动提交事务模式
- 自动提交事务模式会在每次执行sql写操作后,自动提交事务
- 自动提交事务模式开启方法:conn.setAutoCommit(true),该方法一般默认开启
- 自动移交事务是JDBC的默认行为,一般无法保证多数据的一致性
手动提交事务模式
- 顾名思义,手动提交模式是指程序员自己使用程序管理事务,常用方法commit()和rollback()
- 手动开启事务方法:conn.setAutoCommit(false)
- 手动提交事务可以保证多数据一致性,不过必须手动提交/回滚
方法示例
DBUtils封装了获取/关闭数据库连接的程序片断
public static void tc2() {
Connection conn = null;
PreparedStatement pstmt = null;
try {
long startTime = System.currentTimeMillis();
conn = DbUtils.getConnection();
conn.setAutoCommit(false);
String sql = "insert into employee(eno,ename,salary,dname) values(?,?,?,?)";
pstmt = conn.prepareStatement(sql);
for (int i = 200000; i < 300000; i++) {
pstmt.setInt(1, i);
pstmt.setString(2, "员工" + i);
pstmt.setFloat(3, 5000f);
pstmt.setString(4, "市场部");
// pstmt.executeUpdate();
pstmt.addBatch();
}
pstmt.executeBatch();
conn.commit(); //成功提交
long endTime = System.currentTimeMillis();
System.out.println("tc2 cost " +(endTime - startTime) + " ms");
} catch (Exception e) {
try {
if (conn != null && !conn.isClosed()) {
conn.rollback();//失败回滚
}
} catch (SQLException ex) {
e.printStackTrace();
}
e.printStackTrace();
} finally {
DbUtils.closeConnection(conn);
}
}