深入理解JDBC:Java数据库连接的核心技术与实践

一、JDBC概述

1、什么是JDBC?

**JDBC(Java Database Connectivity)**​ 是 Java 语言访问数据库的标准 API(应用程序编程接口)。它提供了一组类和接口,允许 Java 程序与各种关系型数据库(如 MySQL、Oracle、PostgreSQL 等)进行交互。

核心作用

  • 桥梁作用:连接 Java 应用程序和数据库

  • 统一接口:用相同的方法访问不同类型的数据库

  • SQL 执行:执行 SQL 语句并处理结果

2、JDBC的原理

早期SUN公司的天才们想编写一套可以连接所有数据库的API,但是当他们刚刚开始时就发现这是不可完成的任务,因为各个厂商的数据库服务器差异太大了。后来SUN开始与数据库厂商们讨论,最终得出的结论是,由SUN提供一套访问数据库的规范(就是一组接口),并提供连接数据库的协议标准,然后各个数据库厂商会遵循SUN的规范提供一套访问自己公司的数据库服务器的API出现。SUN提供的规范命名为JDBC,而各个厂商提供的,遵循了JDBC规范的,可以访问自己数据库的API被称之为驱动!

JDBC是接口,而JDBC驱动才是接口的实现,没有驱动无法完成数据库连接!每个数据库厂商都有自己的驱动,用来连接自己公司的数据库。

当然还有第三方公司专门为某一数据库提供驱动,这样的驱动往往不是开源免费的!

3、JDBC的API

简单来说,JDBC可做三件事:与数据库建立连接、发送操作数据库的语句、处理查询结果。

  1. DriverManager :依据数据库的不同,管理JDBC驱动

Connection :负责连接数据库并担任传送数据的任务

Statement :由 Connection 产生、负责发送执行SQL语句

ResultSet:负责保存Statement执行后所产生的查询结果

二、常用接口详解

1、DriverManager

用于管理JDBC驱动的服务类。程序中使用该类的的主要功能是获取Connection对象,该类包含如下方法:

|------------------------------------------------------------------------------------------------------|-------------------|
| public static Connection getConnection(String url, String user, String password) throws SQLException | 该方法获得url对应数据库的连接; |

2、Connection

代表数据库连接对象,每个Connection代表一个物理连接会话。要想访问数据库,必须先得到数据库连接。该接口的常用方法如下:

|-------------------------------------------------------------------|-----------------------------------------|
| Statement createStatement() throws SQLException | 该方法返回一个Statement对象 |
| PreparedStatement prepareStatement(String sql)throws SQLException | 该方法返回预编译的Statement对象,即将SQL语句提交到数据库进行预编译 |
| CallableStatement prepareCall(String sql) throws SQLException | 该方法返回CallableStatement对象,该对象用于调用存储过程 |

上面上个方法都返回用于执行sql语句的Statement对象,PreparedStatement和CallableStatement是Statement的子类,只有获得了Statement之后才可以执行sql语句;

除此之外,Connection还有如下几个用于控制事务的方法。

|-------------------------------------------------------------|---------------|
| Savepoint setSavepoint() throws SQLException | 创建一个保存点 |
| Savepoint setSavepoint(String name) throws SQLException | 以指定名字来创建一个保存点 |
| void setTransactionIsolation(int level) throws SQLException | 设置事务的隔离级别 |
| void rollback() throws SQLException | 回滚事务 |
| void rollback(Savepoint savepoint) throws SQLException | 将事务回滚到指定的保存点 |
| void setAutoCommit(boolean autoCommit) throws SQLException | 关闭自动提交,打开事务 |
| void commit() throws SQLException | 提交事务 |

3、Statement

用于执行sql语句的工具接口。该对象既可以执行DDL,DCL语句,也可以用于执行DML语句,还可以用于执行sql查询。当执行sql查询时,返回查询到的结果集。它的常用方法如下:

|--------------------------------------------------------|---------------------------------------------------------------------------------|
| ResultSet executeQuery(String sql) throws SQLException | 该方法用于执行查询语句,并返回查询结果对应ResultSet对象。该方法只能用于执行查询语句 |
| int executeUpdate(String sql) throws SQLException | 该方法用于执行DML语句,并返回受影响的行数;该方法也可用于执行DDL语句,执行DDL语句将返回0 |
| boolean execute(String sql) throws SQLException | 改方法可以执行任何sql语句。如果执行后第一个结果为ResultSet对象,则返回true;如果执行后第一个结果为受影响的行数或没有任何结果,则返回false |

4、PreparedStatement

预编译的Statement对象,PreparedStatement是Statement的子接口,它允许数据库预编译sql语句(这些sql语句通常带有参数),以后每次只改变sql命令的参数,避免数据库每次都需要编译sql语句,无需再传入sql语句,只要为预编译的sql语句传入参数值即可。所以它比Statement多了如下方法:

|--------------------------------------------|---------------------------------------------------|
| void setXxx(int parameterIndex, Xxx value) | 该方法根据传入参数值的类型不同,需要使用不同的方法。传入的值根据索引传给sql语句中指定位置的参数 |

5、ResultSet

