彻底理解JDBC

1注册JDBC驱动

注册JDBC驱动的三种方式(以mysql为例):

  1. Class.forName(“com.mysql.jdbc.Driver”);
  2. DriverManager.registerDriver(new Driver());
  3. System.setProperty(“jdbc.drivers”,“com.mysql.jdbc.Driver”);
    方法1中使用了类的反射,在使用 Class.forName(“com.mysql.jdbc.Driver”)时会向jvm中加载com.mysql.jdbc.Driver类,该类的类容如下:
public class Driver extends com.mysql.cj.jdbc.Driver {
    public Driver() throws SQLException {
    }

    static {
        System.err.println("Loading class `com.mysql.jdbc.Driver'. This is deprecated. The new driver class is `com.mysql.cj.jdbc.Driver'. The driver is automatically registered via the SPI and manual loading of the driver class is generally unnecessary.");
    }
}

在加载类时会执行静态代码块中的类容,所以我们会在命令行中看到

Loading class `com.mysql.jdbc.Driver'. This is deprecated. The new driver class is `com.mysql.cj.jdbc.Driver'. The driver is automatically registered via the SPI and manual loading of the driver class is generally unnecessary.

com.mysql.jdbc.Driver继承了com.mysql.cj.jdbc.Drivercom.mysql.cj.jdbc.Driver类容如下

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.Drive时会调用DriverManager.registerDriver(new Driver()),这正是方法2的实现方式。但二者的效果并不相同,方法1只是在类加载的时候调用了这段代码,因此只创建了一个Driver()对象。然而,方法2显式的执行了DriverManager.registerDriver(new Driver()),这时会注册一个Driver对象,在执行new Driver()会先加载Driver类,然后会执行静态代码块中类容也就是DriverManager.registerDriver(new Driver()),又注册了一个Driver对象。方法3实现比较复杂,但通过断点调试可以发现其作用与方法1类似。

2创建数据库连接

数据库连接可以通过以下方式创建

Connection conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/imooc?useSSL=false&useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai","root","root");

上面注册了JDBC驱动,下面获取JDBC连接时似乎没有使用上面注册时的信息,那注册JDBC驱动又有什么意义呢?
事实上,JDBC只是一套规范,需要各个数据库厂商(如MySQL,Oracle等)自己去实现。而DriverManager属于import java.sql不依赖于任何数据库厂商,理论上不可能获得具体数据库的连接。那这只有一个可能,DriverManager维护了一个Driver对象,在注册Driver对象时,将具体厂商实现的对象赋值给了DriverManager维护的Driver对象,在调用DriverManager.getConnection时其实调用了Driver对象的getConnection方法。通过分析源代码,我们的猜想得到了验证。
在DriverManager对象中维护了一个存储DriverInfo对象的list。

public class DriverManager {
    // List of registered JDBC drivers
    private final static CopyOnWriteArrayList<DriverInfo> registeredDrivers = new CopyOnWriteArrayList<>();

DriverInfo对象的维护了一个Driver 对象。

class DriverInfo {
    final Driver driver;
    DriverAction da;
    DriverInfo(Driver driver, DriverAction action) {
        this.driver = driver;
        da = action;
    }

再来看一下DriverManager.registerDriver(new Driver())方法

    public static synchronized void registerDriver(java.sql.Driver driver)
        throws SQLException {

        registerDriver(driver, null);
    }

他调用了registerDriver方法,registerDriver实现如下

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);
    }

我们发现他其实就是向registeredDrivers列表中添加了一个DriverInfo对象。
再看一下DriverManager.getConnection方法,

    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()));
    }

他调用了另一个重载的getConnection方法,具体实现中最重要的一段代码如下

for(DriverInfo aDriver : registeredDrivers) {
            // If the caller does not have permission to load the driver then
            // skip it.
            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());
            }
        }

