Java JDBC

Java JDBC

JDBC是访问关系型数据库的Java API。JDBC给程序员提供访问和操纵众多关系型数据库的一个统一接口。使用JDBC API,程序员可以用Java以一种用户友好的接口执行SQL语句、获取结果以及显示数据,并且操纵数据。JDBC API还可用于分布式、异构环境中的多种数据源之间实现交互。

JDBC API是一组规范(Java接口和类的集合),用于编写和访问操作关系数据库的Java程序。JDBC驱动程序起一个接口的作用,它使JDBC与具体数据库之间的通信灵活方便(实现了API的规范),由具体的数据库供应商提供。下图显示了Java程序、JDBC API、JDBC驱动程序和关系数据库之间的关系。
image

使用JDBC开发数据库应用程序

准备MySQL

CREATE DATABASE IF NOT EXISTS cuit_scss;
USE cuit_scss;

CREATE TABLE course (
  id INT PRIMARY KEY auto_increment COMMENT '课程标识id',
  title VARCHAR (20) NOT NULL COMMENT '课程名称',
  credits TINYINT UNSIGNED COMMENT '学分',
  intro VARCHAR (200) COMMENT '课程简介'
) COMMENT = '课程信息表';

INSERT INTO course (title, credits, intro)
VALUES
  ('Linux IN Action', 4, '生产环境操作系统Linux实战'),
  ('MySQL 从入门到精通', 6, '关系型数据库MySQL的使用'),
  ('线性代数与空间解析几何', 6, '院长亲自授课,需要抢座');

CREATE TABLE student (
  ssn CHAR (11) PRIMARY KEY COMMENT '学号',
  sname VARCHAR (20) NOT NULL COMMENT '姓名',
  gender ENUM ('M', 'F') COMMENT '学生性别:男(M)或女(F)',
  phone VARCHAR (15) COMMENT '联系电话',
  birth_date DATE COMMENT '学生出生日期',
  address VARCHAR (50) COMMENT '联系地址'
) COMMENT = '学生信息表';

INSERT INTO student (ssn, sname, gender, phone, birth_date, address)
VALUES
  ('20250901001', 'Peppa', 'F', '95533', '2006-10-20', 'xx省xx市xx街道'),
  ('20250901002', 'Emily', 'F', '95588', '2006-5-7', 'yy省yy市yy街道'),
  ('20250901003', 'Pedro', 'M', '95599', '2004-6-2', 'zz省zz市zz街道');

CREATE TABLE score (
  id INT PRIMARY KEY auto_increment COMMENT '成绩标识id',
  ssn CHAR (11) COMMENT '学号',
  cid INT COMMENT '课程编号',
  sc_datte DATE COMMENT '选课日期',
  score FLOAT (5, 2) COMMENT '成绩',
  CONSTRAINT ForeignKey_student_score_ssn FOREIGN KEY (ssn) REFERENCES student (ssn) ON UPDATE CASCADE ON DELETE CASCADE,
  CONSTRAINT ForeignKey_course_score_cid FOREIGN KEY (cid) REFERENCES course (id) ON UPDATE CASCADE ON DELETE CASCADE,
  CONSTRAINT Unique_key_ssn_cid UNIQUE (ssn, cid)
) COMMENT = '成绩信息表';

-- 添加检查约束
ALTER TABLE score ADD CONSTRAINT CK_score_score CHECK (score >= 0 && score <= 100);

核心接口

使用Java开发任何数据库应用程序都需要4个主要接口:Driver、Connection、Statement和ResultSet。这些接口定义了使用SQL访问数据库的一般架构,由JDBC API定义,JDBC驱动程序开发商提供实现。

  • Driver接口:用于加载合适的驱动程序。
  • Connection接口:用于连接到数据库。
  • Statement接口:用于创建和执行SQL语句。
  • ResultSet接口:用于处理SQL语句返回的结果。

下图展示了各接口之间的关系:
image

开发JDBC程序的典型步骤

1.加载驱动程序
在连接到数据库之前,需要加载合适的驱动程序。

Class.forName("com.mysql.cj.jdbc.Driver");

驱动程序是实现java.sql.Driver接口的具体类。Java 6支持驱动程序自动加载,但并非所有驱动程序都具备该特性,为安全起见,建议显式加载。

2.建立连接
使用DriverManager类的静态方法getConnection(url, username, password)连接数据库。

  • url:数据库在Internet上的唯一标识符。
  • username:数据库用户名。
  • password:数据库密码。

不同数据库的url模式如下:

数据库 url模式
Mysql jdbc:mysql://hostname:port/dbname
Oracle jdbc:oracle:thin:@hostname:port:oracleDBSID

3.创建语句
Connection对象如同程序与数据库的连接缆道,Statement对象则像缆车,用于传输SQL语句并返回结果。

Statement stat = conn.createStatement();

4.执行语句

  • execute(sql):可执行任意类型SQL语句,复杂且极少使用。
  • executeUpdate(sql):执行DML语句(insert、update、delete),返回影响行数。
  • executeQuery(sql):执行DQL语句(select),返回查询结果ResultSet

5.处理结果ResultSet
ResultSet维护SQL查询结果,当前行初始位置为null。可通过next方法移动到下一行,通过getter方法获取当前行数据。

6.回收数据库资源
关闭连接并释放相关资源,可使用try-with-resource语法,但不建议。

项目依赖配置(Gradle)

build.gradle中添加如下内容:

dependencies {
    // mysql连接驱动的依赖
    implementation 'mysql:mysql-connector-java:8.0.33'
}

image

示例代码

package com.jdbc;

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;

/**
 * @author Jing61
 */