结果集对象。该对象包含访问查询结果的方法,ResultSet可以通过列索引或列名获得列数据。它包含了如下常用方法来移动记录指针。

|-------------------------------------------------|----------------------------------------------------------------------|
| void close() throws SQLException | 释放ResultSet对象 |
| boolean absolute( int row ) throws SQLException | 将结果集的记录指针移动到第row行,如果row是负数,则移动到倒数第row行,如果移动后的记录指针指向一条有效记录,则该方法返回true |
| boolean next() throws SQLException | 将结果集的记录指针定位到下一行,如果移动后的记录指针指向一条有效的记录,则该方法返回true |
| boolean last() throws SQLException | 将结果集的记录指针定位到最后一行,如果移动后的记录指针指向一条有效的记录,则该方法返回true |

三、操作数据库的步骤

1、加载驱动

加载JDBC驱动是通过调用方法java.lang.Class.forName(),下面列出常用的几种数据库驱动程序加载语句的形式 :

Class.forName("oracle.JDBC.driver.OracleDriver");//使用Oracle的JDBC驱动程序

Class.forName("com.microsoft.JDBC.sqlserver.SQLServerDriver");//使用SQL Server的JDBC驱动程序

Class.forName("com.ibm.db2.JDBC.app.DB2Driver");//使用DB2的JDBC驱动程序

Class.forName("com.mysql.jdbc.Driver");//使用MySql5的JDBC驱动程序

Class.forName("com.mysql.cj.jdbc.Driver");//使用MySql8的JDBC驱动程序

2、创建数据库连接

与数据库建立连接的方法是调用DriverManager.getConnection(String url, String user, String password )方法

java 复制代码
Connection conn=null;
String url="jdbc:mysql://localhost:3306/jdbc_db?useUnicode=true&useSSL=false&charsetUnicode=UTF8";
String user="root";
String password="123456";
conn = DriverManager.getConnection(url, user, password);

3、创建Statement并发送命令

Statement对象用于将 SQL 语句发送到数据库中,或者理解为执行sql语句。有三种 Statement对象:

  • Statement:用于执行不带参数的简单SQL语句;
  • PreparedStatement(从 Statement 继承):用于执行带或不带参数的预编译SQL语句;
  • CallableStatement(从PreparedStatement 继承):用于执行数据库存储过程的调用。

|---------------------------|-----------------------|-------------------------------------------------|
| 方法名 | 适用 SQL 类型 | 返回值 |
| executeQuery(String sql) | SELECT | ResultSet:查询结果集 |
| executeUpdate(String sql) | INSERT/UPDATE/DELETE | int: 受影响行数 |
| execute(String sql) | 任意 SQL | boolean:- true = 查询语句(有结果集) - false = 更新 (无结果集) |

4、处理ResultSet结果

ResultSet对象是executeQuery()方法的返回值,它被称为结果集,它代表符合SQL语句条件的所有行,并且它通过一套getXXX方法(这些get方法可以访问当前行中的不同列)提供了对这些行中数据的访问。

ResultSet里的数据一行一行排列,每行有多个字段,且有一个记录指针,指针所指的数据行叫做当前数据行,我们只能来操作当前的数据行。我们如果想要取得某一条记录,就要使用ResultSet的next()方法 ,如果我们想要得到ResultSet里的所有记录,就应该使用while循环。

ResultSet对象自动维护指向当前数据行的游标。每调用一次next()方法,游标向下移动一行。

初始状态下记录指针指向第一条记录的前面,通过next()方法指向第一条记录。循环完毕后指向最后一条记录的后面。

|-----------------------------------|-----------------------------------|
| 方法名说明 | 方法名说明 |
| boolean next() | 将光标从当前位置向下移动一行 |
| boolean previous() | 游标从当前位置向上移动一行 |
| void close() | 关闭 ResultSet 对象 |
| int getInt(int colIndex) | 根据列的‌索引位置‌(从 1 开始计数)获取int类型的列值 |
| int getInt(String colLabel) | 根据列的‌名称‌(即列别名或原始列名)获取int类型的列值 |
| float getFloat(int colIndex) | 根据列的‌索引位置‌(从 1 开始计数)获取float 类型的列值 |
| float getFloat (String colLabel) | 根据列的‌名称‌(即列别名或原始列名)获取float类型的列值 |
| String getString(int colIndex) | 根据列的‌索引位置‌(从 1 开始计数)获取String类型的列值 |
| String getString(String colLabel) | 根据列的‌名称‌(即列别名或原始列名)获取String类型的列值 |
| Object getObject(String colLabel) | 根据列的‌名称‌(即列别名或原始列名)获取Object类型的列值 |
| ResultSetMetaData getMetaData() | 获取结果集中列的详细信息 |

5、关闭数据库

Connection对象代表与数据库的物理连接,是稀缺资源(数据库服务器的最大连接数有限)。如果连接不关闭,会导致连接池耗尽,后续请求无法获取连接,系统可能瘫痪。Statement对象(包括PreparedStatement)在数据库端占用执行资源(如SQL执行计划缓存),不关闭会耗尽数据库句柄,影响新查询的执行。‌关闭Statement对象和Connection对象的语法形式为:public void close() throws SQLException

