JDBC

  • JDBC: Java Database Connectivity, 是Java程序与数据库连接的中间件

  • JDBC封装了与各种数据库管理系统交互的方法

JDBC使用过程

  1. 准备jar包并导入
  2. 注册驱动
  3. 建立连接(创建Connection对象)
  4. 创建Statement对象
  5. 执行SQL, 获取结果集
  6. 处理结果集中的数据
  7. 释放资源(关闭Statement对象, 关闭Connection对象)

常见的JDBC组件

  • DriverManager: 驱动管理器, 管理数据库驱动
  • Driver: 数据库驱动
  • Connection: 程序与数据库之间的连接
  • Statement: SQL语句声明, 用于SQL语句的执行
  • ResultSet: 执行SQL语句之后的结果集
  • SQLException: 执行SQL操作过程中遇到的异常类

JDBC程序实例

注册驱动

  • 使用DriverManager进行驱动注册, 这个方法的参数是一个java.sql.Driver的接口类型, 在导入的JDBC的jar包中有一个类已经实现了这个接口, 因此注册JDBC驱动需要这个实现类的对象 com.mysql.jdbc.Driver
  • 注意: MySQL 6.x版本开始, 改用 com.mysql.cj.jdbc.Driver
DriverManager.registerDriver(new com.mysql.cj.jdbc.Driver());
  • 虽然标准的注册应该这样做, 但是在com.mysql.cj.jdbc.Driver 这个类中已经实现了驱动注册, 因此没有必要重复注册, 只需要触发静态代码段
        try {
            Class.forName("com.mysql.cj.jdbc.Driver");
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }

建立连接

  • getConnection(String url, String user, String password)
    url: 统一资源定位符, 表示一个数据库的路径(格式: "jdbc:数据库管理系统名称://主机名:端口号/数据库名")
    user: 登录数据库的用户名
    password: 登录数据库的密码
  • JDBC连接MySQL 6.x以上版本还需要指定时区(serverTimezone, GMT即可)
        Connection connection = null;
        try {
            connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/second?serverTimezone=GMT", "root", "2018WH");
        } catch (SQLException e) {
            e.printStackTrace();
        } finally {
            // 6. 关闭连接
            if (connection != null) {
                try {
                    connection.close();
                } catch (SQLException e) {
                    e.printStackTrace();
                }
            }
        }
  • 重载方法getConnection(String url)
    url: 统一资源定位符, 但需要将用户名和密码都拼接到url中
connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/second?user=root&password=2018WH");
  • 建立配置文件(.properties)

    实现数据与代码分离

    user=root
    password=2018WH
    url=jdbc:mysql://localhost:3306/数据库名?serverTimezone=GMT
    driverClass=com.mysql.cj.jdbc.Driver
    
            try {
                Connection connection = DriverManag
                Class.forName("com.mysql.cj.jdbc.Driver");
            } catch (ClassNotFoundException e) {
                e.printStackTrace();
            }
            Connection connection = null;
            try {
                connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/second?serverTimezone=GMT", "root", "2018WH");
            } catch (SQLException e) {
                e.printStackTrace();
            } finally {
                // 6. 关闭连接
                if (connection != null) {
                    try {
                        connection.close();
                    } catch (SQLException e) {
                        e.printStackTrace();
                    }
                }
            }
    

创建Statement对象

        try {
            // 3. 创建Statement对象
            statement = connection.createStatement();
        } catch (SQLException e) {
            e.printStackTrace();
        } finally {
            // 6. 关闭SQL声明Statement
            if (statement != null) {
                try {
                     statement.close();
                } catch (SQLException e) {
                     e.printStackTrace();
                }
            }
        }

执行SQL