public class JDBCTest {
    public static void main(String[] args) {
        Connection connection = null;
        try {
            // 1. 加载驱动(对jdbc规范的实现,由具体的数据库厂商提供)
            Class.forName("com.mysql.cj.jdbc.Driver");
            // 2. 获取连接
            String user = "root";
            String password = "root";
            String url = "jdbc:mysql://localhost:3306/cuit_scss?serverTimezone=Asia/Shanghai&useSSL=false&allowPublicKeyRetrieval=true&useUnicode=true&characterEncoding=utf-8";
            connection = DriverManager.getConnection(url, user, password);
            // 3. 创建语句
            var sql = "INSERT INTO course(title, credits, intro) VALUES ('Spring', 10, 'Spring企业级一站式商业解决方案')";
            var statement = connection.createStatement();
            // 4. 执行语句
            int count = statement.executeUpdate(sql);
            // 5. 处理结果
            if (count > 0) {
                System.out.println("添加成功");
            }
        } catch (ClassNotFoundException | SQLException e) {
            e.printStackTrace();
        } finally {
            // 6. 释放资源
            try {
                connection.close();
            } catch (SQLException e) {
                e.printStackTrace();
            }
        }
    }
}

SQL注入问题

问题演示

创建用户表并插入测试数据:

CREATE TABLE `user`(
 id INT PRIMARY KEY AUTO_INCREMENT,
 username VARCHAR(12) NOT NULL UNIQUE,
 `password` VARCHAR(36) NOT NULL
);

INSERT INTO `user` VALUES(DEFAULT,'peppa','peppa'),
(DEFAULT,'suzy','suzy'),(DEFAULT,'emily','emily');

存在SQL注入漏洞的代码:

package com.jdbc;

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.SQLException;

/**
 * @author Jing61
 */
public class SQLInject {
    public static void main(String[] args) throws RuntimeException {
        findUserByUsernameAndPassword("peppa", "123");
        findUserByUsernameAndPassword("peppa", "1' or true or '1");
    }

    public static void findUserByUsernameAndPassword(String username, String password) {
        Connection connection = null;
        try {
            //1. 加载驱动
            Class.forName("com.mysql.cj.jdbc.Driver");
            // 2. 获取连接
            String url = "jdbc:mysql://localhost:3306/cuit_scss?serverTimezone=Asia/Shanghai&useSSL=false&allowPublicKeyRetrieval=true&useUnicode=true&characterEncoding=utf-8";
            connection = DriverManager.getConnection(url, "root", "root");
            // 3. 创建语句
            var sql = "SELECT id, username, password FROM user where username='" + username + "' and password='" + password + "'";
            var statement = connection.createStatement();
            // 4. 执行语句 (DDL ==> ResultSet)
            ResultSet rs = statement.executeQuery(sql);
            // 5. 处理结果
            while(rs.next()) {
                System.out.println(rs.getString("id") + " " + rs.getString("username") + " " + rs.getString("password"));
            }
            rs.close();
        } catch (ClassNotFoundException | SQLException e) {
            e.printStackTrace();
        } finally {
            // 6. 释放资源
            try {
                connection.close();
            } catch (SQLException e) {
                e.printStackTrace();
            }
        }
    }
}

当密码输入1' or true or '1时,查询字符串变为SELECT id, username, password FROM user where username='peppa' and password='1' or true or '1',会返回所有用户数据,存在安全漏洞。

解决方案:使用PreparedStatement

PreparedStatement接口继承自Statement,用于执行预编译的SQL语句,可避免SQL注入。

优化后的代码:

package com.jdbc;

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.SQLException;

/**
 * @author Jing61
 */
public class SQLInject {
    public static void main(String[] args) throws RuntimeException {
        findUserByUsernameAndPassword("peppa", "peppa");
        findUserByUsernameAndPassword("peppa", "1' or true or '1");
    }

    public static void findUserByUsernameAndPassword(String username, String password) {
        Connection connection = null;
        try {
            //1. 加载驱动
            Class.forName("com.mysql.cj.jdbc.Driver");
            // 2. 获取连接
            String url = "jdbc:mysql://localhost:3306/cuit_scss?serverTimezone=Asia/Shanghai&useSSL=false&allowPublicKeyRetrieval=true&useUnicode=true&characterEncoding=utf-8";
            connection = DriverManager.getConnection(url, "root", "root");
            // 3. 创建语句
            var sql = "SELECT id, username, password FROM user where username=? and password=?";
            var ptst = connection.prepareStatement(sql);
            // 4. 执行语句 (DDL ==> ResultSet)
            ptst.setString(1, username);
            ptst.setString(2, password);
            ResultSet rs = ptst.executeQuery();
            // 5. 处理结果
            while(rs.next()) {
                System.out.println(rs.getString("id") + " " + rs.getString("username") + " " + rs.getString("password"));
            }
            rs.close();
        } catch (ClassNotFoundException | SQLException e) {
            e.printStackTrace();
        } finally {
            // 6. 释放资源
            try {
                connection.close();
            } catch (SQLException e) {
                e.printStackTrace();
            }
        }
    }
}

PreparedStatement的优势:

  1. 预编译SQL语句,性能更好。
  2. 无需拼接SQL语句,编程更简单。
  3. 可防止SQL注入,安全性更好。

规范和封装JDBC代码

目前存在的问题

  1. 驱动重复注册,应在系统启动时注册一次。
  2. 数据库连接属性硬编码,耦合度高。
  3. 获取连接、释放资源等公共操作未抽取,代码冗余。

解决方案:JDBC助手类

配置文件(jdbc.properties)

resources目录下创建jdbc.properties文件:
image

# mysql的连接驱动类名
driverClassName = com.mysql.cj.jdbc.Driver
# mysql的连接地址
url = jdbc:mysql://localhost:3306/cuit_scss?serverTimezone=Asia/Shanghai&useSSL=false&allowPublicKeyRetrieval=true&useUnicode=true&characterEncoding=utf-8
# mysql的连接用户名
username = root
# mysql的连接密码
password = root

这样的好处是,当需要改变连接条件时,无需改变源代码,修改配置文件即可。

JDBCHelper工具类(单例模式)

package com.jdbc;

import java.io.IOException;
import java.io.InputStream;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.util.Properties;

