JDBC基础整理(快速入门、自定义链接池)

JDBC是什么

Java数据库链接(Java DataBase Connectivity)

是Java提供的一套用于链接、操做数据库的一套接口规范,即便用Java语言来操做数据库java

为何须要有JDBC

就像车子跑起来须要发动机的驱动,显卡等电脑硬件想运行起来须要显卡驱动,使用Java语言操做相应的数据库也须要相应的驱动mysql

但存在一个问题,数据库的种类很是繁多,会致使相应的数据库驱动也很繁多,不一样数据库的API又会存在很大的差异
为了操做不一样数据库而去学习不一样数据库的API,对于开发人员而言,无疑增长了不少没必要要的学习成本sql

好在Java官方提供了JDBC接口规范,数据库厂商须要实现该接口来编写各自的数据库驱动
这样对于开发人员而言只须要熟悉JDBC API就能操做各种厂商的数据库了,即面向接口编程数据库

交互的结构

图片来自于百度编程

JDBC的组成

JDBC主要由java.sql以及javax.sql两个包组成
能够先简单了解如下对象,更多详细的描述能够查看JDK API文档
java.sql.DriverManager 驱动的管理类,用于数据库驱动的注册
java.sql.Connection 数据库链接接口,处理客户端与数据库之间的全部交互
java.sql.Statement 语句接口,封装了要向数据库操做的SQL语句信息
java.sql.ResultSet 结果集接口,封装了查询语句返回的结果信息服务器

JDBC快速入门

以操做MySQL数据库为例,先来看一段最简单的JDBC操做数据库的代码,而后再根据代码了解相应的步骤session

// 注册驱动
Class.forName("com.mysql.cj.jdbc.Driver");
// 创建链接
Connection connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/mysql?characterEncoding=UTF-8", "root", "123456");
// 建立语句
Statement statement = connection.createStatement();
// 执行查询
ResultSet resultSet = statement.executeQuery("select * from user");
// 遍历结果
while(resultSet.next()) {
    String host = resultSet.getString("Host");
    String user = resultSet.getString("User");
    System.out.println(host + ":" + user);
}
// 释放资源
resultSet.close();
statement.close();
connection.close();

控制台打印的查询结果并发

localhost:root
localhost:mysql.session
localhost:mysql.sys
%:root

JDBC操做数据库的步骤

1. 注册驱动

Maven引入驱动依赖

上述代码既然是以MySQL数据库为例,天然须要在项目中引入MySQL驱动的Maven构件,当前最新版本为8.0.12
为了方便写测试代码,引入Junit的Maven构件,当前最新版本为4.12oracle

<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>LATEST</version>
</dependency>
<dependency>
    <groupId>junit</groupId>
    <artifactId>junit</artifactId>
    <version>LATEST</version>
</dependency>

com.mysql.jdbc.Driver与com.mysql.cj.jdbc.Driver之间的区别

为了操做数据库,须要先获取到实现了java.sql.Driver接口的驱动
MySQL驱动包提供了com.mysql.jdbc.Drivercom.mysql.cj.jdbc.Driver两个数据库驱动类ide

com.mysql.jdbc.Driver是5.x版本的驱动中的实现类,已通过时
com.mysql.cj.jdbc.Driver是6.x及以上版本的驱动中的实现类

com.mysql.jdbc.Driver代码的静态代码块中描述了该驱动实现类已通过时

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

DriverManager方法注册驱动

有了数据库厂商提供的驱动后,要先在程序中注册加载驱动
java.sql.DrvierManager提供了注册驱动的方法

public static void registerDriver(java.sql.Driver driver)
public static void registerDriver(java.sql.Driver driver, DriverAction da)

注册驱动须要传入java.sql.Driver接口的实现,传入com.mysql.jdbc.Driver驱动进行注册

DriverManager.registerDriver(new com.mysql.cj.jdbc.Driver())

Class.forName反射注册驱动

但经过对com.mysql.cj.jdbc.Driver代码的查看,发现类中静态代码块已经对当前驱动进行了注册,会形成二次注册
因此开发人员不须要手动去注册MySQL驱动,只要让JVM加载com.mysql.cj.jdbc.Driver或其子类便可完成驱动的注册

static {
    try {
        java.sql.DriverManager.registerDriver(new Driver());
    } catch (SQLException E) {
        throw new RuntimeException("Can't register driver!");
    }
}

那最经常使用的方式是经过反射直接加载目标Class

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