注意:关闭顺序必须与创建顺序相反,如果先关闭Connection对象,Statement又依赖Connection的连接,会导致Statement对象的关闭操作失败,因为其依赖的资源已释放,可能抛出异常或造成资源泄漏

四、准备工作

1、创建数据并创建student表

2、创建项目

3、创建lib目录并引入MySQL驱动包

直接下载链接:https://repo1.maven.org/maven2/mysql/mysql-connector-java/5.1.38/mysql-connector-java-5.1.38.jar

4、把lib包引入项目环境

(1)右键lib包,选择"Add as Library..."

(2)点击"ok"

(3)添加成功lib包就能点开了

五、使用JDBC完成添加操作

1、案例

java 复制代码
package com.hg.jdbc;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.sql.Statement;

public class AddTest {
    // 驱动器路径
    private static final String DRIVER = "com.mysql.jdbc.Driver";
    //连接数据库地址
    private static final String URL = "jdbc:mysql://localhost:3306/jdbc_db?useSSL=false&characterEncoding=UTF8";
    //数据库用户名
    private static final String USER_NAME = "root";
    //数据库密码
    private static final String USER_PASSWORD = "1111";
    public static void main(String[] args) throws ClassNotFoundException, SQLException {
        // 加载JDBC访问mysql的驱动
        Class.forName(DRIVER);
        // 建立和数据库的连接
        Connection conn = DriverManager.getConnection(URL, USER_NAME, USER_PASSWORD);
        // 创建SQL命令发送器
        Statement stmt = conn.createStatement();
        // 使用SQL命令发送器发送SQL命令并得到结果
        String sql = "insert into student values(1,'小刚',32,'男','湖北省武汉市')";
        int n = stmt.executeUpdate(sql);
        // 处理结果
        if (n > 0) {
            System.out.println("添加成功");
        } else {
            System.out.println("添加失败");
        }
        // 关闭数据库资源
        stmt.close();
        conn.close();
    }
}

2、URL详解

2.1、为什么要定义URL

Java和MySQL是厂商的,Java程序和MySQL数据库此时不在同一个进程下,此时Java程序需要向MySQL发送请求。

2.2、一个URL由什么组成
  • 协议://服务器主机:端口/服务器路径?查询参数
  • 协议 jdbc:mysql:
  • 服务器主机 localhost
  • 端口 3306
  • 服务器路径 jdbc_db

六、使用JDBC完成更新和删除

1、案例

一般步骤:

  • 加载MySQL的JDBC驱动
  • 建立数据的连接
  • 创建SQL命令的发送器
  • 编写SQL
  • 使用SQL命令发送器发送SQL命令并得到结果
  • 处理结果
  • 关闭数据库资源

2、修改代码:

java 复制代码
package com.hg.jdbc;

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


public class UpdateTest {
    // 驱动器路径
    private static final String DRIVER = "com.mysql.jdbc.Driver";
    //连接数据库地址
    private static final String URL = "jdbc:mysql://localhost:3306/jdbc_db?useSSL=false&characterEncoding=UTF8";
    //数据库用户名
    private static final String USER_NAME = "root";
    //数据库密码
    private static final String USER_PASSWORD = "1111";
    public static void main(String[] args) throws ClassNotFoundException, SQLException {
        // 加载Oracle的JDBC驱动
        Class.forName(DRIVER);
        // 建立数据的连接
        Connection conn=DriverManager.getConnection(URL, USER_NAME, USER_PASSWORD);
        // 创建SQL命令的发送器
        Statement stat=conn.createStatement();
        // 编写SQL
        String sql="update student set name='小明',age=23,sex='女',address='武汉' where id=1";
        // 使用SQL命令发送器发送SQL命令并得到结果
        int res=stat.executeUpdate(sql);
        // 处理结果
        if(res>0){
            System.out.println("修改成功");
        }
        else{
            System.out.println("处理失败");
        }
        // 关闭数据库资源
        stat.close();
        conn.close();
    }
}

3、删除代码

java 复制代码
package com.hg.jdbc;

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

public class DeleteTest {
    // 驱动器路径
    private static final String DRIVER = "com.mysql.jdbc.Driver";
    //连接数据库地址
    private static final String URL = "jdbc:mysql://localhost:3306/jdbc_db?useSSL=false&characterEncoding=UTF8";
    //数据库用户名
    private static final String USER_NAME = "root";
    //数据库密码
    private static final String USER_PASSWORD = "1111";
    public static void main(String[] args) throws ClassNotFoundException, SQLException {
        // 加载Oracle的JDBC驱动
        Class.forName(DRIVER);
        // 建立数据的连接
        Connection conn=DriverManager.getConnection(URL, USER_NAME, USER_PASSWORD);
        // 创建SQL命令的发送器
        Statement stat=conn.createStatement();
        // 编写SQL
        String sql="delete from student where id=1";
        // 使用SQL命令发送器发送SQL命令并得到结果
        int res=stat.executeUpdate(sql);
        // 处理结果
        if(res>0){
            System.out.println("删除成功");
        }
        else{
            System.out.println("删除失败");
        }
        // 关闭数据库资源
        stat.close();
        conn.close();
    }
}

七、DBUtils的简单封装