/**
 * JDBCHelper jdbc工具类
 * 解决:
 * 1.系统启动时注册驱动,只注册一次
 * 2.可配置数据库连接,完成解耦
 * 3.抽取出公共的方法 获取连接、释放资源等
 * 单例模式:该类在系统中只存在唯一的实例
 *      1.构造方法私有化,外部不允许创建对象
 *  饥汉模式:在类加载的时候创建对象,并保存在静态成员变量中
 *      2.获取单例对象
 *  懒汉模式:在需要对象的时候创建对象,保存在静态成员变量中
 * @author Jing61
 */
public class JDBCHelper {
    private static String driver = null;
    private static String url = null;
    private static String username = null;
    private static String password = null;
    // private static JDBCHelper instance = new JDBCHelper(); // 饥汉模式
    private static volatile JDBCHelper instance = null; // volatile保证修改对线程可见 懒汉模式

    private JDBCHelper() {}

    static {
        try {
            // 从资源文件中加载数据库相关连接属性
            Properties properties = new Properties();
            // 获取属性值,使用类加载器加载类路径下的jdbc.properties文件
            InputStream input = JDBCHelper.class.getClassLoader().getResourceAsStream("jdbc.properties");
            properties.load(input);
            driver = properties.getProperty("driverClassName");
            url = properties.getProperty("url");
            username = properties.getProperty("username");
            password = properties.getProperty("password");
            System.out.println("初始化信息:\ndriver:" + driver + "\nurl:" + url + "\nusername:" + username + "\npassword:" + password);
            // 1. 加载驱动
            Class.forName(driver);
        } catch (ClassNotFoundException | IOException e) {
            throw new RuntimeException(e);
        }
    }

    // 2. 获取连接
    public Connection getConnection() throws SQLException {
        return DriverManager.getConnection(url, username, password);
    }

    // 6. 释放资源
    public void free(Connection connection) {
        try {
            if (connection != null) connection.close();
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }

    // 饥汉模式
//    public static JDBCHelper getInstance() {
//        return instance;
//    }

    // 懒汉模式(双重检查锁)
    public static JDBCHelper getInstance() {
        if(instance == null) {
            synchronized (JDBCHelper.class) {
                // 双重检查,解决多线程并发访问时创建多个实例的问题
                if(instance == null) instance = new JDBCHelper();
            }
        }
        return instance;
    }
}

测试增删改查(CourseTest)

package com.jdbc;

import java.sql.Connection;
import java.sql.SQLException;

public class CourseTest {
    public static boolean insert(String title, int credits, String intro) {
        Connection conn = null;
        try {
            conn = JDBCHelper.getInstance().getConnection();
            var sql = "INSERT INTO course(title, credits, intro) VALUES (?, ?, ?)";
            var ptst = conn.prepareStatement(sql);
            ptst.setString(1, title);
            ptst.setInt(2, credits);
            ptst.setString(3, intro);
            return ptst.executeUpdate() == 1;
        } catch (SQLException e) {
            throw new RuntimeException(e);
        } finally {
            JDBCHelper.getInstance().free(conn);
        }
    }

   public static boolean update(int id, String title, int credits, String intro) {
        Connection conn = null;
        try {
            conn = JDBCHelper.getInstance().getConnection();
            var sql = "UPDATE course SET title = ?, credits = ?, intro = ? WHERE id = ?";
            var ptst = conn.prepareStatement(sql);
            ptst.setString(1, title);
            ptst.setInt(2, credits);
            ptst.setString(3, intro);
            ptst.setInt(4, id);
            return ptst.executeUpdate() >= 1;
        } catch (SQLException e) {
            throw new RuntimeException(e);
        } finally {
            JDBCHelper.getInstance().free(conn);
        }
   }

   public static boolean remove(int id) {
        Connection conn = null;
        try {
            conn = JDBCHelper.getInstance().getConnection();
            var sql = "DELETE FROM course WHERE id = ?";
            var ptst = conn.prepareStatement(sql);
            ptst.setInt(1, id);
            return ptst.executeUpdate() >= 1;
        } catch (SQLException e) {
            throw new RuntimeException(e);
        } finally {
            JDBCHelper.getInstance().free(conn);
        }
   }

   public static void findById(int id) {
        Connection conn = null;
        try {
            conn = JDBCHelper.getInstance().getConnection();
            var sql = "SELECT * FROM course WHERE id = ?";
            var ptst = conn.prepareStatement(sql);
            ptst.setInt(1, id);
            var rs = ptst.executeQuery();
            System.out.println("id\ttitle\tcredits\tintro");
            while(rs.next()) {
                System.out.println(rs.getInt("id") + "\t" + rs.getString("title") + "\t" + rs.getInt("credits") + "\t" + rs.getString("intro"));
            }
        } catch (SQLException e) {
            throw new RuntimeException(e);
        } finally {
            JDBCHelper.getInstance().free(conn);
        }
   }

   public static void findAll() {
        Connection conn = null;
        try {
            conn = JDBCHelper.getInstance().getConnection();
            var sql = "SELECT * FROM course";
            var ptst = conn.prepareStatement(sql);
            var rs = ptst.executeQuery();
            System.out.println("id\ttitle\tcredits\tintro");
            while(rs.next()) {
                System.out.println(rs.getInt("id") + "\t" + rs.getString("title") + "\t" + rs.getInt("credits") + "\t" + rs.getString("intro"));
            }
        } catch (SQLException e) {
            throw new RuntimeException(e);
        } finally {
            JDBCHelper.getInstance().free(conn);
        }
   }