且这种方式不一样于传入对象的硬编码,字符串的方式在更换数据库的时候也更方便

2. 创建链接

加载并注册完驱动后就能够创建起链接来实现对数据库的访问了
java.sql.DriverManager提供了三种方法获取Connection对象,客户端与数据库的全部交互都经过该对象完成

public static Connection getConnection(String url, java.util.Properties info)
public static Connection getConnection(String url, String user, String password)
public static Connection getConnection(String url)

最经常使用的方式是经过URL以及数据库帐号密码来获取链接,如今链接本地MySQL下名为mysql的数据库并设置编码属性为UTF-8

DriverManager.getConnection("jdbc:mysql://localhost:3306/mysql?characterEncoding=UTF-8", "root", "123456")

URL链接的组成与写法

MySQL写法:jdbc:mysql://localhost:3306/databaseName
MySQL本地默认端口号简写:jdbc:mysql:///databaseName
Oracle写法:jdbc:oracle:thin:@localhost:1521:databaseName

主要由jdbc协议、相应数据库的子协议以及主机名、端口号和数据库名称构成
其中thin为oracle数据库的一种驱动方式,对应的还有oci方式

3. 操做数据

链接到本地的MySQL数据库后开始对数据库进行操做
操做数据库要经过SQL语句来操做,首先要经过connection对象建立语句对象来实现对数据库的SQL操做

Statement statement = connection.createStatement();

Statement接口提供了execute、executeQuery、executeUpate、addBatch、executeBatch等方法来执行SQL

  • execute:执行任意的SQL语句
  • executeQuery:执行select语句
  • executeUpdate:执行insert、update、delete或SQL DDL等语句
  • addBatch:添加多条SQL语句到批处理中
  • clearBatch:清除批量SQL语句
  • executeBatch:批量执行SQL语句

查询的结果封装在ResultSet对象中,即存放告终果对象的一个容器
ResultSet对象提供了next方法来移动容器中的指针,相似于集合中迭代器的hasNext方法
经过循环判断就能够遍历拿到每一个结果对象的值

ResultSet resultSet = statement.executeQuery("select * from user");
while(resultSet.next()) {
    String host = resultSet.getString("Host");
    String user = resultSet.getString("User");
    System.out.println(host + ":" + user);
}

数据库字段类型与JDBC方法对应表

数据库字段有不一样的类型,ResultSet对象能够经过不一样的方法获取不一样类型的数据

MySQL字段类型 ResultSet对应方法 方法返回类型
BIT(1) getBoolean(String) boolean
BIT getBytes(String) byte[]
TINYINT getByte(String) byte
SMALLINT getShort(String) short
INT getInt(String) int
BIGINT getLong(String) long
CHAR、 VARCHAR getString(String) java.lang.String
TEXT、BLOB getClob(String) getBlob(String) java.sql.Clob java.sql.Blob
DATE getDate(String) java.sql.Date
TIME getTime(String) java.sql.Time
TIMESTAMP getTimestamp(String) java.sql.Timestamp

ResultSet还提供了其余不少方式来获取字段的值
例如getObject(int index)、getObject(String columnName)分别根据字段位置和字段名称来获取任意类型的数据
更多相关的方法能够查看JDK API找到相应的类了解

4. 释放资源

操做完数据库后须要释放资源,依次断开对数据库的操做和链接

传统close方法手动释放资源

传统的close方式必需要在finally中编写确保资源必定会被释放
但代码相对而言比较重复繁琐

@Test
public void closeResource() {
    Connection connection = null;
    Statement statement = null;
    ResultSet resultSet = null;
    try {
        // 省略部分代码
        // ......
    } catch (ClassNotFoundException | SQLException e) {
        e.printStackTrace();
    } finally {
        // 释放资源
        if (resultSet != null) {
            try {
                resultSet.close();
            } catch (SQLException e) {
                e.printStackTrace();
            }
            resultSet = null;
        }
        if (statement != null) {
            try {
                statement.close();
            } catch (SQLException e) {
                e.printStackTrace();
            }
            statement = null;
        }
        if (connection != null) {
            try {
                connection.close();
            } catch (SQLException e) {
                e.printStackTrace();
            }
            connection = null;
        }
    }
}

AutoCloseable自动释放资源