String sql = "insert into t_employee (e_id, e_name, e_age, e_salary) values (7, '小楠', 5, 13000)";
  • 执行DDL, DML都使用executeUpdate()
  • 执行DQL, 使用executeQuery()
  • executeUpdate()返回int类型, 表示SQL语句对几行数据产生影响
        ResultSet set = null;
        try {
            
            String sql = "select * from t_employee";

获取结果集

            set = statement.executeQuery(sql);
  • 遍历set获取每行数据(类似于迭代器)
  • set.getInt(int columnIndex) 通过列索引获取值(索引从1开始)
  • set.getInt(String columnLabel) 通过列标签获取值
            while (set.next()) {  // 判断是否还有下一行
                int id = set.getInt(1);
                String name = set.getString(2);
                int age = set.getInt(3);
                int salary = set.getInt(4);
                System.out.printf("|%02d|%5s|%2d|%5d|\n", id, name, age, salary);
                System.out.println("-------------------");
            }
        }
        finally {
            // 6. 关闭ResultSet(如果set不关闭则执行不了其他操作)
            if (set != null) {
                try {
                    set.close();
                } catch (SQLException e) {
                    e.printStackTrace();
                }
            }
        }
  • ResultSet.next()

    如果有下一行数据就返回true并指针下移

    如果没有下一行数据就返回false且指针不移动

  • 获取结果集的元数据

    通过元数据的个数获取字段个数

    ResultSetMetaData metadata = resultset.getMetaData();
    

    获取结果集列数(字段数)

    int columnCount = metadata.getColumnCount();
    

数据访问模型(DAO)

  • Data Access Object: 将程序中用来访问数据库中的数据封装成模块

程序设计的原则

  • 可复用性: 减少重复的代码
  • 可拓展性: 当程序需要添加新的功能, 只需要在现有结果上添加模块, 不需要改动现有逻辑
  • 可维护性: 当需求变更时候, 能用最少的修改实现变更

DAO层的设计

DAO包的设计规范
  • 一个DAO包由DAO接口, DAO实现类和描述类组成
  • 在开发过程中还需要测试类
DAO包的命名规范
  • 主包: com.包名.项目名.DAO
    • domain包(存放描述类): com.包名.项目名.DAO.domain
    • DAO包(存放DAO接口): com.包名.项目名.DAO.dao
    • impl包(存放DAO接口实现类): com.包名.项目名.DAO.dao.impl
    • test包(存放测试类): com.包名.项目名.DAO.test
接口和实现类命名规范
  • 接口: 以I开头, 以DAO结尾, 中间部分是访问的domain类名字
  • 实现类: 以Impl结尾

DAO设计实例

补充

下面测试说明

子类对象创建时会默认调用父类构造方法, 并且父类构造方法里的this对象指的也是子类对象

并且父类构造先执行

public class NormalTest {
    @Test
    public void test() {
        new B();
    }
}

class A {
    public A() {
        System.out.println(this.getClass());  //(1)
        System.out.println(this.getClass().getSuperclass());  //(2)
    }
}

class B extends A {
    public B() {
        System.out.println(this.getClass());  //(3)
    }
}

输出结果:

class Gogoing.Tests.B

class Gogoing.Tests.A

class Gogoing.Tests.B

为了子类对象生成时, 动态获取到父类类型

父类要定义一个Class类型对象cls

并在父类构造方法中给cls赋值

此时不要构造父类对象, 而用子类对象继承, 且子类必须标明继承父类的泛型类型

public class NormalTest {
    @Test
    public void test() {
        new B();

    }
}

class A {
    private Class cls;

    public A() {
        Type genericSuperclass = this.getClass().getGenericSuperclass();
        ParameterizedType paramType = (ParameterizedType) genericSuperclass;
        Type[] typeArgs = paramType.getActualTypeArguments();
        cls = (Class) typeArgs[0];
        System.out.println(cls);
    }
}

class B extends A {
    public B() {
        System.out.println(this.getClass());
    }
}

输出结果:

class java.lang.String

class Gogoing.Tests.B

预编译

  • Statement存在的弊端

    存在SQL注入问题

    SQL注入问题是利用某些系统没有对用户输入的数据进行充分的检查

    而在用户输入数据中注入非法的SQL语句段或命令

    SELECT `user`, `password`
    FROM 表
    WHERE `user` = 'a' OR 1 = 'AND password = 'OR'1' = '1';
    

    对于Java而言, 要防范SQL注入

    则使用PreparedStatement取代Statement

  • PrepareStatement

    DBServer会对预编译的语句提供性能优化

    预编译语句有可能重复利用, 语句在被DBServer编译器编译后的执行代码被缓存下来

    下次调用相同预编译语句时就不需要编译, 只需要将参数直接传入编译过的语句执行代码

    • 优点
      • 使用预编译, 提高SQL语句
      • 一定程度提高SQL的执行效率(预编译, MySQL不支持, Oracle支持)
      • 保证数据安全性
  • PreparedStatement与Statement

    • 关系: 接口与子接口
    • 开发中使用PreparedStatement
    • PreparedStatement提高性能
    • PreparedStatement防止SQL注入
  • 过程

    • 创建预编译指令(需要制定一个SQL)
    • 使用在预编译指定中的SQL可以直接用?做占位符
    • 给每一个占位符设定值