    public static void main(String[] args) {
        // insert("MyBatis", 2, "MyBatis是一个持久性框架");
        update(5, "MyBatis", 3, "MyBatis是一个持久性框架");
        remove(8);
        findById(5);
        findAll();
    }
}

CallableStatement(执行存储过程)

创建存储过程

-- 创建存储过程:根据学生学号获取该学生的平均成绩
DELIMITER $$

CREATE PROCEDURE compute_average(IN p_ssn CHAR(11), OUT p_score FLOAT(5, 2))
BEGIN
  SELECT AVG(score) INTO p_score FROM score WHERE ssn = p_ssn;
END;
//

DELIMITER ;

调用存储过程的代码

package com.jdbc;

import java.sql.Connection;
import java.sql.SQLException;
import java.sql.Types;

/**
 * @author Jing61
 */
public class JDBCCallableStatementTest {
    public static void main(String[] args) {
        Connection conn = null;
        try {
            conn = JDBCHelper.getInstance().getConnection();
            // 准备调用存储过程的SQL语句(转义语法)
            var st = conn.prepareCall("{call compute_average(?, ?)}");
            // 设置IN参数
            st.setString(1, "20250901001");
            // 注册OUT参数的类型
            st.registerOutParameter(2, Types.FLOAT);
            // 执行存储过程
            st.execute();
            // 获取OUT参数的值
            System.out.println(st.getFloat(2));

        } catch (SQLException e) {
            System.out.println(e);
        } finally {
            JDBCHelper.getInstance().free(conn);
        }
    }
}

CallableStatement继承自PreparedStatement,提供了注册OUT参数和获取OUT参数值的方法。

管理结果集

ResultSet的滚动和更新配置

ResultSet默认只能向后滚动且只读,若需支持向前滚动、指定行跳转或更新,需在创建PreparedStatement时指定参数:

参数类型 可选值 说明
resultSetType ResultSet.TYPE_FORWARD_ONLY 结果集不能滚动(默认值)
ResultSet.TYPE_SCROLL_INSENSITIVE 结果集可以滚动,对数据库变化不敏感
ResultSet.TYPE_SCROLL_SENSITIVE 结果集可以滚动,对数据库变化敏感
resultSetConcurrency ResultSet.CONCUR_READ_ONLY 只读,结果集不能更新(默认值)
ResultSet.CONCUR_UPDATABLE 结果集可用于更新数据库

示例代码

package com.jdbc;

import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.SQLException;

/**
 * @author Jing61
 */
public class ResultSetTest {
    public static void main(String[] args) {
        Connection conn = null;
        try {
            conn = JDBCHelper.getInstance().getConnection();
//            var ptst = conn.prepareStatement("SELECT id, title, credits, intro FROM course");
//            ResultSet rs = ptst.executeQuery();
//            while(rs.next()) { // 向后滚动访问查询结果
//                System.out.println(rs.getInt("id") + " " + rs.getString("title") + " " + rs.getInt("credits") + " " + rs.getString("intro"));
//            }
//            while (rs.previous()) {// 向前滚动访问查询结果,默认不允许
//                System.out.println(rs.getInt("id") + " " + rs.getString("title") + " " + rs.getInt("credits") + " " + rs.getString("intro"));
//            }
//            rs.absolute(3); // 移动到指定行,默认不允许
            /*
             * 若需要移动到指定行或者向前滚动或者更新,则需要设置允许
             * ResultSet
             *         prepareStatement(String sql, int resultSetType,int resultSetConcurrency)
             * resultSetType:
             *         ResultSet.TYPE_FORWARD_ONLY:结果集不能滚动(默认值)
             *         ResultSet.TYPE_SCROLL_INSENSITIVE:结果集可以滚动,但是对数据库变化不敏感,即不能变化更新
             *         ResultSet.TYPE_SCROLL_SENSITIVE:结果集可以滚动,对数据库变化敏感
             * resultSetConcurrency
             *         ResultSet.CONCUR_READ_ONLY:只读,结果集不能更新(默认值)
             *         ResultSet.CONCUR_UPDATABLE:结果集可以用于更新数据库
             */
            var ptst = conn.prepareStatement("SELECT id, title, credits, intro FROM course", ResultSet.TYPE_SCROLL_SENSITIVE,  ResultSet.CONCUR_UPDATABLE);
            ResultSet rs = ptst.executeQuery();
            while (rs.next()) { // 向后滚动访问查询结果
                System.out.println(rs.getInt("id") + " " + rs.getString("title") + " " + rs.getInt("credits") + " " + rs.getString("intro"));
            }
            while (rs.previous()) {// 向前滚动访问查询结果,现在允许
                System.out.println(rs.getInt("id") + " " + rs.getString("title") + " " + rs.getInt("credits") + " " + rs.getString("intro"));
            }
            rs.absolute(3); // 移动到指定行,现在运行
            System.out.println(rs.getInt("id") + " " + rs.getString("title") + " " + rs.getInt("credits") + " " + rs.getString("intro"));
            // 更新第四条记录的课程学分为5
            if(rs.absolute(4)) {
                rs.updateInt("credits", 5);
                rs.updateRow(); // 更新结果集
                System.out.println(rs.getInt("id") + " " + rs.getString("title") + " " + rs.getInt("credits") + " " + rs.getString("intro"));
            }

        } catch (SQLException e) {
            System.out.println(e);
        } finally {
            JDBCHelper.getInstance().free(conn);
        }
    }
}

处理大数据类型数据

利用Clob处理大文本数据,Blob处理长二进制数据(实际开发中,文件通常存储在文件服务系统,数据表仅存储访问路径,此处为演示)。

新增字段

ALTER TABLE student ADD COLUMN resume LONGTEXT COMMENT '大数据文本';
ALTER TABLE student ADD COLUMN avatar LONGBLOB COMMENT '二进制数据,头像';

示例代码(写入和读取大数据)

package com.jdbc;

import java.io.*;
import java.sql.Connection;
import java.sql.SQLException;

/**
 * @author Jing61
 */
public class WriteBigDate {
    public static void main(String[] args) {
        Connection conn = null;
        try {
            conn = JDBCHelper.getInstance().getConnection();
            // 待写入的文件
            File file1 = new File("E:/CUIT/Java/Demo/cuit/homework.txt"); // 大文本文件
            File file2 = new File("D:/qqDownload/d320fe824a20ae0a1af3df8e936d9fa9.png"); // 二进制文件(头像)
            
            // 写入大数据
            String sql = "UPDATE student SET resume=?, avatar=? WHERE ssn=?";
            var ptst = conn.prepareStatement(sql);
            // 设置大文本数据(Clob)(字符输入流)
            ptst.setClob(1, new FileReader(file1), file1.length());
            // 设置二进制数据(Blob)(字节输入流)
            ptst.setBlob(2, new FileInputStream(file2), file2.length());
            ptst.setString(3, "20250901001");
            ptst.executeUpdate();
            
            // 读取大数据
            String sql2 = "SELECT resume, avatar FROM student WHERE ssn=?";
            var ptst2 = conn.prepareStatement(sql2);
            ptst2.setString(1, "20250901001");
            var rs = ptst2.executeQuery();
            while(rs.next()) {
                System.out.println("20250901001的简历和头像");
                var resume = rs.getClob("resume");
                var avatar = rs.getBlob("avatar");
                // 写出大文本到文件
                resume.getCharacterStream().transferTo(new FileWriter("E:/CUIT/Java/Demo/cuit/test.txt"));
                // 写出二进制数据到文件
                avatar.getBinaryStream().transferTo(new FileOutputStream("E:/CUIT/Java/Demo/cuit/test.png"));
            }
        } catch (SQLException | IOException e) {
            throw new RuntimeException(e);
        }
    }
}

获取元数据

数据库元数据

用于获取数据库的基本信息(如URL、版本、驱动等)。

package com.jdbc;

import java.sql.SQLException;

/**
 * @author Jing61
 */
public class DatabaseMetaDemo {
    public static void main(String[] args) {
        try(var conn = JDBCHelper.getInstance().getConnection()){
            var dbMeta = conn.getMetaData();
            System.out.println("数据库连接url:" + dbMeta.getURL());
            System.out.println("数据库用户名:" + dbMeta.getDatabaseProductName());
            System.out.println("数据库主版本:" + dbMeta.getDatabaseMajorVersion());
            System.out.println("数据库次版本:" + dbMeta.getDatabaseMinorVersion());
            System.out.println("数据库驱动:" + dbMeta.getDriverName());
            System.out.println("数据库驱动版本:" + dbMeta.getDriverVersion());
            System.out.println("数据库供应商:" + dbMeta.getDatabaseProductName());
            System.out.println("=======================结果集数据库表=================");
            var rsTables = dbMeta.getTables(null, null, null, new String[] {"TABLE"});
            System.out.println("user tables:");
            while(rsTables.next())
                System.out.println(rsTables.getString("TABLE_NAME"));
        }catch (SQLException e) {
            e.printStackTrace();
        }
    }
}

结果集元数据

用于获取查询结果集的结构信息(如列名、数据类型等)。

package com.jdbc;

import java.sql.Connection;
import java.sql.SQLException;

/**
 * @author Jing61
 */
public class ResultSetMetaDataDemo {
    public static void main(String[] args) {
        Connection conn = null;
        try {
            conn = JDBCHelper.getInstance().getConnection();
            var sql = "SELECT id, title, credits, intro FROM course";
            var ptst = conn.prepareStatement(sql);
            var rs = ptst.executeQuery();
            var meta = rs.getMetaData(); // 获取结果集元数据
            for (int i = 1; i <= meta.getColumnCount(); i++) {
                /*
                 * getColumnLabel(int column) 获取列的别名
                 * getColumnTypeName(int column) 获取列的数据类型
                 * getColumnName(int column) 获取列名
                 */
                System.out.println(meta.getColumnLabel(i) + "\t" + meta.getColumnTypeName(i));
            }
            while(rs.next()) {
                for (int i = 1; i <= meta.getColumnCount(); i++) {
                    // 若不知道列的数据类型,则使用getObject方法
                    System.out.print(rs.getObject(i) + "\t");
                }
                System.out.println();
            }
        } catch (SQLException e) {
            System.out.println(e.getMessage());
        } finally {
            JDBCHelper.getInstance().free(conn);
        }
    }
}

事务处理

事务的ACID特性