这段代码将registeredDrivers队列中的最后一个DriverInfo中的Driver对象的connect(url, info)返回值作为DriverManager.getConnection方法的返回值。
这时候我们就不难理解DriverManager.getConnection为什么是这串类容了"jdbc:mysql://localhost:3306/imooc?useSSL=false&useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai","root","root"。每个厂商的URL命名规则都有可能不同,不论字符串的内容是什么,只要遵守对应的命名规则即可,因为最终还要交由对应的JDBC实现来解析。MySQL的JDBC链接字符串格式是jdbc:mysql://主机ip:端口/数据库名?参数1=值1&参数2=值2

3执行crud

  1. 使用Statement对象
Statement stmt = conn.createStatement();

(1)执行查询

ResultSet set = stmt.executeQuery(String sql);

遍历结果

while (resultSet.next()){
	Integer eno = resultSet.getInt("eno");	//获取整型
	String ename = resultSet.getString("ename");	//获取String类型
	Date hiredate = resultSet.getDate("hiredate");	//获取Date类型
}

(2)执行插入操作

Statement stmt = conn.createStatement();
int n = stmt.executeUpdate("insert into employee(eno, ename, salary, dname) values(1011,'员工1011',5000,'市场部') ");

(3)执行更新操作

Statement stmt = conn.createStatement();
int n = stmt.executeUpdate("update employee set dname = '市场营销' where ename = '张三'");

(4)删除操作

Statement stmt = conn.createStatement();
int n = stmt.executeUpdate("delete from employee where ename = '张三'");
  1. 使用PreparedStatement 防止SQL注入攻击
    (1)执行查询
String sql = "select * from employee where dname = ?";
PreparedStatement pstmt = conn.prepareStatement(sql);
pstmt.setString(1,"市场部");
ResultSet resultSet = pstmt.executeQuery();

(2)执行插入操作

String sql = "insert into employee(eno, ename, salary, dname) values(?, ?, ?, ?) ";
PreparedStatement pstmt = conn.prepareStatement(sql);
pstmt.setInt(1,1012);
pstmt.setString(2, "员工1012");
pstmt.setInt(3, 5000);
pstmt.setString(4,"营销部");
pstmt.executeUpdate();

(3)执行更新操作

String sql = "update employee set dname = '市场营销' where ename = ?";
PreparedStatement pstmt = conn.prepareStatement(sql);
pstmt.setString(1, "王五");
int n = pstmt.executeUpdate();

(4)删除操作

String sql = "delete from employee where ename = ?";
PreparedStatement pstmt = conn.prepareStatement(sql);
pstmt.setString(1,"王五");
int n = pstmt.executeUpdate();
  1. 事务
    JDBC中默认是开启事务的,也就是执行一条语句就提交一次事务,使用conn.setAutoCommit(false)关闭事务自动开启,然后手动控制事务的提交时机。
conn.setAutoCommit(false);//关闭事务自动提交
conn.commit();//提交事务
conn.rollback();//事务回滚

在使用conn.commit()提交事务前,事务是写在事务区中,并没有更新数据库,如果事务执行失败conn.commit()不会执行到,事务也就无法被提交。必须要注意的是,事务提交失败事务区中的类容并没有被删除,再次提交仍然能够修改数据库,因此需要使用conn.rollback();对事务进行回滚,删除事务区中的内容。
4.数据库中的时间对象
MySQL中的时间类型有Date和DateTime等,Date精确到天,DateTime精确到秒。可以使用"YYYY-MM-DD"或者"YYYY-MM-DD HH:MM:SS"进行对应的时间输入,也可以使用YYYYMMDD或者YYYYMMDDHHMMSS(注意是一个整数而不是字符串),还可以使用now()获取当前时间。在JDBC中可以,可以使用String字符串或者数字或者sql.Date()对象设置时间,获取时间时只能使用得到String或者sql.Date()对象。
使用Statement

Statement stmt = conn.createStatement();
int n = stmt.executeUpdate("update employee set hiredate = '2020-09-07 16:08:30' where eno = 1012");
//int n = stmt.executeUpdate("update employee set hiredate = 20200907160830 where eno = 1012");
ResultSet resultSet = stmt.executeQuery("select * from employee where eno = 1012");
resultSet.next();
System.out.println(resultSet.getInt("hiredate"));
//System.out.println(resultSet.getString("hiredate"));

使用PreparedStatement