JDK7开始提供了AutoCloseable接口,该接口的主要功能是帮助开发人员自动释放资源
ResultSet、Statement、Connection接口都继承了AutoCloseable接口
使用AutoCloseable接口管理资源须要使用JDK7的try-catch-resources语法
建立的资源在退出`try-block代码块时会自动调用该资源的close方法,释放的顺序为先建立后释放

@Test
public void autoCloseable() {
    // 省略部分代码
    // ......
    try {
        Class.forName(driverClass);
        // try-catch-resources语法的try-block代码块
        try (Connection connection = DriverManager.getConnection(url, username, password);
            Statement statement = connection.createStatement();
            ResultSet resultSet = statement.executeQuery(sql)) {
            while(resultSet.next()) {
                String host = resultSet.getString("Host");
                String user = resultSet.getString("User");
                System.out.println(host + ":" + user);
            }
        }
    } catch (ClassNotFoundException | SQLException e) {
        e.printStackTrace();
    }
}

为何要释放资源

数据库是部署在服务器上的,服务器有着相应的硬件配置
程序对数据库的操做与链接都会占用服务器的CPU、内存等硬件资源
当程序处于空闲状态,对数据库没有任何操做时,应及时释放资源好让数据库能分配其余程序资源
资源的过多占用可能会致使服务器宕机中止工做而致使严重的后果
说的直白一点就是不要上完了厕所还要一直霸占着坑位

JDBC工具类封装

经过上述代码的流程,能够发现每次使用JDBC操做数据库都要先注册驱动、创建链接而后再操做数据库、最后释放资源
其中注册驱动、创建链接以及释放资源都是重复的,能够封装一个工具类来消除这种重复的编码操做
使开发人员只须要关注SQL的编写以及对结果的处理

public class JDBCUtil {

    private static final String DRIVERCLASS;
    private static final String URL;
    private static final String USERNAME;
    private static final String PASSWORD;

    static{
        Properties pro = new Properties();
        // 经过ClassLoader类加载器从classpath路径下加载配置了数据库信息的属性文件
        InputStream in = JDBCUtil.class.getClassLoader().getResourceAsStream("db.properties");
        try {
            pro.load(in);
        } catch (IOException e) {
            e.printStackTrace();
        }
        DRIVERCLASS = pro.getProperty("driverClass");
        URL = pro.getProperty("url");
        USERNAME = pro.getProperty("username");
        PASSWORD = pro.getProperty("password");
    }
    
    // 注册驱动
    private static void loadDriver(){
        try {
            Class.forName(DRIVERCLASS);
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
    }
    
    // 获取链接
    public static Connection getConnection(){
        // 加载驱动
        loadDriver();
        Connection conn = null;
        try {
            conn = DriverManager.getConnection(URL, USERNAME , PASSWORD);
        } catch (SQLException e) {
            e.printStackTrace();
        }
        return conn;
    }
}

在代码中直接编写数据库配置信息的硬编码方式显然是不利于维护与修改的
因此将配置信息编写在classpath路径下的db.properties文件中
再经过类加载器读取文件获取文件中的键值对,完成驱动的注册与链接的创建,资源的释放则经过try-catch-resources实现

@Test
public void jdbcUtil() {
    String sql = "select * from user";
    try (Connection connection = JDBCUtil.getConnection();
         Statement statement = connection.createStatement();
         ResultSet resultSet = statement.executeQuery(sql)) {
        while(resultSet.next()) {
            String host = resultSet.getString("Host");
            String user = resultSet.getString("User");
            System.out.println(host + ":" + user);
        }
    } catch (SQLException e) {
        e.printStackTrace();
    }
}

结合封装的JDBC工具类以前的代码能获得这样进一步的精简

为何须要数据库链接池

JDBCUtil存在的问题:
每次访问数据库都要从新创建链接,而创建链接是很是耗时的,当程序频繁访问数据库会形成程序性能的降低

解决的方案:
在一个容器中初始化必定数量的数据库链接,程序每次访问数据库直接从容器中获取到链接,执行完对数据库的操做后再把链接归还到容器中,这个容器便称之为数据库链接池

自定义一个最简单的链接池

Java官方提供了数据库链接池的接口规范,实现javax.sql.DataSource接口来编写链接池

public class DataSourceUtil implements DataSource {

    /**
     * 存放链接的容器
     */
    private List<Connection> connectionPool = new ArrayList<>();
    
    public DataSourceUtil() {
        addConnection();
    }

    /**
     * 初始化链接池
     */
    private void addConnection() {
        // 初始化10个链接
        for (int i = 0; i < 10; i++) {
            connectionPool.add(JDBCUtil.getConnection());
        }
    }

    /**
     * 从链接池中获取链接
     * @return 链接对象
     */
    @Override
    public Connection getConnection() {
        // 若是链接池空了,对链接池进行扩容
        if (connectionPool.isEmpty()) {
            addConnection();
        }
        return connectionPool.remove(0);
    }

    /**
     * 归还链接到链接池中
     * @param connection 链接对象
     */
    public void closeConnection(Connection connection) {
        connectionPool.add(connection);
    }
    
    // ......省略部分代码
}

使用该链接池进行数据库操做测试

@Test
public void dataSourceUtil() {
    String sql = "select * from user";
    DataSourceUtil dataSourceUtil = new DataSourceUtil();
    Connection connection = dataSourceUtil.getConnection();
    try (Statement statement = connection.createStatement();
        ResultSet resultSet = statement.executeQuery(sql)) {
        while(resultSet.next()) {
            String host = resultSet.getString("Host");
            String user = resultSet.getString("User");
            System.out.println(host + ":" + user);
        }
    } catch (SQLException e) {
        e.printStackTrace();
    } finally {
        // 使用DataSourceUtil手动归还链接
        dataSourceUtil.closeConnection(connection);
    }
}

本身写的数据源毕竟是比较简单的,并不会涉及到方方面面的问题
好在有各类强大的开源的链接池能够供平时开发时使用,例如hikari、dbcp、c3p0等经常使用数据源

关于滚动结果集

使用JDBC查询数据库会返回ResultSet对象,默认经过Statement createStatement()方法建立执行返回的结果集是只能向下滚动且只读的
使用Statement createStatement(int resultSetType, int resultSetConcurrency)来指定结果集的类型以及策略

经常使用结果集类型
resultSetType
    TYPE_FORWARD_ONLY          结果集只能向下
    TYPE_SCROLL_INSENSITIVE    能够滚动,不能修改记录
    TYPE_SCROLL_SENSITIVE      能够滚动,能够修改记录
    
经常使用结果集并发策略
resultSetConcurrency
    CONCUR_READ_ONLY           只读,不能修改
    CONCUR_UPDATABLE           结果集能够修改

另外ResultSet还提供了不少的方法来对结果集内的指针进行操做

next()                         移动到下一行
previous()                     移动到前一行
absolute(int row)              移动到指定行
beforeFirst()                  移动到resultSet最前面
afterLast()                    移动到resultSet最后面
updateRow()                    更新行数据

编写测试例子查询结果集中第四行的数据

@Test
public void resultSet() {
    String sql = "select * from user";
    try (Connection connection = JDBCUtil.getConnection();
         Statement statement = connection.createStatement(ResultSet.TYPE_SCROLL_SENSITIVE, ResultSet.CONCUR_UPDATABLE);
         ResultSet resultSet = statement.executeQuery(sql)) {
        resultSet.absolute(4);
        String host = resultSet.getString("Host");
        String user = resultSet.getString("User");
        System.out.println(host + ":" + user);
    } catch (SQLException e) {
        e.printStackTrace();
    }
}

执行结果

%:root

关于SQL注入

使用Statement对象执行SQL语句是直接采用拼接字符串的方式,会致使SQL注入的危害
例如登陆校验用户是否存在的简单SQL注入,SQL以下

String sql = "select * from user where username = ' + username + ' and password = ' + password + ' ";

若是用户传入的username 为xxx ' or ' 1 = 1,SQL将会变成

String sql = "select * from user where username = 'xxx' or '1 = 1' and password = ''";

这样致使表达式username = 'xxx' or '1 = 1'结果老是为true,因此无论密码填什么都无所谓了
执行这样的SQL将致使用户并无输入正确的帐号密码却经过了验证进入到了系统

使用PreparedStatement防止SQL注入

Statement会使数据库频繁编译SQL,可能形成数据库缓冲区溢出
PreparedStatement对象支持SQL预编译,还能经过占位符来管理变量从而防止SQL注入
此时username再传入xxx ' or ' 1 = 1,程序会将其当作总体,在数据库查找username为"xxx ' or ' 1 = 1"的用户

String sql = "select * from user where username = ? and password = ?";
PreparedStatement preparedStatement = connection.prepareStatement(sql);
// 设置第一个、第二个占位符的值
preparedStatement.setString(1, user.getUsername());
preparedStatement.setString(2, user.getPassword());
相关文章
相关标签/搜索