  • 原子性(Atomicity):事务是一个整体的工作单元,事务对数据库所做的操作要么全部执行,要么全部取消。如果某条语句执行失败,则所有语句全部回滚。
  • 一致性(Consistency):事务在完成时,必须使所有的数据都保持一致状态。在相关数据库中,所有规则都必须应用于事务的修改,以保持所有数据的完整性。如果事务成功,则所有数据将变为一个新的状态;如果事务失败,则所有数据将处于开始之前的状态。
  • 隔离性(Isolation):事务与事务之间是相互隔离的。由事务所作的修改必须与其他事务所作的修改隔离。事务查看数据时数据所处的状态,要么是另一并发事务修改它之前的状态,要么是另一事务修改它之后的状态,事务不会查看中间状态的数据。
  • 持久性(Durability):事务提交后,对数据库数据的影响是持久性(永久的)。

JDBC事务控制

JDBC默认自动提交事务,每条SQL语句作为一个独立事务。可通过setAutoCommit(false)关闭自动提交,手动控制事务。

增强JDBCHelper(添加事务管理)

package com.jdbc;

import java.io.IOException;
import java.io.InputStream;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.util.Properties;

/**
 * JDBCHelper jdbc工具类
 * 解决:
 * 1.系统启动时注册驱动,只注册一次
 * 2.可配置数据库连接,完成解耦,
 * 3.抽取出公共的方法 获取连接、释放资源等
 * 单例模式:该类在系统中只存在唯一的实例
 * 1.构造方法私有化,外部不允许创建对象
 * 饥汉模式:在类加载的时候创建对象,并保存在静态成员变量中
 * 2.获取单例对象
 * 满汉模式:在需要对象的时候创建对象,保存在静态成员变量中
 *
 * @author Jing61
 */
public class JDBCHelper {
    private static String driver = null;
    private static String url = null;
    private static String username = null;
    private static String password = null;
    // private static JDBCHelper instance = new JDBCHelper(); // 饥汉模式
    private static volatile JDBCHelper instance = null; // volatile保证修改对线程可见 满汉模式
    private static ThreadLocal<Connection> threadLocal = new ThreadLocal<>();

