JDBC: Java Database Connectivity, 是Java程序与数据库连接的中间件
JDBC封装了与各种数据库管理系统交互的方法
JDBC使用过程
- 准备jar包并导入
- 注册驱动
- 建立连接(创建Connection对象)
- 创建Statement对象
- 执行SQL, 获取结果集
- 处理结果集中的数据
- 释放资源(关闭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();
步骤
- 关闭自动提交功能(setAutoCommit(false);)
- 将需要批量执行的任务添加到批处理任务分组中: statement.addBatch(sql);
- 批量执行这个分组中所有的任务: statement.executeBatch();
- 将变更提交到数据库(因为第一步关闭了自动提交): 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);
}
}
- 过程
- 取消自动提交
- 执行两个DML语句后提交
- 如果捕捉到异常要回滚
- 关闭资源之前, 把自动提交设置为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加载到内存中, 再验证用户名和密码, 需要数据库连接时就像数据库请求一个, 执行完再断开连接, 这种方式消耗大量资源和时间, 数据库的连接资源得不到很好的重复利用, 若同时发生海量的数据库连接请求, 可能会造成服务器崩溃
- 对于每一次连接, 使用完都得断开连接, 否则如果程序出现异常未能关闭, 将导致数据库管理系统的内存泄漏, 导致重启数据库
- 不能控制被创建的连接对象数, 系统资源会被毫无顾忌的分配出去, 连接过多也会导致内存泄漏, 服务器崩溃
数据库连接池
基本思想: 为数据库连接建立一个"缓冲池", 预先在"缓冲池"中放入一定数量的连接, 需要建立连接时, 从"缓冲池"中取出一个使用完毕后再放回去
数据库连接池负责分配, 管理, 释放数据库连接, 允许程序重复使用一个现有的连接, 而不是重新建立一个
-
数据库连接池在初始化时将创建一定数量的数据库连接, 并放入池中
这些数据库连接的数量是由最小数据库连接数来设定的, 无论这些数据库连接是否被使用, 连接池都将一直保证至少拥有这么多数量的连接
连接池的最大数据库连接数限定了这个连接池能占有的最大连接数, 当程序向连接池请求获取连接数超过最大连接数时, 这些请求将被加入等待队列
使用连接池的好处
-
实现资源重用
避免频繁创建, 释放连接引起大量性能开销, 减少系统消耗同时增加系统运行稳定性
-
提高系统反应速度
数据库连接池初始化, 创建了若干数据库连接于池中备用, 对于业务请求而言, 直接利用现有连接, 避免数据库连接初始化和释放的时间开销, 从而减少系统反应时间
-
优化资源分配手段
通过配置实现某一应用最大可用数据库连接数的限制, 避免某一应用独占所有资源
-
避免数据库连接泄漏
可根据预先的占用超时设定, 强制回收被占用的连接, 从而避免常规数据库连接操作可能出现的资源泄漏
自定义连接池
设计一个连接池类
- 包含存储连接对象的集合
- 用来获连接取对象的方法
- 归还连接对象的方法
开源的数据库连接池
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查询多行数据流程
建立数据库连接
创建预编译SQL语句
填充预编译SQL语句的占位符
-
执行SQL语句获取结果集
-
statement.execute()
如果执行DQL(查), 有查询结果, 则返回true
其他操作(增删改)返回false
-
statement.executeUpdate()
返回操作影响行数(int)
-
从结果集中获取元数据, 取出列数, 列别名, 列值
-
逐行扫描结果集
- 建立与表中一行数据对应的Java类对象
- 用反射机制获取字段别名, 字段值, 设置对象该属性的值
- 对象加入集合
返回结果对象集合
关闭资源(连接, 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
-
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);