// 创建预编译指令(需要制定一个SQL)
String sql = "insert into t_employee values (?, ?, ?, ?)";
PreparedStatement statement = connection.prepareStatement(sql);
// 给每一个占位符设定值 (index, value)
// 第一个参数是表示第几个字段(从1开始)
// 第二个参数是表示该字段的值
statement.setInt(1, 11);
statement.setString(2, "Xing");
statement.setInt(3, 38376);
statement.setInt(4, 3);
// 执行SQL
int rows = statement.executeUpdate();
// 如果还有其他值添加, 则直接再进行每一个占位符的赋值即可
statement.close();
connection.close();

过程完整代码

增删改操作

    public void update(String sql, Object... args) {
        Connection connection = null;
        PreparedStatement statement = null;
        try {
            connection = JDBCUtils.getConnection();
            statement = connection.prepareStatement(sql);
            // 填充占位符
            for (int i = 0; i < args.length; i++) {
                statement.setObject(i + 1, args[i]);
            }
            statement.execute();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            JDBCUtils.closeResource(connection, statement);
        }
    }

查操作

    public  T getInstance(Class cls, String sql, Object... args) {
        Connection connection = null;
        PreparedStatement statement = null;
        ResultSet set = null;
        try {
            connection = JDBCUtils.getConnection();
            statement = connection.prepareStatement(sql);
            for (int i = 0; i < args.length; i++) {
                statement.setObject(i + 1, args[i]);
            }
            set = statement.executeQuery();
            ResultSetMetaData metaData = set.getMetaData();
            int columnCount = metaData.getColumnCount();
            if (set.next()) {
                T t = cls.newInstance();
                for (int i = 0; i < columnCount; i++) {
                    String columnLabel = metaData.getColumnLabel(i + 1);
                    Object columnValue = set.getObject(i + 1);
                    Field field = t.getClass().getDeclaredField(columnLabel);
                    field.setAccessible(true);
                    field.set(t, columnValue);
                }
                return t;
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            JDBCUtils.closeResource(connection, statement, set);
        }
        return null;
    }

事务

一组逻辑操作单元使数据从一种状态变换到另一种状态(一个或多个DML操作)

自动提交

数据一旦提交, 就不可以回滚

  • DDL操作一旦执行, 就会自动提交, 且不能修改自动提交的设置

  • DML默认一旦执行, 就会自动提交, 但可以修改自动提交的设置

    connection.setAutoCommit(false);
    
  • 默认情况下关闭连接时会自动提交数据

批处理任务

  • 将若干功能相关的SQL语句放到一个批处理分组(Batch)中, 批量执行分组中的SQL
  • 每次调用executeUpdate()时, JDBC都与数据库进行交互花费较多时, 因此将若干任务放到一个分组, 则只需要和数据库进行一次交互
            connection = JDBCUtils.getConnection();
            // 设置不允许自动提交
            connection.setAutoCommit(false);
            String sql = "INSERT INTO t_employee (e_id) VALUES (?)";
            statement = connection.prepareStatement(sql);
            for (int i = 1; i <= 20000; i++) {
                statement.setInt(1, 78 + i);
                // "攒"SQL到Batch中
                statement.addBatch();
                // 每隔500次
                if (i % 500 == 0) {
                    // 执行一次Batch中的SQL
                    statement.executeBatch();
                    // 清空batch
                    statement.clearBatch();
                }
            }
            // 手动提交
            connection.commit();
步骤
  1. 关闭自动提交功能(setAutoCommit(false);)
  2. 将需要批量执行的任务添加到批处理任务分组中: statement.addBatch(sql);
  3. 批量执行这个分组中所有的任务: statement.executeBatch();
  4. 将变更提交到数据库(因为第一步关闭了自动提交): commit

概述

  • 由若干个SQL组成的一个执行单元, 一个事务中的任务要么同时成功, 要么同时失败
  • 并没有一个类描述事务,事务就是一个逻辑代码段
try {
    // 3. 关闭自动提交, 意味着开启了一个事务
    connection.setAutoCommit(false);
    // 4. 将多个操作放到一个事务中
    statement.executeUpdate("insert into t_employee values (18, 'Ding', 3674, 3)");
    statement.executeUpdate("insert into t_employee values (19, 'Aing', 3674, 3)");
    statement.executeUpdate("insert into t_employee values (20, 'Bing', 3674, 3)");
    statement.executeUpdate("insert into t_employee values (21, 'Cing', 3674, 3)");
    statement.executeUpdate("insert into t_employee values (22, 'Eing', 3674, 3)");
    // 5. 手动将事务提交
    connection.commit();
} catch (SQLException e) {
    e.printStackTrace();
    // 如果一组事务中存在失败的任务, 则一旦失败, 将事务回滚到初始状态(撤销变更)
    // 保证一组事务的多个任务同时成功或失败
    // 回滚到事务开启的状态
    try {
        connection.rollback();
    } catch (SQLException ex) {
        ex.printStackTrace();
    }
}

起点

  • 连接到数据库, 并执行一条DML语句
  • 上一个事务执行结束, 又执行了一条DML语句

终点

  • 执行了commit()或rollback()
  • 执行了一条DDL语句, 此时会自动提交上一个事务
  • 断开数据库的连接
  • 执行了一条DML语句, 但是语句出错, 此时会为这条语句执行rollback()

特点(事务的ACID属性)

  • 原子性(Atomicity)

    事务中的所有操作同时成功或同时失败

  • 一致性(Consistency)

    事务必须使数据库从一个一致性状态变换到另一个一致性状态

    比如: 转账问题, 转账双方总金额不变

    事务中的某个任务失败则会回滚到修改之前的状态

  • 隔离性(Isolation)

    一个事务的执行不会也不能被其他事务干扰

    并发的各个事务的内部的操作及数据的使用都是不互相影响

    一个事务查看数据的时候, 查看的要么是另一个事务所有任务修改前的数据, 要么是修改后的数据

  • 持久性(Durability)

    事务一旦被提交, 对数据库的影响则是永久的, 接下来的其他操作和数据库鼓掌都不会对其造成影响

代码示例

  • 将事务中的两个DML封装在方法update(connection, sql, args)中

  • update()方法中的连接不要在方法中创建, 改为传参数

    一个事物的两个DML, 要共用一个连接对象, 并且执行完两个DML才关闭

    所以不要单独创建连接对象

    public void update(Connection connection, String sql, Object... args) {
        PreparedStatement statement = null;
        try {
            statement = connection.prepareStatement(sql);
            for (int i = 0; i < args.length; i++) {
                statement.setObject(i + 1, args[i]);
            }
            statement.execute();
        } catch (SQLException e) {
            e.printStackTrace();
        } finally {
            JDBCUtils.closeResource(null, statement);
        }
    }
  • 过程
    1. 取消自动提交
    2. 执行两个DML语句后提交
    3. 如果捕捉到异常要回滚
  • 关闭资源之前, 把自动提交设置为true, 使用线程池时必须这么做
    @Test
    public void testUpdateWithTx() {
        Connection connection = null;
        try {
            connection = JDBCUtils.getConnection();
            // 取消自动提交
            connection.setAutoCommit(false);
            String sql1 = "UPDATE t_employee SET e_salary = e_salary + 100 WHERE e_id = ?";
            String sql2 = "UPDATE t_employee SET e_salary = e_salary - 100 WHERE e_id = ?";
            update(connection, sql1, 4);
//            System.out.println(1 / 0);
            update(connection, sql2, 3);
            // 提交数据
            connection.commit();
        } catch (Exception e) {
            try {
                // 出现异常则回滚数据
                connection.rollback();
            } catch (Exception ex) {
                ex.printStackTrace();
            }
            e.printStackTrace();
        } finally {
            try {
                connection.setAutoCommit(true);
            } catch (SQLException e) {
                e.printStackTrace();
            }
            JDBCUtils.closeResource(connection, null);
        }
    }
  • 关于隔离级别的操作代码

    • 设置隔离级别
    connection.setTransactionIsolation(Connection.TRANSACTION_READ_COMMITTED);
    
    • 查看隔离级别
    connection.getTransactionIsolation();
    

JDBC传统模式开发存在的问题

  • 普通的JDBC数据库连接使用DriverManager来获取, 每次向数据库建立连接时将Connection加载到内存中, 再验证用户名和密码, 需要数据库连接时就像数据库请求一个, 执行完再断开连接, 这种方式消耗大量资源和时间, 数据库的连接资源得不到很好的重复利用, 若同时发生海量的数据库连接请求, 可能会造成服务器崩溃
  • 对于每一次连接, 使用完都得断开连接, 否则如果程序出现异常未能关闭, 将导致数据库管理系统的内存泄漏, 导致重启数据库
  • 不能控制被创建的连接对象数, 系统资源会被毫无顾忌的分配出去, 连接过多也会导致内存泄漏, 服务器崩溃

数据库连接池

  • 基本思想: 为数据库连接建立一个"缓冲池", 预先在"缓冲池"中放入一定数量的连接, 需要建立连接时, 从"缓冲池"中取出一个使用完毕后再放回去

  • 数据库连接池负责分配, 管理, 释放数据库连接, 允许程序重复使用一个现有的连接, 而不是重新建立一个

  • 数据库连接池在初始化时将创建一定数量的数据库连接, 并放入池中

    这些数据库连接的数量是由最小数据库连接数来设定的, 无论这些数据库连接是否被使用, 连接池都将一直保证至少拥有这么多数量的连接

    连接池的最大数据库连接数限定了这个连接池能占有的最大连接数, 当程序向连接池请求获取连接数超过最大连接数时, 这些请求将被加入等待队列

使用连接池的好处

  1. 实现资源重用

    避免频繁创建, 释放连接引起大量性能开销, 减少系统消耗同时增加系统运行稳定性

  2. 提高系统反应速度

    数据库连接池初始化, 创建了若干数据库连接于池中备用, 对于业务请求而言, 直接利用现有连接, 避免数据库连接初始化和释放的时间开销, 从而减少系统反应时间

  3. 优化资源分配手段

    通过配置实现某一应用最大可用数据库连接数的限制, 避免某一应用独占所有资源

  4. 避免数据库连接泄漏

    可根据预先的占用超时设定, 强制回收被占用的连接, 从而避免常规数据库连接操作可能出现的资源泄漏

自定义连接池

设计一个连接池类

  • 包含存储连接对象的集合
  • 用来获连接取对象的方法
  • 归还连接对象的方法

开源的数据库连接池

JDBC数据库连接池使用javax.sql.DataSource来表示, DataSource是一个接口

DBCP

Apache提供的数据库连接池, tomcat服务器自带DBCP

速度相对C3P0较快, 但自身存在bug, Hibernate3已经不再支持

C3P0

速度相对较慢, 稳定性还可以, Hibernate官方推荐使用

  • QuickStart

    • 获取连接池

      ComboPooledDataSource cpds = new ComboPooledDataSource();
      
    • 设置基本信息

      • 加载驱动类
      • JDBCurl
      • 用户名和登录密码
      • 连接池初始化时的连接数
                  cpds.setDriverClass( "com.mysql.cj.jdbc.Driver" ); 
                  cpds.setJdbcUrl( "jdbc:mysql://localhost:3306/first?serverTimezone=GMT" );
                  cpds.setUser("root");
                  cpds.setPassword("2018WH");
                  // 设置初始时数据库连接池中的连接数
                  cpds.setInitialPoolSize(10);
      
    • 获取连接对象

                  Connection connection = cpds.getConnection();
      

    完整代码如下

        @Test
        public void test01() {
            // 获取C3P0连接池
            ComboPooledDataSource cpds = new ComboPooledDataSource();
            try {
                cpds.setDriverClass( "com.mysql.cj.jdbc.Driver" ); //loads the jdbc driver
                cpds.setJdbcUrl( "jdbc:mysql://localhost:3306/first?serverTimezone=GMT" );
                cpds.setUser("root");
                cpds.setPassword("2018WH");
                // 设置初始时数据库连接池中的连接数
                cpds.setInitialPoolSize(10);
                Connection connection = cpds.getConnection();
                System.out.println(connection);
                // 销毁连接池(一般不需要)
    //            DataSources.destroy(cpds);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    
  • 使用XML文件设置基本属性(推荐)

    • 配置文件别名
    • 获取连接的基本信息
      • 驱动类
      • JDBCurl
      • 用户名和登录密码
    • 进行数据库连接池管理的基本信息
      • 当池中连接不够时, c3p0一次性向服务器申请的连接数
      • 最小/最大连接数
      • 池维护的最大Statement数, 每个连接维护的最大Statement数
    
        
        
            
            com.mysql.cj.jdbc.Driver
            jdbc:mysql://localhost:3306/first?serverTimezone=GMT
            root
            2018WH
            
            
            5
            
            10
            
            10
            
            100
            
            50
            
            2
        
    
    
Druid

阿里巴巴提供的数据库连接池, 稳定性与性能平衡, 针对监控而生的数据库连接池, 开发主流使用, 目前最好的连接池之一

  • 配置文件(druid.properties)如下

    # 获取连接的基本属性
    url=jdbc:mysql://localhost:3306/first?serverTimezone=GMT
    username=root
    password=2018WH
    driverClassName=com.mysql.cj.jdbc.Driver
    # 管理连接的基本属性
    # 连接池初始化时有10个连接对象
    initialSize=10
    maxActive=20
    maxWait=1000
    filters=wall
    
  • 详细配置参数

参数 缺省 说明
name 配置这个属性的意义在于
如果存在多个数据源, 监控的时候可以通过名字来区分
如果没有配置, 将会生成一个名字
格式是: "DataSource-" + System.identityHashCode(this)
jdbcUrl 连接数据库的资源标识符
username 连接数据库的用户名
password 连接数据库的密码
driverClassName 根据url自动识别 数据库驱动类
initialSize 0 初始化时建立的连接数
初始化发生在显示调用init(), 或getConnection()时
maxActive 8 最大连接数
minIdle 最小连接数
maxWait 获取连接时最大等待时间, 单位毫秒。
配置之后, 缺省启用公平锁, 并发效率会有所下降
如果需要可以通过配置useUnfairLock属性为true使用非公平锁
poolPreparedStatements false 是否缓存preparedStatement, 也就是PSCache
PSCache对支持游标的数据库性能提升巨大
比如说oracle, 但在mysql下建议关闭
maxOpenPreparedStatements -1 要启用PSCache, 必须配置大于0
大于0时, poolPreparedStatements触发改为true
Druid中没有Oracle下PSCache占用内存过多的问题
可以把这个数值配置大一些, 比如说100
validationQuery 用来检测连接是否有效的SQL, 要求是一个查询语句
如果validationQuery为null, testOnBorrow、testOnReturn、testWhileIdle都不会其作用
testOnBorrow true 申请连接时执行validationQuery检测连接是否有效
做了这个配置会降低性能
testOnReturn false 归还连接时执行validationQuery检测连接是否有效
做了这个配置会降低性能
testWhileIdle false 建议配置为true, 不影响性能, 并且保证安全性
申请连接的时候检测
如果空闲时间大于timeBetweenEvictionRunsMillis
执行validationQuery检测连接是否有效
timeBetweenEvictionRunsMillis 有两个含义:
1.Destroy线程会检测连接的间隔时间
2.testWhileIdle的判断依据, 详细看testWhileIdle属性的说明
connectionInitSqls 物理连接初始化的时候执行的SQL
exceptionSorter 根据dbType自动识别 当数据库抛出一些不可恢复的异常时, 抛弃连接
filters 属性类型是字符串, 通过别名的方式配置扩展插件
常用的插件有:
监控统计用的filter:stat
日志用的filter:log4j
防御sql注入的filter:wall
proxyFilters 如果同时配置了filters和proxyFilters
则是组合关系, 并非替换关系

自定义JDBC工具类

获取连接

/**
 * 获取数据库连接
 *
 * @return java.sql.Connection
 */
public static Connection getConnection() throws Exception {
    InputStream stream = ClassLoader.getSystemClassLoader().getResourceAsStream("jdbc.properties");
    Properties properties = new Properties();
    Connection connection;
    properties.load(stream);
    String url = properties.getProperty("url");
    String password = properties.getProperty("password");
    String user = properties.getProperty("user");
    String driverClass = properties.getProperty("driverClass");
    // 注册驱动
    Class.forName(driverClass);
    // 获取连接
    connection = DriverManager.getConnection(url, user, password);
    return connection;
}

资源关闭

/**
 * 关闭资源
 * @param connection 数据库连接
 * @param statement PreparedStatement对象
 * @return void
 */
public static void closeResource(Connection connection, Statement statement) {
    try {
        if (statement != null) {
            statement.close();
        }
        if (connection != null) {
            connection.close();
        }
    } catch (SQLException e) {
        e.printStackTrace();
    }
}

/**
 * 关闭资源
 * @param connection 数据库连接
 * @param statement PreparedStatement对象
 * @param set 结果集
 * @return void
 */
public static void closeResource(Connection connection, Statement statement, ResultSet set) {
    try {
        if (set != null) {
            set.close();
        }
        if (statement != null) {
            statement.close();
        }
        if (connection != null) {
            connection.close();
        }
    } catch (SQLException e) {
        e.printStackTrace();
    }
}

Java与MySQL相应的数据类型

Java类型 SQL类型
boolean BIT
byte TINYINT
short SMALLINT
int INT(INTEGER)
long BIGINT
String CHAR, VARCHAR, LONGVARCHAR
byte[] BINARY, VAR BINARY
java.sql.Date DATE
java.sql.Time TIME
java.sql.Timestamp TIMESTAMP

ORM编程思想

Object Relational Mapping, 对象关系映射

  • 数据库的一个表对应一个Java类

  • 表的一行数据对应一个Java类的对象

  • 表的一个字段对应一个Java类的属性

元数据

元数据(Metadata), 又称中介数据, 中继数据, 为描述数据的数据(data about data)

主要是描述数据属性(property)的信息, 用来支持如指示存储位置、历史数据、资源查找、文件记录等功能

在数据库与JDBC中, 结果集元数据(ResultSetMetaData)记录了字段数, 字段名, 字段别名

获取结果集的元数据

// 获取结果集
ResultSet set = statement.excuteQuery();
// 获取元数据
ResultSetMetaData metadata = set.getMetaData();

获取的字段数

int columnCount = metadata.getColumnCount();

获取字段名

for (int i = 0; i < columnCount; i++) {
    String columnName = metadata.getColumnName(i + 1);
}

获取字段别名

  • 根据ORM思想, 数据库字段对应Java类属性
  • 字段和属性命名方式不同, 故JDBC查询时会出错, SQL语句中加入字段别名则可以解决该问题
  • 用元数据的getColumnLabel()可以获取字段别名(没有别名时返回字段名)
for (int i = 0; i < columnCount; i++) {
    String columnLabel = metadata.getColumnLabel(i + 1);
}

JDBC查询多行数据流程

  1. 建立数据库连接

  2. 创建预编译SQL语句

  3. 填充预编译SQL语句的占位符

  4. 执行SQL语句获取结果集

    • statement.execute()

      如果执行DQL(查), 有查询结果, 则返回true

      其他操作(增删改)返回false

    • statement.executeUpdate()

      返回操作影响行数(int)

  5. 从结果集中获取元数据, 取出列数, 列别名, 列值

  6. 逐行扫描结果集

    1. 建立与表中一行数据对应的Java类对象
    2. 用反射机制获取字段别名, 字段值, 设置对象该属性的值
    3. 对象加入集合
  7. 返回结果对象集合

  8. 关闭资源(连接, statement, 结果集)

    public  List selectMulLine(Class cls, String sql, Object... args) {
        Connection connection = null;
        PreparedStatement statement = null;
        ResultSet set = null;
        ArrayList result = new ArrayList<>();
        try {
            connection = JDBCUtils.getConnection();
            statement = connection.prepareStatement(sql);
            for (int i = 0; i < args.length; i++) {
                statement.setObject(i + 1, args[i]);
            }
            set = statement.executeQuery();
            ResultSetMetaData metaData = set.getMetaData();
            int columnCount = metaData.getColumnCount();
            while (set.next()) {
                E e = cls.newInstance();
                for (int i = 0; i < columnCount; i++) {
                    String columnLabel = metaData.getColumnLabel(i + 1);
                    Field field = cls.getDeclaredField(columnLabel);
                    Object value = set.getObject(i + 1);
                    field.setAccessible(true);
                    field.set(e, value);
                }
                result.add(e);
            }
            return result;
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            JDBCUtils.closeResource(connection, statement, set);
        }
        return null;
    }

操作Blob类型字段

MySQL BLOB类型

  • MySQL中, Blob是一个二进制大型对象(视频, 图片), 存放大量数据的容器

  • 插入Blob类型数据必须使用PreparedStatement

  • MySQL四种Blob类型(除了储存大小外, 无差别)

    Blob类型 大小
    TINYBLOB
    BLOB
    MEDIUMBLOB
    LONGBLOB
  • 如果指定了相关的BLOB类型后, 报错: xxx too large

    则在MySQL的安装目录找到my.ini文件加上配置参数(重启MySQL服务生效)

    max_allowed_packet=16M

JDBC插入Blob类型数据

  • 建立连接

  • 预编译SQL, 用文件输入流填充占位符(setBlob(parameterIndex, stream))

    FileInputStream stream = null;
    try {
        stream = new FileInputStream(new File("src/成果.jpg"));
        statement.setBlob(5, stream);
    } catch (Exception e) {
        e.printStackTrace();
    } finally {
      try {
          if (stream != null) {
              stream.close();
              }
      } catch (IOException e) {
              e.printStackTrace();
          }
    }
    
  • 执行SQL

JDBC读取Blob类型数据

  • 建立连接

  • 预编译SQL并执行获取结果集

  • 从结果集获取元数据(取出列数, 列标签)

  • 利用反射

    • 如果不是Blob类型, 则设置为对象属性
    • 如果是Blob类型, 则 Blob 变量 = resultSet.getBlob(字段标签);
    Blob photo = null;
    for (int i = 0; i < columnCount; i++) {
      if (field.getName() == "photo") {
          photo = set.getBlob("photo");
        } else {
            field.set(employee, value);
        }
    }
    
  • 对于Blob类型需用IO流读写

    InputStream inputStream = photo.getBinaryStream();

    if (photo != null) {
        inputStream = photo.getBinaryStream();
        outputStream = new FileOutputStream(new File("src/Gogoing/bean/成果.jpg"));
        byte[] buffer = new byte[1024];
        int length;
        while ((length = inputStream.read(buffer)) != -1) {
            outputStream.write(buffer, 0, length);
            outputStream.flush();
        }
    }
    
  • 关闭资源

    finally {
        JDBCUtils.closeResource(connection, statement, set);
        try {
            if (inputStream != null) {
                inputStream.close();
            }
            if (outputStream != null) {
                outputStream.close();
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    

DBUtils

Apached的一个开源项目, 对JDBC进行封装, 方便对数据库进行操作切不影响性能

增删改数据

String sql = "INSERT INTO t_employee VALUES (?, ?, ?, ?)";
// runner.update(connection, sql, params)
runner.update(connection, sql, 2, "蔡徐坤", 2222, 2);

查询数据

  • Handler

    • BeanHandler

      ResultSetHandler接口的实现类, 用于封装表中的一行数据

      // 如果Java对象属性与表中一行数据的字段不匹配, 一定要起别名
      String sql = "SELECT e_id id, e_name name, e_salary salary, d_id FROM t_employee WHERE e_id = ?";
      BeanHandler handler = new BeanHandler<>(Employee.class);
      // runner.query(connection, sql, handler, params)
      Employee employee = runner.query(connection, sql, handler, 4);
      
    • BeanListHandler

      ResultSetHandler接口的实现类, 用于封装表中的多行数据

      String sql = "SELECT e_id id, e_name name, e_salary salary, d_id FROM t_employee WHERE e_id BETWEEN ? AND ?";
      BeanListHandler handler = new BeanListHandler<>(Employee.class);
      List employees = runner.query(connection, sql, handler, 1, 4);
      
    • MapHandler

      ResultSetHandler接口的实现类, 以键值对的形式读取表中的一行数据

      String sql = "SELECT e_id id, e_name name, e_salary salary, d_id FROM t_employee WHERE e_id = ?";
      MapHandler handler = new MapHandler();
      Map result = runner.query(connection, sql, handler, 4);
      System.out.println(result);
      

      输出: {id=4, name=成果, salary=15200, d_id=2}

    • MapListHandler

      ResultSetHandler接口的实现类, 以键值对的形式读取表中的多行数据

      String sql = "SELECT e_id id, e_name name, e_salary salary, d_id FROM t_employee WHERE e_id BETWEEN ? AND ?";
      MapListHandler handler = new MapListHandler();
      List> result = runner.query(connection, sql, handler, 1, 4);
      
    • ScalarHandler

      ResultSetHandler接口的实现类, 用于查询特殊值

      String sql = "SELECT COUNT(*) FROM t_employee";
      ScalarHandler handler = new ScalarHandler();
      Long result = (Long) runner.query(connection, sql, handler);
      

关闭资源

DbUtils.closeQuietly(connection);
DbUtils.closeQuietly(statement);
DbUtils.closeQuietly(resultSet);

你可能感兴趣的:(JDBC)