JDBC
1、介绍
- Java中数据库存取技术可分为:
- JDBC直接访问数据库
- JDO(Java Data Object)技术
- 第三方O/R工具,如Hibernate、Mybatis等
其中,JDBC是Java访问数据库的基石,JDO、Hibernate、Mybatis等只是更好的封装了JDBC
- JDBC(Java Database Connectivity)是一个独立于特定数据库管理系统,通用的SQL数据库存取和操作的公共接口(一组API),定义了用来访问数据库的标准Java类库,(java.sql,javax.sql)使用这些类库可以以一种标准的方法、方便地访问数据库资源
- JDBC接口包含两个层次:
- 面向应用的API:Java API,抽象接口,供应用程序开发人员使用(连接数据库,执行SQL语句,获得结果)
- 面向数据库的API:Java Driver API,供开发商开发数据库驱动程序用
- JDBC程序编写步骤:
2、获取数据库连接
方式一
- 实例化Driver:Driver driver = new com.mysql.cj.jdbc.Driver();
- 数据库连接:Connection connect = driver.connect(String url,Properties info);
url:例:"jdbc:mysql://localhost:3306/jdbc_learn"
- jdbc:mysql:协议
- localhost:ip地址
- 3306:端口
- jdbc_learn:对应数据库
info:例:
1
2
3Properties info = new Properties();
info.setProperty("user", "root");
info.setProperty("password", "123456");- Properties类,需要将用户名和密码信息封装在里面
例: 1
2
3
4
5
6
7Driver driver = new com.mysql.cj.jdbc.Driver();
String url = "jdbc:mysql://localhost:3306/jdbc_learn";
Properties info = new Properties();
info.setProperty("user", "root");
info.setProperty("password", "123456");
Connection connect = driver.connect(url, info);
System.out.println(connect);
方式二:通过反射
实例化Driver:
1
2Class clazz = Class.forName("com.mysql.cj.jdbc.Driver");
Driver driver = (Driver) clazz.newInstance();其它和方式一一样
方式三:使用DriverManager
- 实例化Driver
- 注册驱动:DriverManager.registerDriver(Driver driver);
- 获取连接:
- Connection connection = DriverManager.getConnection(String url,Properties info);
- Connection connection = DriverManager.getConnection(String url,String user, String password);
1
2
3
4
5
6
7
8
9
10
11
12
13
14Class clazz = Class.forName("com.mysql.cj.jdbc.Driver");
Driver driver = (Driver) clazz.newInstance();
// 注册驱动
DriverManager.registerDriver(driver);
String url = "jdbc:mysql://localhost:3306/jdbc_learn";
Properties info = new Properties();
info.setProperty("user", "root");
info.setProperty("password", "123456");
Connection connection = DriverManager.getConnection(url, info);
// 或
// String user = "root";
// String password = "123456";
// DriverManager.getConnection(url, user, password);
// System.out.println(connection);
方式四:代码优化
注册驱动部分代码可省略 1
2
3
4
5
6
7Class.forName("com.mysql.cj.jdbc.Driver");
String url = "jdbc:mysql://localhost:3306/jdbc_learn";
Properties info = new Properties();
info.setProperty("user", "root");
info.setProperty("password", "123456");
Connection connection = DriverManager.getConnection(url, info);
System.out.println(connection);1
2
3
4
5
6
7static {
try {
DriverManager.registerDriver(new Driver());
} catch (SQLException var1) {
throw new RuntimeException("Can't register driver!");
}
}
方式五:最终常用版,配置文件加载
数据和代码分离,实现了解耦 1
2
3
4url=jdbc:mysql://localhost:3306/jdbc_learn
user=root
password=123456
driverClass=com.mysql.cj.jdbc.Driver1
2
3
4
5
6
7InputStream is = TestJDBC.class.getClassLoader().getResourceAsStream("jdbc.properties");
Properties properties = new Properties();
properties.load(is);
Class.forName(properties.getProperty("driverClass"));
Connection connection = DriverManager.getConnection(properties.getProperty("url"),
properties.getProperty("user"), properties.getProperty("password"));
System.out.println(connection);
3、使用PreparedStatement实现CRUD操作
- java.sql包中有3个接口分别定义了对数据库的调用的不同方式:
- Statement:用于执行静态SQL语句并返回它所生成结果的对象
- PreparedStatement:SQL语句被预编译并存储在此对象中,可以使用此对象多次高效地执行该语句
- CallableStatement:用于执行SQL语句存储过程
- 为什么不用Statement?使用Statement操作数据表的弊端
- SQL语句需要拼串,繁琐
- 存在SQL注入的问题
使用PreparedStatement
- 获取连接
- 预编译sql语句,返回PreparedStatement实例:
PreparedStatement ps = connection.prepareStatement(String sql);
- 填充占位符
ps.setString(int parameterIndex, String x)、ps.setDate(int parameterIndex, java.sql.Date date)等......
- 执行操作
ps.execute(); 或ps.executeUpdate();返回操作影响的数据行数
- 资源关闭
ps.close();connection.close();
增删改:
增:
1 | Connection connection = getConnection(); |
改:
修改案例: 1
JDBCUtils.update("update customers set name=? where id=?", "灰太狼", 19);
删:
1 | JDBCUtils.update("delete from customers where id=?", 8); |
查询:
查询过程与增删改有所不同,因为会有返回值:
- 获取连接
- 预编译SQL语句,并填充占位符
- 执行SQL并返回结果集:ResultSet resultSet = ps.executeQuery();
- 处理结果集
- resultSet.next():判断结果集下一条是否有数据,如果有返回true并指针下移,否则返回false
- resultSet.getXXX(int columnIndex):获取索引columnIndex对应的XXX类型数据,索引从1开始
- ResultSetMetaData rsmd = resultSet.getMetaData():获取结果集的元数据,可以通过元数据获取结果集中的列数
- rsmd.getColumnCount();获取结果集中列数
- rsmd.getColumnName(int index);获取index列的列名
- rsmd.getColumnLabel(int index);获取index列的别名,没有别名则返回列名
例(异常未处理): 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19Connection connection = JDBCUtils.getConnection();
String sql = "select id,name,email,birth from customers where id = ?"; //u ?:
PreparedStatement ps = connection.prepareStatement(sql);
ps.setObject(1, 19);
// 执行并返回结果集
ResultSet resultSet = ps.executeQuery();
while (resultSet.next()) { // 判断结果集的下一条是否有数据
// 获取当前数据的各个字段值
int id = resultSet.getInt(1);
String name = resultSet.getString(2);
String email = resultSet.getString(3);
Date birth = resultSet.getDate(4);
Object[] data = new Object[]{id, name, email, birth};
// 封装到类中
Customer customer = new Customer(id, name, email, birth);
System.out.println(customer);
}
// 资源关闭
JDBCUtils.closeResource(connection, ps, resultSet);
Java与SQL对应数据类型转换表
ORM编程思想(Object Relational Mapping)
- 一个数据表对应一个Java类
- 表中的一条记录对应Java类的一个对象
- 表中的一个字段对应Java类的一个属性
封装到JDBCUtils
1 |
|
注意事项:
SQL语句中如果存在表名或其它名与SQL关键字冲突的,需要用`(着重号),如: update("update **
order`** set order_name=? where id=?", "DD", 3);
PreparedStatment的好处
- 解决SQL注入问题
- 通过预编译导致SQL格式固定,之后填充的只认为是数据,避免了SQL注入问题
- 可以操作Blob类型数据,而Statement
- 占位符可以传流作为参数
- 可以实现更高效的批量操作
- 能最大可能提高性能:
- DBServer会对预编译语句提供性能优化,因为预编译语句有可能重复调用,所以语句在被DBServer的编译器编译后的执行代码被缓存下来,那么下次调用时只要是相同的预编译语句就不用编译,只要将参数直接传入编译过的语句执行代码中就会得到执行
- 在Statement语句中,即使是相同操作但因为数据内容不一样,所以整个语句本身不能匹配,没有缓存语句的意义。事实是没有数据库会对普通语句编译后的执行代码缓存。这样每执行一次都要对传入的语句编译一次。
4、操作Blob类型字段
- MySQL的四种Blog类型: | 类型 | 最大大小(单位:字节) | | --- | --- | | TinyBlob | 255 | | Blob | 65K | | MediumBlob | 16M | | LongBlob | 4G |
- 实际使用中根据需要存入的数据大小定义不同的BLOB类型
- 需要注意的是:如果存储的文件过大,数据库的性能会下降
- 如果在指定了相关的Blob类型以后,还报错:xxx too large,则在mysql安装目录下,找到my.ini文件加上配置参数:max_allowed_packet=16M。同时注意:修改了my.ini文件后,需要重启mysql服务
增删改
1
2
3InputStream is = new FileInputStream("C:\\Users\\Zephon\\Pictures\\bg.jpg");
JDBCUtils.update("insert into customers(name,email,birth,photo) values(?,?,?,?)",
"喜羊羊", "xxx@gmail.com", "2001-03-02", is);查询(简写)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19InputStream is = null;
OutputStream os = null;
Connection connection = JDBCUtils.getConnection();
String sql = "select name,email,birth,photo from customers where id=?";
PreparedStatement ps = connection.prepareStatement(sql);
ps.setInt(1, 20);
ResultSet resultSet = ps.executeQuery();
if(resultSet.next()){
Blob blob = resultSet.getBlob(4);
is = blob.getBinaryStream();
os = new FileOutputStream("pic.jpg");
byte[] buf = new byte[1024];
int len;
while((len=is.read(buf))!=-1){
os.write(buf, 0, len);
}
}
if(is!=null) is.close();
if(os!=null) os.close();
5、批量操作(插入)
update和delete本身就具有批量操作的效果,而insert则不是
循环每次执行
1
2
3
4
5
6
7Connection connection = JDBCUtils.getConnection();
String sql = "insert into goods(name) values (?)";
PreparedStatement ps = connection.prepareStatement(sql);
for (int i = 0; i < 20000; i++) {
ps.setObject(1, "name_"+i);
ps.execute();
}通过addBatch()、executeBatch()、clearBatch()执行 :::success mysql服务器默认是关闭批处理的,需要通过参数开启,即将以下内容写在配置文件url后面: ?rewriteBatchedStatements=true :::
1
2
3
4
5
6
7
8
9
10
11Connection connection = JDBCUtils.getConnection();
String sql = "insert into user(name) values (?)";
PreparedStatement ps = connection.prepareStatement(sql);
for (int i = 0; i < 20000; i++) {
ps.setObject(1, "name_" + i);
ps.addBatch(); // 累积SQL
if (i % 500 == 0) {
ps.executeBatch(); // 执行所有SQL batch
ps.clearBatch(); // 清空batch
}
}终极方案
关闭自动提交 1
2
3
4
5
6
7
8
9
10
11
12
13
14Connection connection = JDBCUtils.getConnection();
// 设置不自动提交
connection.setAutoCommit(false);
String sql = "insert into user(name) values (?)";
PreparedStatement ps = connection.prepareStatement(sql);
for (int i = 0; i < 20000; i++) {
ps.setObject(1, "name_" + i);
ps.addBatch(); // 累积SQL
if (i % 500 == 0) {
ps.executeBatch(); // 执行所有SQL batch
ps.clearBatch(); // 清空batch
}
}
connection.commit();
6、数据库事务
不用事务时可能存在的问题:
AA用户给BB用户转帐100时,中间出现异常则会出现问题 1
2
3
4
5JDBCUtils.update("update user_table set balance=balance-100 where user = ?", "AA");
// 中间发生异常
int i = 3/0;
JDBCUtils.update("update user_table set balance=balance+100 where user = ?", "BB");
System.out.println("转账成功");
哪些操作会导致数据的自动提交?
- DDL操作一旦执行,就会自动提交
- DML默认情况下,一旦执行,就会自动提交
- 可以通过 set autocommit = false的方式取消DML操作的自动提交(无法取消DDL操作的自动提交)
- 默认在关闭连接时,会自动提交数据
事务形式提交:
1 | Connection connection = null; |
注:如果最后连接connection不需要关闭,还可能被重复使用,则需要恢复其自动提交状态,即:connection.setAutoCommit(false);
JDBC设置MySQL隔离级别
- 获取事务隔离级别:int transactionIsolation = connection.getTransactionIsolation(); // 默认是4
- 1:读未提交
- 2:读已提交
- 4:可重复读
- 8:串行化
- 设置事务隔离级别:connection.setTransactionIsolation(int level);
- level可直接用常量:Connection.TRANSACTION_READ_UNCOMMITTED、Connection.TRANSACTION_READ_COMMITTED等
7、数据库连接池
- 传统模式的问题:
- 每次都需要建立连接,将Connection加载到内存,耗时,数据库的连接资源也没有得到很好的重复利用
- 对于每次数据库连接,使用完后都得断开
- 不能控制被创建的连接对象数,如连接过多可能导致内存泄漏、服务器崩溃
- 为了解决传统开发中的数据库连接问题,可以采用数据库连接池技术
- 数据库连接池的基本思想:为数据连接建立一个“缓冲池”。预先在缓冲池中放入一定数量的连接,当需要建立数据库连接时,只需要从“缓冲池”中取出一个,使用完毕之后再放回去
- 数据库连接池负责分配、管理和释放数据库连接,允许应用程序重复使用一个现有的数据库连接,而不是重新建立一个
- 数据库连接池在初始化时将创建一定数量的数据库连接放到连接池中,这些数据库连接的数量是由最小数据库连接数来设定的。无论这些数据库连接是否被使用,连接池都将一直保持至少拥有这么多的连接数量。连接池的最大数据库连接数量限定了这个连接池能占有的最大连接数,当应用程序向连接池请求的连接数超过最大连接数量时,这些请求将被加入到等待队列中。
- 数据库连接池的优点:
- 资源重复
- 更快的系统反应速度
- 新的资源分配手段
- 统一的连接管理,避免数据库连接泄漏
多种开源的数据库连接池
- JDBC的数据库连接池使用java.sql.DataSource来表示,DataSource只是一个接口,该接口通常由服务器(Weblogic, WebSphere, Tomcat)提供实现,也有一些开源组织提供实现
- DBCP是Apache提供的数据库连接池。tomcat服务器自带dbcp数据库连接池。速度相比c3p0较快,但因自身存在bug,hibernate3已不再提供支持
- C3P0是一个开源组织提供的数据连接池,速度相对较慢,稳定性还行,Hibernate官方推荐使用
- Proxool是sourceforge下的一个开源项目数据库连接池,有监控连接状态的功能,稳定性较C3P0差一点
- BoneCP是一个开源组织提供的数据库连接池,速度快
- Druid是阿里提供的数据库连接池,据说是集DBCP、C3P0、Proxool优点于一身的数据库连接池,但速度不确定是否有BoneCP快
- DataSource通常被称为数据源,包含连接池和连接池管理两个部分,习惯上也常把DataSource称为连接池
- DataSource用来取代DriverManager来获取connection,获取速度快,同时可以大幅度提高数据库访问速度
以C3P0为例:
- 初始化:
原始方式: 1
2
3
4
5ComboPooledDataSource cpds = new ComboPooledDataSource();
cpds.setDriverClass("com.mysql.cj.jdbc.Driver");
cpds.setJdbcUrl("jdbc:mysql://localhost:3306/jdbc_learn");
cpds.setUser("root");
cpds.setPassword("123456");1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<c3p0-config>
<named-config name="testc3p0">
<property name="driverClass">com.mysql.cj.jdbc.Driver</property>
<property name="jdbcUrl">jdbc:mysql://localhost:3306/jdbc_learn?useSSL=false&serverTimezone=UTC</property>
<property name="user">root</property>
<property name="password">123456</property>
<!-- 当数据库连接池中的连接数不够时,一次性向数据库服务器申请的连接数 -->
<property name="acquireIncrement">5</property>
<!-- c3p0数据库连接池中初始化时的连接数 -->
<property name="initialPoolSize">10</property>
<!-- c3p0数据库连接池中维护的最少连接数 -->
<property name="minPoolSize">10</property>
<!-- c3p0数据库连接池最多维护的连接数 -->
<property name="maxPoolSize">100</property>
<!-- c3p0数据库连接池最多维护的Statement的个数 -->
<property name="maxStatements">50</property>
<!-- 每个连接中可以最多使用的Statement的个数 -->
<property name="maxStatementsPerConnection">2</property>
</named-config>
</c3p0-config>1
2
3ComboPooledDataSource cpds = new ComboPooledDataSource("testc3p0");
Connection connection = cpds.getConnection();
System.out.println(connection);
- 相关设置:
- 设置初始数据库连接池中连接数量:cpds.setInitialPoolSize(int initialSize);
- 设置数据库连接池中最大连接数量:cpds.setMaxPoolSize(int maxSize);
- 获取连接:Connection connection = cpds.getConnection();
- 使用连接
- (可选)如果连接池不需要了可以销毁:DataSources.destroy(cpds);
以Druid为例
- 通过配置文件实例化
1
2
3
4
5
6
7url=jdbc:mysql://localhost:3306/jdbc_learn
username=root
password=123456
driverClass=com.mysql.cj.jdbc.Driver
initialSize=10
maxActive=10后续步骤都是一样 > 其它几种数据库连接池具体使用时可具体查看文档1
2
3
4
5
6Properties properties = new Properties();
InputStream is = ClassLoader.getSystemClassLoader().getResourceAsStream("druid.properties");
properties.load(is);
DataSource source = DruidDataSourceFactory.createDataSource(properties);
Connection connection = source.getConnection();
System.out.println(connection);
8、Apache-DBUtils实现CRUD操作
- common-dbutils是Apache提供的一个开源JDBC工具类库,对JDBC进行简单封装,极大简化jdbc编码的工作量,同时也不影响程序的性能
- 官方文档