String sql = "update employee set hiredate = ? where eno = 1012";
PreparedStatement pstmt = conn.prepareStatement(sql);
//pstmt.setString(1,"2020-07-09");
//pstmt.setInt(1,20200708);
Date date = new Date();
long time = date.getTime();
pstmt.setDate(1, new java.sql.Date(time));
pstmt.executeUpdate();

需要注意的是,resultSet.getInt(“hiredate”)返回的是java.sql.Date,pstmt.setDate()也需要传入java.sql.Date
2. 批处理

String sql = "insert into employee(eno, ename, salary, dname) values(?, ?, ?, ?)";
PreparedStatement pstmt = conn.prepareStatement(sql);
pstmt = conn.prepareStatement(sql);
for(int i = 200; i < 300; i++){
	pstmt.setInt(1, i);
	pstmt.setString(2, "员工" + i);
	pstmt.setInt(3, 5000);
	pstmt.setString(4, "员工" + i);
	pstmt.addBatch();
}
pstmt.executeBatch();
  1. 连接池
    web应用的服务器端与数据库有可能不放在一台计算机上,访问数据库时需要使用网络连接(一般使用TCP/IP),频繁的创建连接需要花费大量的时间。连接池将需要的连接先创建好,需要访问数据库时再去连接池中取。关闭连接时并没有将这个连接释放掉,而是将链接资源放回了连接池中。
    (1)c3p0连接池
    先在资源文件夹下创建c3p0-config.xml配置文件,
<?xml version="1.0" encoding="UTF-8"?>
<c3p0-config>
    <!-- 这是默认配置信息 -->
    <default-config>
        <!-- 连接四大参数配置 -->
        <property name="jdbcUrl">jdbc:mysql://localhost:3306/imooc?useSSL=false&amp;useUnicode=true&amp;characterEncoding=utf-8&amp;serverTimezone=Asia/Shanghai</property>
        <property name="driverClass">com.mysql.cj.jdbc.Driver</property>
        <property name="user">root</property>
        <property name="password">root</property>
        <!-- 池参数配置 -->
        <property name="acquireIncrement">3</property>
        <property name="initialPoolSize">10</property>
        <property name="minPoolSize">2</property>
        <property name="maxPoolSize">10</property>
    </default-config>
</c3p0-config>

获取数据库连接

DataSource dataSource = new ComboPooledDataSource();
Connection conn = dataSource.getConnection();

(2)druid连接池
在资源文件夹下创建druid-config.properties文件

driverClassName=com.mysql.cj.jdbc.Driver
url=jdbc:mysql://localhost:3306/imooc?useSSL=false&useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai
username=root
password=root
initialSize=10
maxActive=20

获取数据库连接

Properties properties = new Properties();
String propertyFile = StandardJDBCExample.class.getResource("/druid-config.properties").getPath();
propertyFile = URLDecoder.decode(propertyFile, "UTF-8");
properties.load(new FileInputStream(propertyFile));
DataSource dataSource = DruidDataSourceFactory.createDataSource(properties);
  1. Commons Dbutils(Appache开发的一组工具包,可以更方便的执行crud)
    (1)查询操作
DataSource dataSource = new ComboPooledDataSource();
QueryRunner qr = new QueryRunner(dataSource);
try {
	List<Employee> list = qr.query("select * from employee where salary > ?",
	new BeanListHandler<Employee>(Employee.class),
	new Object[]{4000});
	for(Employee employee : list)
		System.out.println(employee.getDname());
	} catch (SQLException e) {
	e.printStackTrace();
}

(2)更新操作

DataSource dataSource = new ComboPooledDataSource();
Connection conn = null;
try {
	conn = dataSource.getConnection();
	conn.setAutoCommit(false);
	QueryRunner qr = new QueryRunner(dataSource);
	qr.update("update employee set salary = 6000 where salary = ?", new Object[]{3500});
	conn.commit();
} catch (SQLException e) {
	try {
		conn.rollback();
	} catch (SQLException e1) {
		e1.printStackTrace();
	}
	e.printStackTrace();
}finally {
	try {
		conn.close();
	} catch (SQLException e) {
		e.printStackTrace();
	}
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值