1、为什么要封装

从我们上面的代码大家可以看到,每一次写我们创建一个连接,创建一个发送SQL的对象,最后还要关闭,那么我们可以考虑把这重复的代码提取出来!

2、创建db.properties

java 复制代码
driverClass=com.mysql.jdbc.Driver
url=jdbc:mysql://localhost:3306/jdbc_db
username=root
password=1111

3、创建DBUtils

java 复制代码
public class DBUtils {

    //1.声明所需要的配置变量
    private static String DRIVER;
    private static String URL;
    private static String USER_NAME;
    private static String USER_PASSWORD;

    //2.提供静态代码块。读取配置文件的信息为变量赋值,注册驱动
    static{
        try {
            //读取配置文件的信息为变量赋值
            InputStream is = 
DBUtils.class.getClassLoader().getResourceAsStream("db.properties");
            Properties prop = new Properties();
            prop.load(is);

            DRIVER = prop.getProperty("driverClass");
            URL = prop.getProperty("url");
            USER_NAME = prop.getProperty("username");
            USER_PASSWORD = prop.getProperty("password");

            //注册驱动
            Class.forName(DRIVER);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
    //3.提供获取数据库连接方法
    public static Connection getConn() {
        try {
            return  DriverManager.getConnection(URL, USER_NAME, USER_PASSWORD);
        } catch (SQLException e) {
            e.printStackTrace();
            System.out.println("创建连接对象异常");
        }
        return null;
    }

    //4.提供释放资源的方法
    public static void close(AutoCloseable closeable) {
        try {
            if (closeable != null) {
                closeable.close();
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

4、对前面的代码进行改造

java 复制代码
package com.hg.jdbc;
import com.hg.utils.DBUtils;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.sql.Statement;

public class Test01Add {
    public static void main(String[] args) throws ClassNotFoundException, SQLException {
        Connection conn = DBUtils.getConn();
        // 创建SQL命令发送器
        Statement stmt = conn.createStatement();
        // 使用SQL命令发送器发送SQL命令并得到结果
        String sql = "insert into student values(1,'小刚',32,'男','湖北省武汉市')";
        int n = stmt.executeUpdate(sql);
        // 处理结果
        if (n > 0) {
            System.out.println("添加成功");
        } else {
            System.out.println("添加失败");
        }
        // 关闭数据库资源
        DBUtils.close(stmt);
        DBUtils.close(conn);
    }
}

八、使用JDBC完成查询

1、环境准备:

java 复制代码
package com.hg.jdbc;

import com.hg.utils.DBUtils;

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

public class Test01Add20 {
    public static void main(String[] args) throws ClassNotFoundException, SQLException {
        Connection conn = DBUtils.getConn();
        // 创建SQL命令发送器
        Statement stmt = conn.createStatement();
        // 使用SQL命令发送器发送SQL命令并得到结果
        Random random=new Random();
        for (int i = 1; i <=20 ; i++) {
            Integer id=i;
            String name="小明"+i;
            int age=random.nextInt(100);
            String sex=random.nextBoolean()?"男":"女";
            String address="武汉"+i;

            String sql = "insert into student values("+i+",'"+name+"',"+age+",'"+sex+"','"+address+"')";
            int n = stmt.executeUpdate(sql);
            // 处理结果
            if (n > 0) {
                System.out.println("添加成功");
            } else {
                System.out.println("添加失败");
            }
        }
        // 关闭数据库资源
        DBUtils.close(stmt);
        DBUtils.close(conn);
    }
}

2、案例

java 复制代码
package com.hg.jdbc;
import com.hg.utils.DBUtils;
import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;

public class Test04Query {
    public static void main(String[] args) throws ClassNotFoundException, SQLException {
        Connection conn = DBUtils.getConn();
        // 创建SQL命令发送器
        Statement stmt = conn.createStatement();
        // 编写SQL
        String sql="select * from student";
        // 使用SQL命令发送器发送SQL命令并得到结果
        ResultSet rs=stmt.executeQuery(sql);
        // 处理结果
        while(rs.next()){
            int id=rs.getInt(1);
    String name=rs.getString(2);
            int age=rs.getInt(3);
            String sex=rs.getString(4);
            String address=rs.getString(5);
            System.out.println(id+"  "+name+"  "+age+"   "+sex+"   "+address);
        }
        // 关闭数据库资源
        DBUtils.close(rs);
        DBUtils.close(stmt);
        DBUtils.close(conn);
    }
}

九、使用JDBC完成分页查询

1、案例

java 复制代码
package com.hg.jdbc;

import com.hg.utils.DBUtils;

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

public class Test05QueryForPage {
    public static void main(String[] args) throws ClassNotFoundException, SQLException {
        Connection conn = DBUtils.getConn();
        // 创建SQL命令发送器
        Statement stmt = conn.createStatement();
        int pageNum=2; //页码
        int pageSize=5;//每页显示的条数
        // 编写SQL
        String sql="select * from student limit "+(pageNum-1)*pageSize+","+pageSize;
        // 使用SQL命令发送器发送SQL命令并得到结果
        ResultSet rs=stmt.executeQuery(sql);
        // 处理结果
        while(rs.next()){
            int id=rs.getInt(1);
            String name=rs.getString(2);
            int age=rs.getInt(3);
            String sex=rs.getString(4);
            String address=rs.getString(5);
            System.out.println(id+"  "+name+"  "+age+"   "+sex+"   "+address);
        }
        // 关闭数据库资源
        DBUtils.close(rs);
        DBUtils.close(stmt);
        DBUtils.close(conn);
    }
}

十、SQL注入问题

1、问题引入

1.1、创建sys_user表并初始化数据
1.2、编写代码实现登录
java 复制代码
package com.hg.jdbc;

import com.hg.utils.DBUtils;

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

public class Test06Login {
    public static void main(String[] args) throws ClassNotFoundException, SQLException {
        Connection conn = DBUtils.getConn();
        // 创建SQL命令发送器
        Statement stmt = conn.createStatement();
        //从键盘输入
        Scanner scanner=new Scanner(System.in);
        System.out.println("请输入用户名:");
        String username = scanner.nextLine();
        System.out.println("请输入密码:");
        String password=scanner.nextLine();
        // 编写SQL
        String sql="select * from sys_user where username='"+username+"' and password='"+password+"'";
        // 使用SQL命令发送器发送SQL命令并得到结果
        ResultSet rs=stmt.executeQuery(sql);
        System.out.println(sql);
        // 处理结果
        if(rs.next()){
            System.out.println("登陆成功");
            int id=rs.getInt(1);
            String name=rs.getString(2);
            String pwd=rs.getString(3);
            System.out.println(id+"  "+name+"  "+pwd);
        }
        // 关闭数据库资源
        DBUtils.close(rs);
        DBUtils.close(stmt);
        DBUtils.close(conn);
    }
}
1.3、测试登录

2、解决办法【使用PrepareStatement】

2.1、技术原理

该 PreparedStatement接口继承Statement,并与之在两方面有所不同:

PreparedStatement 实例包含已编译的 SQL 语句。这就是使语句"准备好"。包含于 PreparedStatement 对象中的 SQL 语句可具有一个或多个 IN 参数。IN参数的值在 SQL 语句创建时未被指定。相反的,该语句为每个 IN 参数保留一个问号**("?")作为占位符**。每个问号的值必须在该语句执行之前,通过适当的setXXX 方法来提供。

由于 PreparedStatement 对象已预编译过,所以其执行速度要快于 Statement 对象。因此,多次执行的 SQL 语句经常创建为 PreparedStatement 对象,以提高效率。

作为 Statement 的子类,PreparedStatement 继承了 Statement 的所有功能。另外它还添加了一整套方法,用于设置发送给数据库以取代 IN 参数占位符的值。同时,三种方法 execute、 executeQuery 和 executeUpdate 已被更改以使之不再需要参数。这些方法的 Statement 形式(接受 SQL 语句参数的形式)不应该用于 PreparedStatement 对象。

2.2、创建对象

以下的代码段(其中 con 是 Connection 对象)创建包含带两个 IN 参数占位符的 SQL 语句的 PreparedStatement 对象:

PreparedStatement pstmt = con.prepareStatement("UPDATE table4 SET m = ? WHERE x = ?");

pstmt 对象包含语句 "UPDATE table4 SET m = ? WHERE x = ?",它已发送给DBMS,并为执行作好了准备。

2.3、传递参数

在执行 PreparedStatement 对象之前,必须设置每个 ? 参数的值。这可通过调用 setXXX 方法来完成,其中 XXX 是与该参数相应的类型。例如,如果参数具有Java 类型 long,则使用的方法就是 setLong。setXXX 方法的第一个参数是要设置的参数的序数位置,第二个参数是设置给该参数的值。例如,以下代码将第一个参数设为 123456789,第二个参数设为 100000000:

pstmt.setLong(1, 123456789);

pstmt.setLong(2, 100000000);

一旦设置了给定语句的参数值,就可用它多次执行该语句,直到调用clearParameters 方法清除它为止。在连接的缺省模式下(启用自动提交),当语句完成时将自动提交或还原该语句。

如果基本数据库和驱动程序在语句提交之后仍保持这些语句的打开状态,则同一个 PreparedStatement 可执行多次。如果这一点不成立,那么试图通过使用PreparedStatement 对象代替 Statement 对象来提高性能是没有意义的。

2.4、修改代码
java 复制代码
package com.hg.jdbc;

import com.hg.utils.DBUtils;

import java.sql.*;
import java.util.Scanner;

public class Test07Login {
    public static void main(String[] args) throws ClassNotFoundException, SQLException {
        //从键盘输入
        Scanner scanner=new Scanner(System.in);
        System.out.println("请输入用户名:");
        String username = scanner.nextLine();
        System.out.println("请输入密码:");
        String password=scanner.nextLine();
        Connection conn = DBUtils.getConn();
        // 编写SQL
        String sql="select * from sys_user where username=? and password=? ";
        System.out.println(sql);
        // 创建SQL命令发送器
        PreparedStatement pstmt = conn.prepareStatement(sql);
        pstmt.setString(1,username);
        pstmt.setString(2,password);
        // 使用SQL命令发送器发送SQL命令并得到结果
        ResultSet rs=pstmt.executeQuery();
        // 处理结果
        if(rs.next()){
            System.out.println("登陆成功");
            int id=rs.getInt(1);
            String name=rs.getString(2);
            String pwd=rs.getString(3);
            System.out.println(id+"  "+name+"  "+pwd);
        }
        // 关闭数据库资源
        DBUtils.close(rs);
        DBUtils.close(pstmt);
        DBUtils.close(conn);
    }
}
2.5、再次测试

十一、事务处理解决转账问题

1、什么是事务

是数据库操作的最小工作单元,是作为单个逻辑工作单元执行的一系列操作;这些操作作为一个整体一起向系统提交,要么都执行、要么都不执行;事务是一组不可再分割的操作集合(工作逻辑单元)

2、事务的四大特性

①原子性:

事务是数据库的逻辑工作单位,事务中包含的各操作要么都做,要么都不做

②一致性:

事务执行的结果必须是使数据库从一个一致性状态变到另一个一致性状态。因此当数据库只包含成功事务提交的结果时,就说数据库处于一致性状态。如果数据库系统 运行中发生故障,有些事务尚未完成就被迫中断,这些未完成事务对数据库所做的修改有一部分已写入物理数据库,这时数据库就处于一种不正确的状态,或者说是 不一致的状态。

③隔离性

一个事务的执行不能其它事务干扰。即一个事务内部的操作及使用的数据对其它并发事务是隔离的,并发执行的各个事务之间不能互相干扰。

④持久性

也称永久性,指一个事务一旦提交,它对数据库中的数据的改变就应该是永久性的。接下来的其它操作或故障不应该对其执行结果有任何影响。

3、需求描述

完成转账操作

要求,两次数据库操作,要么全成功。要么全失败

4、准备工作

4.1、创建表

CREATE TABLE account

(

aid INT PRIMARY KEY AUTO_INCREMENT,

aname VARCHAR(30) NOT NULL,

amount DECIMAL(10,2) NOT NULL

);

4.2、初始化数据

5、代码实现

java 复制代码
package com.hg.jdbc;

import com.hg.utils.DBUtils;

import java.sql.*;
import java.util.Scanner;

public class Test08Transaction {
    public static void main(String[] args) {
        //声明连接对象
        Connection conn=null;
        //声明发送SQL的接口对象
        Statement stmt=null;
        try {
            //创建连接对象
            conn = DBUtils.getConn();
            // 关闭自动提交事务
            conn.setAutoCommit(false);
            // 编写SQL
            String sql1 = "update account set amount = amount-1000 where aid=1";
            String sql2 = "update account set amount = amount+1000 where aid=2";
            // 创建SQL命令发送器
            stmt = conn.createStatement();
            stmt.executeUpdate(sql1);
            stmt.executeUpdate(sql2);
         }catch (Exception e){
            e.printStackTrace();
            try {
                conn.rollback();
            } catch (SQLException e1) {
                e1.printStackTrace();
            }
        }finally {
            // 关闭数据库资源
            DBUtils.close(stmt);
            DBUtils.close(conn);
        }
    }
}

十二、BaseDAO的封装

1、概述

对于JDBC市面上有一些封装的非常好的ORM(对象关系映射)框架,如mybatis mybatis plus hibernate,但是我们现在还没有学到框架,如何自己做模拟一个ORM的框架呢,接下来我们来讲解BaseDAO的封装和使用

2、代码

java 复制代码
public class BaseDao {

    /**
     * @param sql    sql 指令
     * @param clss   orm关联类
     * @param params sql中占位符 对应的参数
     * @param <T>
     * @return
     * @throws Exception
     */
    public  <T> List<T> selectList(String sql, Class<T> clss, Object... params)  {
        //创建一个空容器  规避空指针异常问题
        List<T> data = new ArrayList<>(); // 需要改动
        //2. 创建连接 使用驱动管理器 创建连接
        Connection conn = DBUtils.getConnection();
        //4. 获取指令的指令对象
        PreparedStatement prep = null;
        ResultSet rs = null;
        try {
            //4. 获取指令的指令对象
            prep = conn.prepareStatement(sql);
            //设置预编译参数
            for (int i = 0; i < params.length; i++) {
                Object param = params[i]; //获取参数
                //设置参数
                prep.setObject(i + 1, param);
            }
            //5. 执行指令
            rs = prep.executeQuery();
            //获取结果的元信息
            ResultSetMetaData metaData = rs.getMetaData();
            //获取列数
            int columnCount = metaData.getColumnCount();
            //遍历结果
            while (rs.next()) {
                T t = clss.newInstance();
                for (int i = 0; i < columnCount; i++) {
                    //获取别名  也是属性名
                    String columnLabel = metaData.getColumnLabel(i + 1);
                    //根据别名获取值
                    Object columnValue = rs.getObject(columnLabel);
                    //获取属性
                    Field field = clss.getDeclaredField(columnLabel); //需要改动
                    //设置属性值
                    field.setAccessible(true);
                    field.set(t, columnValue);
                }
                data.add(t);//放入list
            }
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            DBUtils.close(conn);
            DBUtils.close(prep);
            DBUtils.close(rs);
        }
        return data;
    }


    /**
     * 查询单个对象
     *
     * @param sql
     * @param clss
     * @param params
     * @param <T>
     * @return
     * @throws Exception
     */
    public  <T> T selectOne(String sql, Class<T> clss, Object... params)  {
        List<T> list = selectList(sql, clss, params);
        if (!list.isEmpty() && list.size() == 1) {
            return list.get(0);
        }
        return null;
    }

    /**
     * 通用的更新操作
     * @param sql
     * @param params
     * @return
     */
    public  boolean update(String sql, Object... params) {
        //1. 获取连接
        Connection conn = DBUtils.getConnection();
        PreparedStatement prep = null;
        try {
            prep = conn.prepareStatement(sql);
            //设置预编译参数
            for (int i = 0; i < params.length; i++) {
                Object param = params[i]; //获取参数
                //设置参数
                prep.setObject(i + 1, param);
            }
            //执行sql指令
            int m = prep.executeUpdate();

            return m >= 1;
        } catch (SQLException throwables) {
            throwables.printStackTrace();
        }finally {
            DBUtils.close(conn);
            DBUtils.close(prep);
        }
        return  false;
    }
}

3、使用

java 复制代码
package com.hg.common;

import java.util.List;


public class PageInfo<T> {

    private List<T> data;// 具体的数据
    private Long count; //符合条件的数据条数


    public List<T> getData() {
        return data;
    }
        User user = super.selectOne(sql, User.class, username, password);
        return user;
    };

    public void updateState(String id, Integer delete) {
        String sql = "update user set deleted =  ?,deleted_time = now() where id = ?";
        super.update(sql,delete,id);
    }
}

十三、数据库连接池

1、什么是连接池

连接池是创建和管理一个连接的缓冲池的技术,这些连接准备好被任何需要它们的线程使用。连接池是装有连接的容器,使用连接的话,可以从连接池中进行获取,使用完成之后将连接归还给连接池。

2、为什么使用连接池

连接对象创建和销毁是需要耗费时间的,在服务器初始化的时候就初始化一些连接。把这些连接放入到内存中,使用的时候可以从内存中获取,使用完成之后将连接放入连接池中。从内存中获取和归还的效率要远远高于创建和销毁的效率。(提升性能)。

3、连接池工作原理

4、自定义连接池

java 复制代码
public class MyDataSource {
    // 准备一个容器。用于保存多个数据库连接对象
    private static final Queue<Connection> connectionQueue = new LinkedList<>();

    // 数据库连接配置(可抽成配置类,示例简化直接写死)
    private static final String URL = "jdbc:mysql://localhost:3306/jdbc_db";
    private static final String USER = "root";
    private static final String PASSWORD = "1111";
    // 连接池参数
    private  int initSize; // 初始化连接数
        return conn;
    }

    /**
     * 归还连接:将连接放回队列(核心:不关闭连接,而是复用)
     * @param conn 要归还的连接
     */
    public void releaseConnection(Connection conn) {
        if (conn == null) {
            return;
        }
        // offer():将连接放回队尾
        connectionQueue.offer(conn);
        System.out.println("连接归还成功,剩余可用连接数:" + connectionQueue.size());
    }
}

测试:

java 复制代码
public class MyDataSourceTest {
    public static void main(String[] args) throws Exception {
       // 1. 创建连接池:初始5个连接
        MyDataSource pool = new MyDataSource(5);

        // 2. 获取连接1
        Connection conn1 = pool.getConnection();

        // 3.查询学生表的全部信息
        String sql = "SELECT * FROM student";
        PreparedStatement pst = conn1.prepareStatement(sql);
        ResultSet rs = pst.executeQuery();
        while (rs.next()) {
            System.out.println(rs.getInt("id") + "\t" + rs.getString("name") + "\t" + rs.getInt("age"));
        }

        // 5. 归还连接1
        pool.releaseConnection(conn1);

        // 6. 再次获取连接2(此时有可用连接)
        Connection conn2 = pool.getConnection();

        // 7. 归还连接2
        ds.releaseConnection(conn2);
    }
}

5、druid连接池

下载地址:https://github.com/alibaba/druid/releases/tag/druid-1.1.0

案例:

java 复制代码
public class DruidDataSourceTest {
    private static DruidDataSource initDruidDataSource() {
        DruidDataSource ds = new DruidDataSource();
        // 基础配置
        ds.setUrl("jdbc:mysql://localhost:3306/jdbc_db");
        ds.setUsername("root");
        ds.setPassword("1111");
        // 连接池配置
        ds.setInitialSize(2); // 初始连接数
        ds.setMaxActive(5);   // 最大活跃连接数
        ds.setMinIdle(1);     // 最小空闲连接数
        return ds;
    }

    public static void main(String[] args) throws SQLException {
        // 1. 创建连接池:初始5个连接
        DruidDataSource ds = initDruidDataSource();
        // 2. 获取连接1
        Connection conn1 = ds.getConnection();
        System.out.println("获取连接1成功,剩余可用连接数:" + ds.getPoolingCount());

        // 3.查询学生表的全部信息
        String sql = "SELECT * FROM student";
        PreparedStatement pst = conn1.prepareStatement(sql);
        ResultSet rs = pst.executeQuery();
        while (rs.next()) {
            System.out.println(rs.getInt("id") + "\t" + rs.getString("name") + "\t" + rs.getInt("age"));
        }

        // 5. 归还连接1, Druid重写了该方法,实际是归还而非关闭
        conn1.close();
        System.out.println("归还连接1,剩余可用连接数:" + ds.getPoolingCount());

        // 6. 再次获取连接2(此时有可用连接)
        Connection conn2 = ds.getConnection();
        System.out.println("获取连接2成功,剩余可用连接数:" + ds.getPoolingCount());
        // 7. 归还连接2
        conn2.close();
        System.out.println("归还连接2,剩余可用连接数:" + ds.getPoolingCount());
    }
}

6、修改DBUtils

java 复制代码
public class DBUtils {

    //1.声明所需要的配置变量
    private static String DRIVER;
    private static String URL;
    private static String USER_NAME;
    private static String USER_PASSWORD;
    private static DruidDataSource ds;

    //2.提供静态代码块。读取配置文件的信息为变量赋值,注册驱动
    static{
        try {
            //读取配置文件的信息为变量赋值
            InputStream is = DBUtils.class.getClassLoader().getResourceAsStream("jdbc.properties");
            Properties prop = new Properties();
            prop.load(is);
            DRIVER = prop.getProperty("driverClass");
  URL = prop.getProperty("url");
            USER_NAME = prop.getProperty("username");
            USER_PASSWORD = prop.getProperty("password");

            //注册驱动
            Class.forName(DRIVER);
            ds = new DruidDataSource();
            // 基础配置
            ds.setUrl(URL);
            ds.setUsername(USER_NAME);
            ds.setPassword(USER_PASSWORD);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    //3.提供获取数据库连接方法
    public static Connection getConn() {
        try {
            return  ds.getConnection();
        } catch (SQLException e) {
            e.printStackTrace();
            System.out.println("创建连接对象异常");
        }
        return null;
    }

    //4.提供释放资源的方法
    public static void close(AutoCloseable closeable) {
        try {
            if (closeable != null) {
                closeable.close();
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

总结

一、JDBC 核心概念

JDBC 是 Java 数据库连接的标准 API,提供了一套访问关系型数据库的统一接口。其核心价值在于"面向接口编程"------SUN 公司定义规范,数据库厂商提供具体驱动实现,实现了 Java 程序与数据库的解耦。

二、关键组件与流程

  1. DriverManager:驱动管理类,负责加载驱动并建立连接

  2. Connection:数据库连接对象,代表物理连接会话

  3. Statement/PreparedStatement:SQL 执行接口,后者支持预编译防止 SQL 注入

  4. ResultSet:结果集对象,以游标方式遍历查询结果

  5. 标准操作流程:加载驱动→建立连接→创建 Statement→执行 SQL→处理结果→关闭资源

三、核心技术要点

  1. SQL 注入防御:使用 PreparedStatement 的预编译机制,参数化查询确保安全

  2. 事务管理:通过 setAutoCommit(false) 开启事务,commit()/rollback() 保证数据一致性

  3. 连接池优化:解决频繁创建销毁连接的性能问题,Druid 等工具提供连接复用、监控功能

  4. 资源管理:遵循"后开先关"原则,确保 Connection、Statement、ResultSet 正确释放

四、架构设计演进

  1. DBUtils 封装:提取公共代码,实现配置外部化,提高代码复用性

  2. BaseDAO 抽象:通过反射+泛型实现基础 CRUD 操作的通用封装,初步体现 ORM 思想

  3. 分页查询:通过 LIMIT 子句实现数据库端分页,提升大数据量查询效率

五、实践价值

学习 JDBC 不仅掌握了数据库操作的基础技能,更重要的是理解了数据访问层的设计原理。从原生 JDBC 到连接池优化,再到 DAO 模式封装,体现了软件开发中"抽象封装、关注分离、性能优化"的核心思想。这为后续学习 MyBatis、Hibernate 等持久层框架奠定了坚实基础,也培养了编写安全、高效、可维护数据访问代码的能力。

相关推荐
猿来是泥鸭2 小时前
Spring IOC 实现机制
java
80530单词突击赢2 小时前
MPPI算法:ROS下的智能控制实战
开发语言·python
JSON_L2 小时前
使用 SQLite 创建数据库和表
数据库·sqlite·php
qinyia2 小时前
如何在服务器上查看网络连接数并进行综合分析
linux·运维·服务器·开发语言·人工智能·php
小满zs2 小时前
Next.js第二十五章(CSS方案)
开发语言·javascript·css
m0_706653232 小时前
自然语言处理(NLP)入门:使用NLTK和Spacy
jvm·数据库·python
m0_736919102 小时前
Python游戏中的碰撞检测实现
jvm·数据库·python
猴哥聊项目管理2 小时前
2026年免费项目管理工具,支持任务分配+甘特图+协作 推荐
大数据·数据库·甘特图·项目管理工具·项目管理软件·免费项目管理软件·研发项目管理软件
wuhen_n2 小时前
JavaScript事件循环(下) - requestAnimationFrame与Web Workers
开发语言·前端·javascript