    private JDBCHelper() {
    }

    static {
        try {
            // 从资源文件中加载数据库相关连接属性
            Properties properties = new Properties();
            // 获取属性值,使用类加载器位于类路径下的jdbc.properties文件
            InputStream input = JDBCHelper.class.getClassLoader().getResourceAsStream("jdbc.properties");
            properties.load(input);
            driver = properties.getProperty("driverClassName");
            url = properties.getProperty("url");
            username = properties.getProperty("username");
            password = properties.getProperty("password");
            System.out.println("初始化信息:\ndriver:" + driver + "\nurl:" + url + "\nusername:" + username + "\npassword:" + password);
            // 1. 加载驱动
            Class.forName(driver);
        } catch (ClassNotFoundException | IOException e) {
            throw new RuntimeException(e);
        }
    }

    // 2. 获取连接
    public Connection getConnection() throws SQLException {
        var connection = threadLocal.get();
        if (connection == null) {
            connection = DriverManager.getConnection(url, username, password);
            // 保存连接对象
            threadLocal.set(connection);
        }
        return connection;
    }

    // 6. 释放资源
    public void free(Connection connection) {
        try {
            if (connection != null) connection.close();
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }

    // 饥汉模式
//    public static JDBCHelper getInstance() {
//        return instance;
//    }

    // 满汉模式
    public static JDBCHelper getInstance() {
        if (instance == null) {
            synchronized (JDBCHelper.class) {
                //双重检查,解决多线程并发访问时,多个线程同时进入if判断,创建多个实例
                if (instance == null) instance = new JDBCHelper();
            }
        }
        return instance;
    }

    /**
     * 开启事务
     */
    public void transaction() {
        try {
            Connection connection = getConnection();
            connection.setAutoCommit(false);
        } catch (SQLException e) {
            throw new RuntimeException(e);
        }
    }

    /**
     * 提交事务
     */
    public void commit() {
        try {
            Connection connection = getConnection();
            connection.commit();
        } catch (SQLException e) {
            throw new RuntimeException(e);
        }
    }

    /**
     * 回滚事务
     */
    public void rollback() {
        try {
            Connection connection = getConnection();
            connection.rollback();
        } catch (SQLException e) {
            throw new RuntimeException(e);
        } 
    }
    
    /**
     * 用于事务控制中释放资源
     */
    public void free() {
        var connection = threadLocal.get();
        try {
            if (connection != null) {
                connection.close();
                // 移除连接对象
                threadLocal.remove();
            }
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }
}

事务测试代码

package com.jdbc;

import java.sql.Connection;
import java.sql.SQLException;
import java.sql.Savepoint;

/**
 * @author Jing61
 */
public class TransactionalDemo {
    public static void main(String[] args) {
        Connection conn = null;
        Savepoint sp = null;
        try {
            conn = JDBCHelper.getInstance().getConnection();
            // 开启事务就是默认的自动提交关闭
//            var sql = "UPDATE course SET credits = credits + 1 WHERE id = ?";
//            var ptst = conn.prepareStatement(sql);
//            ptst.setInt(1, 1);
//            ptst.executeUpdate(); // 成功
//            sql = "UPDATE course SET credits = 1 - credits WHERE id = ?"; // 规定了学分大于等于0
//            ptst = conn.prepareStatement(sql);
//            ptst.setInt(1, 1);
//            ptst.executeUpdate(); // fail(不是事务的情况下抛出异常,事务的情况下则回滚)
            // 不利用事务,上面的语句就会出现部分成功,部分不成功

            // 利用事务
            // 开启事务
            JDBCHelper.getInstance().transaction();
            var sql = "UPDATE course SET credits = credits + 1 WHERE id = ?";
            var ptst = conn.prepareStatement(sql);
            ptst.setInt(1, 1);
            ptst.executeUpdate();
            sp = conn.setSavepoint();
            sql = "UPDATE course SET credits = credits - 1 WHERE id = ?";
            ptst = conn.prepareStatement(sql);
            ptst.setInt(1, 1);
            ptst.executeUpdate();
        } catch (SQLException e) {
            JDBCHelper.getInstance().rollback(); // 回滚
            throw new RuntimeException(e);
        } finally {
            JDBCHelper.getInstance().commit(); // 提交
            JDBCHelper.getInstance().free();
        }
    }
}

批量更新

多条SQL语句作为一批操作同时提交,提升执行效率。

示例代码

package com.jdbc;

import java.sql.Connection;
import java.sql.SQLException;
import java.util.Random;

/**
 * @author Jing61
 */
public class BatchUpdatesDemo {
    public static void main(String[] args) {
        Connection conn = null;
        Random random = new Random();
        try {
            conn = JDBCHelper.getInstance().getConnection();
            var sql = "INSERT INTO course(title, credits, intro) VALUES (?, ?, ?)";
            var ptst = conn.prepareStatement(sql);
            
            // 批量添加5200条数据,每1000条提交一次
            for (int i = 1; i <= 5200; i++) {
                ptst.setString(1, "课程_" + i);
                ptst.setInt(2, random.nextInt(5)); // 学分随机0-4
                ptst.setString(3, "课程" + i + "的简介");
                ptst.addBatch(); // 添加到批量操作中
                
                // 每1000条执行一次批量更新
                if (i % 1000 == 0) {
                    ptst.executeLargeBatch();
                }
            }
            // 执行剩余的批量操作
            ptst.executeLargeBatch();
        } catch (SQLException e) {
            throw new RuntimeException(e);
        } finally {
            JDBCHelper.getInstance().free(conn);
        }
    }
}

使用连接池

数据库连接的建立和关闭耗费资源,连接池可复用连接,提升系统性能。JDBC 2.0规范引入javax.sql.DataSource接口,常用实现有Druid、C3P0、Hikari等。

依赖配置(Gradle)

dependencies {
    // mysql连接驱动依赖
    implementation 'mysql:mysql-connector-java:8.0.33'
    // 数据库连接池(Druid)
    implementation 'com.alibaba:druid:1.2.27'
    testImplementation platform('org.junit:junit-bom:5.10.0')
    testImplementation 'org.junit.jupiter:junit-jupiter'
}

image

连接池工具类(JDBCUtil)

package com.jdbc;

import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.Properties;

import javax.sql.DataSource;
import com.alibaba.druid.pool.DruidDataSourceFactory;

/**
 * jdbc工具类
 * 采用数据源(jdbc扩展规范)来获取连接
 *         传统方式:DriverManager.getConnection(url,username,password);使用完毕后进行释放
 *                创建数据库连接是非常消耗系统资源,频繁创建及释放连接,对系统性能影响很大        
 *         连接池参数
 *             初始化连接数
 *             最大连接数
 *             最大等待时间
 *             最大空闲数
 *             最小空闲数
 * 
 *         选用产品:
 *             dbcp2、c3p0、druid、boneCP、hikari .......
 *
 */
@SuppressWarnings("static-access")
public class JDBCUtil {
    //数据源
    private static DataSource dataSource;
    private static JDBCUtil instance;
    
    static {
        try { //初始化数据源
            var prop = new Properties();
            prop.load(JDBCUtil.class.getClassLoader().getResourceAsStream("jdbc.properties"));
            //通过工厂创建数据源,只要properties中的property的名称为DruidDataSourceFactory里的属性名相同,会自动将对应的属性值设置给对应的属性
            var factory = new DruidDataSourceFactory();
            dataSource = factory.createDataSource(prop);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
    
    private JDBCUtil() {
        
    }
    
    /**
     * 通过数据源获取连接对象
     * @return 数据库连接对象
     * @throws SQLException
     */
    public Connection getConnetion() throws SQLException {
        return dataSource.getConnection();
    }
    
    /**
     * 释放jdbc相关资源
     * @param rs 结果集
     * @param stat 语句对象
     * @param conn 连接对象
     */
    public void free(ResultSet rs, Statement stat, Connection conn) {
        try {
            if(rs != null)
                rs.close();
        } catch (SQLException e) {
            e.printStackTrace();
        } finally {
            try {
                if(stat != null)
                    stat.close();
            } catch (SQLException e) {
                e.printStackTrace();
            } finally {
                try {
                    if(conn != null)
                        conn.close();
                } catch (SQLException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    
    public static JDBCUtil getInstance() {
        if(instance == null) {
            synchronized (JDBCUtil.class) {
                if(instance == null)
                    instance = new JDBCUtil();
            }
        }
        return instance;
    }
}

DAO模式

DAO(Data Access Object)是数据访问对象,为数据库操作提供抽象接口,隔离数据访问逻辑与业务逻辑。

DAO模式组成

  1. 数据对象(Data Object):封装数据表数据的JavaBean(如Course)。
  2. 数据访问接口(Data Accessor):定义数据操作方法(如CourseDao)。
  3. 接口实现类:实现数据访问接口,具体执行数据库操作(如CourseDaoImpl)。

示例实现

数据对象(Course.java)

package com.jdbc.course;

/**
 * 课程数据对象(JavaBean)
 * @author Jing61
 */
public class Course {
    private int id;
    private String title;
    private int credits;
    private String intro;

    public Course() {
    }

    public Course(int id, String title, int credits, String intro) {
        this.id = id;
        this.title = title;
        this.credits = credits;
        this.intro = intro;
    }

    // getter和setter方法
    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public String getTitle() {
        return title;
    }

    public void setTitle(String title) {
        this.title = title;
    }

    public int getCredits() {
        return credits;
    }

    public void setCredits(int credits) {
        this.credits = credits;
    }

    public String getIntro() {
        return intro;
    }

    public void setIntro(String intro) {
        this.intro = intro;
    }

    @Override
    public String toString() {
        return "Course{" +
                "id=" + id +
                ", title='" + title + '\'' +
                ", credits=" + credits +
                ", intro='" + intro + '\'' +
                '}';
    }
}

数据访问接口(CourseDao.java)

package com.jdbc.course;

/**
 * 课程数据访问接口
 * @author Jing61
 */
public interface CourseDao {
    /**
     * 根据id查询课程信息
     * @param id 课程id
     * @return 课程信息
     */
    Course getCourseById(int id);
    
    /**
     * 更新课程信息
     * @param course 课程信息
     */
    void updateCourse(Course course);
    
    /**
     * 删除课程信息
     * @param id 课程id
     */
    void deleteCourse(int id);
    
    /**
     * 添加课程信息
     * @param course 课程信息
     */
    void insertCourse(Course course);
}

接口实现类(CourseDaoImpl.java)

package com.jdbc.course;

import com.jdbc.JDBCHelper;

import java.sql.Connection;
import java.sql.SQLException;

/**
 * 课程数据访问接口实现类
 * @author Jing61
 */
public class CourseDaoImpl implements CourseDao{
    
    private JDBCHelper jdbcHelper = JDBCHelper.getInstance();
    
    @Override
    public Course getCourseById(int id) {
        Course course = null;
        Connection conn = null;
        try {
            conn = jdbcHelper.getConnection();
            var sql = "SELECT id, title, credits, intro FROM course WHERE id = ?";
            var ptst = conn.prepareStatement(sql);
            ptst.setInt(1, id);
            var rs = ptst.executeQuery();
            if (rs.next()) {
                course = new Course();
                course.setId(rs.getInt("id"));
                course.setTitle(rs.getString("title"));
                course.setCredits(rs.getInt("credits"));
                course.setIntro(rs.getString("intro"));
            }
        } catch (SQLException e) {
            throw new RuntimeException(e);
        } finally {
            jdbcHelper.free(conn);
        }
        return course;
    }

    @Override
    public void updateCourse(Course course) {
        Connection conn = null;
        try {
            conn = jdbcHelper.getConnection();
            var sql = "UPDATE course SET title = ?, credits = ?, intro = ? WHERE id = ?";
            var ptst = conn.prepareStatement(sql);
            ptst.setString(1, course.getTitle());
            ptst.setInt(2, course.getCredits());
            ptst.setString(3, course.getIntro());
            ptst.setInt(4, course.getId());
            ptst.executeUpdate();
        } catch (SQLException e) {
            throw new RuntimeException(e);
        } finally {
            jdbcHelper.free(conn);
        }
    }

    @Override
    public void deleteCourse(int id) {
        Connection conn = null;
        try {
            conn = jdbcHelper.getConnection();
            var sql = "DELETE FROM course WHERE id = ?";
            var ptst = conn.prepareStatement(sql);
            ptst.setInt(1, id);
            ptst.executeUpdate();
        } catch (SQLException e) {
            throw new RuntimeException(e);
        } finally {
            jdbcHelper.free(conn);
        }
    }

    @Override
    public void insertCourse(Course course) {
        Connection conn = null;
        try {
            conn = jdbcHelper.getConnection();
            var sql = "INSERT INTO course(title, credits, intro) VALUES (?, ?, ?)";
            var ptst = conn.prepareStatement(sql);
            ptst.setString(1, course.getTitle());
            ptst.setInt(2, course.getCredits());
            ptst.setString(3, course.getIntro());
            ptst.executeUpdate();
        } catch (SQLException e) {
            throw new RuntimeException(e);
        } finally {
            jdbcHelper.free(conn);
        }
    }
}

RowSet

RowSet接口继承自ResultSet,默认支持滚动、更新、序列化,部分实现(如CachedRowSet)支持离线操作,无需保持数据库连接。

常用RowSet子接口

  • JdbcRowSet:需保持数据库连接。
  • CachedRowSet:离线结果集,数据缓存到内存。
  • FilteredRowSet:支持数据过滤。
  • JoinRowSet:支持多结果集关联。
  • WebRowSet:支持XML数据交互。

离线结果集

在使用ResultSet的时代,程序查询得到ResultSet之后必须立即读取或处理它对应的记录,否则一旦Connection关闭,再取通过ResultSet读取记录就会引发异常。在这种模式下,JDBC编程十分痛苦,假设应用程序分为数据访问层和视图显式层,当应用程序在数据访问层查询得到ResultSet后,对ResultSet的处理有如下常见操作:使用迭代访问ResultSet里的记录,并将这些记录转成JavaBean,再将多个JavaBean封装到一个List集合,处理完成后就可以关闭Connection等资源,然后再将JavaBean集合传到视图显示层。直接将ResultSet传到视图显示层——这要求当视图显示层显示数据时,底层Connection必须一直处于打开状态,否则ResultSet无法读取数据,这种方式比较安全,但编程要求较高,对各种类型转换处理也比较繁琐。另一种方式则需要Connection一直处于打开状态,这样不仅不安全,而且对程序性能也有很大影响。通过使用离线RowSet可以十分"优雅"地处理上面的问题,离线RowSet会直接将底层数据读入内存中,封装成RowSet对象,而RowSet对象可以完全当成JavaBean来使用。因此不仅安全而且编程十分简单。CachedRowSet是所有离线RowSet的父接口。

示例代码(CachedRowSet)

package com.jdbc;

import javax.sql.rowset.CachedRowSet;
import javax.sql.rowset.RowSetFactory;
import javax.sql.rowset.RowSetProvider;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.logging.Logger;

/**
 * 离线RowSet示例(CachedRowSet)
 * @author Jing61
 */
public class RowSetDemo {
    private JDBCHelper helper = JDBCHelper.getInstance();

    /**
     * 执行查询并返回离线RowSet
     * @param sql 查询语句
     * @param params 参数
     * @return CachedRowSet 离线结果集
     */
    public CachedRowSet executeQuery(String sql, Object...params){
        Connection conn = null;
        PreparedStatement ptst = null;
        ResultSet rs = null;
        try{
            conn = helper.getConnection();
            ptst = conn.prepareStatement(sql);
            // 设置SQL参数
            for (var i = 0; params != null && i < params.length; i++) {
                ptst.setObject(i + 1, params[i]);
            }
            rs = ptst.executeQuery();
            
            // 创建RowSetFactory
            RowSetFactory factory = RowSetProvider.newFactory();
            // 创建CachedRowSet实例
            CachedRowSet crs = factory.createCachedRowSet();
            // 用ResultSet填充CachedRowSet(数据缓存到内存)
            crs.populate(rs);
            return crs;
        }catch (SQLException e){
            throw new RuntimeException(e);
        }finally {
            // 关闭数据库连接(CachedRowSet已离线,不影响后续操作)
            try {
                if (rs != null) rs.close();
                if (ptst != null) ptst.close();
                if (conn != null) conn.close();
            } catch (SQLException e) {
                e.printStackTrace();
            }
        }
    }

    // 测试
    public static void main(String[] args) {
        RowSetDemo demo = new RowSetDemo();
        var sql = "SELECT id, title, credits, intro FROM course WHERE credits > ?";
        CachedRowSet crs = demo.executeQuery(sql, 4);
        
        try {
            // 离线状态下操作结果集(无需数据库连接)
            System.out.println("离线结果集数据:");
            while (crs.next()) {
                System.out.println(crs.getInt("id") + "\t" + crs.getString("title") + "\t" + crs.getInt("credits"));
            }
            
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }
}
posted @ 2025-11-20 12:57  Jing61  阅读(2)  评论(0)    收藏  举报