title: 【学习之路】JDBC学习
draft: true
tags:
- 学习之路
- JavaEE
- JDBC
categories: - JavaEE
- JDBC
description: ‘关于JDBC技术的学习.如何使用IDEA加载驱动类,使用JDBC操作数据库,SQL注入问题和数据库事务处理,学习Druid数据库连接池’
abbrlink: 53289
date: 2020-10-13 16:24:22
cover: https://cdn.jsdelivr.net/gh/CodeZixuan/Blog_Images/img/封面.jpg
java中的数据存储技术
在java中,数据库存取技术可分为以下几类
- JDBC直接访问数据
- JDO (Java Data Object)技术
- 第三方O/R工具,如Hibernate,Mybatis
- JDBC是java访问数据库的基石JDO、Hibernate、MyBatis只是更好的封装了JDBC
JDBC介绍
- 如果没有JDBC,那么java程序访问数据库应该是这样的
- 有了JDBC,java程序访问数据库时是这样的
- 总结如下
Driver接口实现类
- java.sql.Driver接口是所有JDBC驱动程序需要实现的接口。这个几口是提供给数据库厂商使用的,不同的数据库厂商提供不同的实现。
在程序中不需要直接去访问实现了 Driver 接口的类,而是由驱动程序管理器类(java.sql.DriverManager)去调用这些Driver实现。
- Oracle的驱动:oracle.jdbc.driver.OracleDriver
- mySql的驱动: com.mysql.jdbc.Driver
用IDEA导入JDBC驱动
点击File选择Project Structure
点击Modules选择当前工程再点击+符号添加JARs包
选择JDBC连接池的驱动
点击方框应用驱动
注意:如果是Dynamic Web Project(动态的web项目)话,则是把驱动jar放到WebContent(有的开发工具叫WebRoot)目录中的WEB-INF目录中的lib目录下
加载与注册JDBC驱动
加载驱动:加载 JDBC 驱动需调用 Class 类的静态方法 forName(),向其传递要加载的 JDBC 驱动的类名
- Class.forName(“com.mysql.jdbc.Driver”);
注册驱动:DriverManager 类是驱动程序管理器类,负责管理驱动程序
使用DriverManager.registerDriver(com.mysql.jdbc.Driver)来注册驱动
通常不用显式调用 DriverManager 类的 registerDriver() 方法来注册驱动程序类的实例,因为 Driver 接口的驱动程序类都包含了静态代码块,在这个静态代码块中,会调用 DriverManager.registerDriver() 方法来注册自身的一个实例。下图是MySQL的Driver实现类的源码:
JDBC URL
- JDBC URL 用于标识一个被注册的驱动程序,驱动程序管理器通过这个 URL 选择正确的驱动程序,从而建立到数据库的连接。
JDBC URL的标准由三部分组成,各部分间用冒号分隔。
- jdbc:子协议:子名称
- 协议:JDBC URL中的协议总是jdbc
- 子协议:子协议用于标识一个数据库驱动程序
- 子名称:一种标识数据库的方法。子名称可以依不同的子协议而变化,用子名称的目的是为了定位数据库提供足够的信息。包含主机名(对应服务端的ip地址),端口号,数据库名
- 举例:
几种常用数据库的 JDBC URL
MySQL的连接URL编写方式:
jdbc:mysql://主机名称:mysql服务端口号/数据库名称?参数=值&参数=值
- jdbc:mysql://localhost:3306/atguigu
- jdbc:mysql://localhost:3306/atguigu?useUnicode=true&characterEncoding=utf8(如果JDBC程序与服务器端的字符集不一致,会导致乱码,那么可以通过参数指定服务器端的字符集)
- jdbc:mysql://localhost:3306/atguigu?user=root&password=123456
Oracle 9i的连接URL编写方式:
jdbc:oracle:thin:@主机名称:oracle服务端口号:数据库名称
- jdbc:oracle:thin:@localhost:1521:atguigu
SQLServer的连接URL编写方式:
jdbc:sqlserver://主机名称:sqlserver服务端口号:DatabaseName=数据库名称
jdbc:sqlserver://localhost:1433:DatabaseName=atguigu
用户名和密码
- user,password可以用“属性名=属性值”方式告诉数据库
- 可以调用 DriverManager 类的 getConnection() 方法建立到数据库的连接
数据库连接方式
连接方式一
@Test
public void testConnection1() {
try {
//1.提供java.sql.Driver接口实现类的对象
Driver driver = null;
driver = new com.mysql.jdbc.Driver();
//2.提供url,指明具体操作的数据
String url = "jdbc:mysql://localhost:3306/test";
//3.提供Properties的对象,指明用户名和密码
Properties info = new Properties();
info.setProperty("user", "root");
info.setProperty("password", "123");
//4.调用driver的connect(),获取连接
Connection conn = driver.connect(url, info);
System.out.println(conn);
} catch (SQLException e) {
e.printStackTrace();
}
}
说明:上述代码中显式出现了第三方数据库的API
连接方式二
@Test
public void testConnection2() {
try {
//1.实例化Driver
String className = "com.mysql.jdbc.Driver";
Class clazz = Class.forName(className);
Driver driver = (Driver) clazz.newInstance();
//2.提供url,指明具体操作的数据
String url = "jdbc:mysql://localhost:3306/test";
//3.提供Properties的对象,指明用户名和密码
Properties info = new Properties();
info.setProperty("user", "root");
info.setProperty("password", "123");
//4.调用driver的connect(),获取连接
Connection conn = driver.connect(url, info);
System.out.println(conn);
} catch (Exception e) {
e.printStackTrace();
}
}
说明:相较于方式一,这里使用反射实例化Driver,不在代码中体现第三方数据库的API。体现了面向接口编程思想。
连接方式三
@Test
public void testConnection3() {
try {
//1.数据库连接的4个基本要素:
String url = "jdbc:mysql://localhost:3306/test";
String user = "root";
String password = "123";
String driverName = "com.mysql.jdbc.Driver";
//2.实例化Driver
Class clazz = Class.forName(driverName);
Driver driver = (Driver) clazz.newInstance();
//3.注册驱动
DriverManager.registerDriver(driver);
//4.获取连接
Connection conn = DriverManager.getConnection(url, user, password);
System.out.println(conn);
} catch (Exception e) {
e.printStackTrace();
}
}
说明:使用DriverManager实现数据库的连接。体会获取连接必要的4个基本要素。
连接方式四
@Test
public void testConnection4() {
try {
//1.数据库连接的4个基本要素:
String url = "jdbc:mysql://localhost:3306/test";
String user = "root";
String password = "123";
String driverName = "com.mysql.jdbc.Driver";
//2.加载驱动 (①实例化Driver ②注册驱动)
Class.forName(driverName);
//Driver driver = (Driver) clazz.newInstance();
//3.注册驱动
//DriverManager.registerDriver(driver);
/*
可以注释掉上述代码的原因,是因为在mysql的Driver类中声明有:
static {
try {
DriverManager.registerDriver(new Driver());
} catch (SQLException var1) {
throw new RuntimeException("Can't register driver!");
}
}
*/
//3.获取连接
Connection conn = DriverManager.getConnection(url, user, password);
System.out.println(conn);
} catch (Exception e) {
e.printStackTrace();
}
}
说明:不必显式的注册驱动了。因为在DriverManager的源码中已经存在静态代码块,实现了驱动的注册。
连接方式五(最终版)
@Test
public void testConnection5() throws Exception {
//1.加载配置文件
InputStream is = ConnectionTest.class.getClassLoader().getResourceAsStream("jdbc.properties");
Properties pros = new Properties();
pros.load(is);
//2.读取配置信息
String user = pros.getProperty("user");
String password = pros.getProperty("password");
String url = pros.getProperty("url");
String driverClass = pros.getProperty("driverClass");
//3.加载驱动
Class.forName(driverClass);
//4.获取连接
Connection conn = DriverManager.getConnection(url,user,password);
System.out.println(conn);
}
其中,配置文件声明在工程的src目录下:【jdbc.properties】
user=root
password=123
url=jdbc:mysql://localhost:3306/test
driverClass=com.mysql.jdbc.Driver
说明:使用配置文件的方式保存配置信息,在代码中加载配置文件
使用配置文件的好处:
①实现了代码和数据的分离,如果需要修改配置信息,直接在配置文件中修改,不需要深入代码
②如果修改了配置信息,省去重新编译的过程。
使用PreparedStatement实现CRUD操作
操作和访问数据库
数据库连接被用于向数据库服务器发送命令和 SQL 语句,并接受数据库服务器返回的结果。其实一个数据库连接就是一个Socket连接。
在 java.sql 包中有 3 个接口分别定义了对数据库的调用的不同方式:
- Statement:用于执行静态 SQL 语句并返回它所生成结果的对象。
- PrepatedStatement:SQL 语句被预编译并存储在此对象中,可以使用此对象多次高效地执行该语句。
- CallableStatement:用于执行 SQL 存储过程
使用Statement操作数据库
创建User类
- 定义user和password两个属性
- 写get、set、构造方法和重写toString方法
public class User {
private String user;
private String password;
public User(String user, String password) {
super();
this.user = user;
this.password = password;
}
public User() {
}
public String getUser() {
return user;
}
public void setUser(String user) {
this.user = user;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
@Override
public String toString() {
return "User{" +
"user='" + user + '\'' +
", password='" + password + '\'' +
'}';
}
}
创建StatementTest测试类
public class StatementTest<T> {
public static void main(String[] args) {
StatementTest st = new StatementTest();
st.testLogin();
}
// 使用Statement的弊端:需要拼写sql语句,并且存在SQL注入的问题
public void testLogin() {
Scanner scan = new Scanner(System.in);
System.out.print("用户名:");
String userName = scan.nextLine();
System.out.print("密码:");
String password = scan.nextLine();
/*
当userName输入:1' or
password输入:='1' or '1' = '1
就会产生SQL注入问题
*/
String sql = "SELECT user,password FROM user_table WHERE user = '" + userName + "' AND password = '" + password
+ "'";
User user = get(sql, User.class);
if (user != null) {
System.out.println("登陆成功!");
} else {
System.out.println("用户名或密码错误!");
}
}
// 使用Statement实现对数据表的查询操作
public <T> T get(String sql, Class<T> clazz) {
T t = null;
Connection conn = null;
Statement st = null;
ResultSet rs = null;
try {
// 1.加载配置文件
InputStream is = StatementTest.class.getClassLoader().getResourceAsStream("jdbc.properties");
Properties pros = new Properties();
pros.load(is);
// 2.读取配置信息
String user = pros.getProperty("user");
String password = pros.getProperty("password");
String url = pros.getProperty("url");
String driverClass = pros.getProperty("driverClass");
// 3.加载驱动
Class.forName(driverClass);
// 4.获取连接
conn = DriverManager.getConnection(url, user, password);
st = conn.createStatement();
rs = st.executeQuery(sql);
// 获取结果集的元数据
ResultSetMetaData rsmd = rs.getMetaData();
// 获取结果集的列数
int columnCount = rsmd.getColumnCount();
if (rs.next()) {
t = clazz.newInstance();
for (int i = 0; i < columnCount; i++) {
// 1. 获取列的别名
String columnName = rsmd.getColumnLabel(i + 1);
// 2. 根据列名获取对应数据表中的数据
Object columnVal = rs.getObject(columnName);
// 3. 将数据表中得到的数据,封装进对象
Field field = clazz.getDeclaredField(columnName);
field.setAccessible(true);
field.set(t, columnVal);
}
return t;
}
} catch (Exception e) {
e.printStackTrace();
} finally {
// 关闭资源
if (rs != null) {
try {
rs.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
if (st != null) {
try {
st.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
if (conn != null) {
try {
conn.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
}
return null;
}
}
使用Statement操作数据表的弊端
- 通过Connection对象的createStatement方法创建对象。该对象用于执行静态的SQL语句,并返回执行结果
Statement接口中定义了下列方法用于执行SQL语句:
int excuteUpdate(String sql)
执行更新操作INSERT、UPDATE、DELETEResultSet executeQuery(String sql)
执行查询操作
使用Statement操作数据表存在弊端:
- 存在拼串操作,比较繁琐
- 存在SQL注入问题
- SQL 注入是利用某些系统没有对用户输入的数据进行充分的检查,而在用户输入数据中注入非法的 SQL 语句段或命令(如:SELECT user, password FROM user_table WHERE user=’a’ OR 1 = ‘ AND password = ‘ OR ‘1’ = ‘1’) ,从而利用系统的 SQL 引擎完成恶意行为的做法。
- 对于Java而言,需要防范SQL注入,我们只需要使用PreparedStatement(从Statement扩展而来)取代Statement
PreparedStatement的使用
- 可以通过Connection对象的
PreparedStatement(String sql)
方法获取PreparedStatement对象 - PreparedStatement节后时候Statement的子接口,表示一条预编译过的SQL语句
Java与SQL对应数据类型装换表
Java类型 | SQL类型 |
---|---|
boolean | BIT |
byte | TINYINT |
short | SMALLINT |
int | INTEGER |
long | BIGINT |
String | CHAR,VARCHAR,LONGVARCHAR |
byte array | BINARY , VAR BINARY |
java.sql.Date | DATE |
java.sql.Time | TIME |
java.sql.Timestamp | TIMESTAMP |
使用PreparedStatement实现增删改操作
- PreparedStatementSQL语句使用 ? 充当占位符
public void testInsert(){
Connection conn = null;
PreparedStatement ps = null;
//读取配置文件的4个配置信息
try{
InputStream is = ClassLoader.getSystemClassLoader().getResourceAsStream("jdbc.properties");
Properties pros = new Properties();
pros.load(is);
String user = pros.getProperty("user");
String password = pros.getProperty("password");
String url = pros.getProperty("url");
String driverClass = pros.getProperty("driverClass");
//加载驱动
Class.forName(driverClass);
//获取连接
Connection conn = DriverManager.getConnection(url, user, password);
//预编译SQL语句返回PreparedStatement实列
//PreparedStatement使用?作为占位符?所代表的是对应填入的数据
String sql = "INSERT INTO customers(`name`, email, birth)VALUES(?, ?, ?)";
PreparedStatement ps = conn.PrepareStatement(sql);
//第一个值代表数据所代表的索引,第二个所代表字段的值
ps.setString(1, "张三");
ps.setString(2, "zhangsan@163.com");
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
java.util.Date date = sdf.parse("2020-10-1");
ps.setDate(3, new Date(date.getTime()));
//执行SQL语句
ps.execute();
}catch(Exception e){
e.printStackTrace();
}finally{
//关闭资源
try{
if (ps != null)
ps.close();
}catch(SQLException e){
e.printStackTrace();
}
try{
if (conn != null)
conn.close();
}catch(SQLException e){
e.printStackTrace();
}
}
}
连接操作和关闭资源操作每次执行SQL语句时都会使用,可以将这些代码写成方法便于使用
public class JDBCUtils{
public static Connection getConnection() throws SQLException, ClassNotFoundException, IOException {
InputStream is = ClassLoader.getSystemClassLoader().getResourceAsStream("jdbc.properties");
Properties pros = new Properties();
pros.load(is);
String user = pros.getProperty("user");
String password = pros.getProperty("password");
String url = pros.getProperty("url");
String driverClass = pros.getProperty("driverClass");
//加载驱动
Class.forName(driverClass);
//获取连接
Connection conn = DriverManager.getConnection(url, user, password);
return conn;
}
public static void close(Connection conn, PreparedStatement ps){
try {
if (conn != null)
conn.close();
} catch (SQLException e) {
e.printStackTrace();
}
try {
if (ps != null)
ps.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
}
- 实现PreparedStatement通用的增删改操作
//通用的增删改操作
//SQL中占位符的长度等于可变形参的长度
public void update(String sql, Object... args){
Conneciton conn = null;
PreparedStatement ps = null;
try{
//获取数据库连接
conn = JDBCUtils.getConnection();
//获取PreparedStatement对象实列预编译SQL语句
ps = conn.PrepareStatement(sql);
//填充占位符
for(int i = 0; i < args.length; i++){
ps.setObject(i + i, args[i]);
}
//执行SQL语句
ps.execute();
} catch(Exception e){
e. printStackTrace();
}finally{
//关闭资源
JDBCUtils.closeResource(conn, ps)
}
}
使用PreparedStatement实现查询操作
- 针对一条数据时的查询操作
import java.util.Date;
/**
* ORM编程思想
* 一个数据表对应一个Java类
* 表中的一条记录对应Java类的一个对象
* 表中的一个字段对应Java类的一个属性
*/
public class Customer {
private int id;
private String name;
private String email;
private Date birth;
public Customer() {
super();
}
public Customer(int id, String name, String email, Date birth) {
this.id = id;
this.name = name;
this.email = email;
this.birth = birth;
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
public Date getBirth() {
return birth;
}
public void setBirth(Date birth) {
this.birth = birth;
}
@Override
public String toString() {
return "Customer{" +
"id=" + id +
", name='" + name + '\'' +
", email='" + email + '\'' +
", birth=" + birth +
'}';
}
}
public void test(){
Connection conn = null;
PreparedStatement ps = null;
ResultSet re = null;
try {
//获取连接对象
conn = JDBCUtils.getConnection();
String sql = "SELECT id, name, email, birth FROM customers WHERE id = ?";
ps = conn.prepareStatement(sql);
ps.setObject(1,1);
//执行并返回结果集
re = ps.executeQuery();
//处理结果集
if(re.next()){
int id = re.getInt(1);
String name = re.getString(2);
String email = re.getString(3);
Date birth = re.getDate(4);
//方法1:直接输出结果集
System.out.println("id = " + id + ",name = " + name + ",email = " + email + ",birth = " + birth);
//方法2:使用数组输出结果集
Object[] data = {id, name, email, birth};
//方法3:将数据封装成一个对象输出(推荐)
Customer customer = new Customer(id, name, email, birth);
System.out.println(customer);
}
} catch (Exception e) {
e.printStackTrace();
}finally {
JDBCUtils.close(conn, ps, re);
}
}
- 针对一张表时的通用查询操作
public Customer queryFroCustomers(String sql, Object... args){
Connection conn = null;
PreparedStatement ps = null;
ResultSet rs = null;
Customer cust = null;
try {
conn = JDBCUtils.getConnection();
ps = conn.prepareStatement(sql);
for (int i = 0; i < args.length; i++){
ps.setObject(i + 1, args[i]);
}
rs = ps.executeQuery();
//获取结果集的元数据
ResultSetMetaData rsmd = rs.getMetaData();
//通过ResultSetMetaData获取结果集中的列数
int columnCount = rsmd.getColumnCount();
while (rs.next()) {
cust = new Customer();
//处理一列数据的结果集
for (int i = 0; i< columnCount; i++){
Object columnNameValue = rs.getObject(i + 1);
//获取每个列的列名
String columnName = rsmd.getColumnName(i + 1);
//给cust对象指定的columnName属性,赋值为columnNameValue
Field field = Customer.class.getDeclaredField(columnName);
field.setAccessible(true);
field.set(cust, columnNameValue);
}
return cust;
}
} catch (Exception e) {
e.printStackTrace();
} finally {
JDBCUtils.close(conn, ps, rs);
}
return null;
}
注意:如果表名和Java类中的属性名不相同时,可以给SQL语句的字段名取别名对应上Java类中的属性名,并将
getColumnName
替换成getColumnLable
,并且也更加推荐使用getColumnLable
- 针对所有表的查询操作
public <T> List<T> getInstance(Class<T> clazz, String sql, Object... args){
Connection conn = null;
PreparedStatement ps = null;
ResultSet rs = null;
try {
//获取连接对象
conn = JDBCUtils.getConnection();
//预编译SQL语句
ps = conn.prepareStatement(sql);
for (int i = 0; i < args.length; i++){
ps.setObject(i + 1, args[i]);
}
rs = ps.executeQuery();
//获取结果集的元数据
ResultSetMetaData rsmd = rs.getMetaData();
//通过ResultSetMetaData获取结果集中的列数
int columnCount = rsmd.getColumnCount();
//创建集合对象
ArrayList<T> list = new ArrayList<>();
while (rs.next()) {
//通过反射技术获取对象
T t = clazz.newInstance();
//处理一列数据的结果集
for (int i = 0; i< columnCount; i++){
Object columnNameValue = rs.getObject(i + 1);
//获取每个列的列名
String columnName = rsmd.getColumnLable(i + 1);
//给t对象指定的columnName属性,赋值为columnNameValue
Field field = Customer.class.getDeclaredField(columnName);
field.setAccessible(true);
field.set(t, columnNameValue);
}
list.add(t);
}
return list;
} catch (Exception e) {
e.printStackTrace();
} finally {
JDBCUtils.close(conn, ps, rs);
}
return null;
}
PreparedStatement操作BLOB类型字段
MySQL中,BLOB是一个二进制大型对象,是一个可以存储大量数据的容器,它能容纳不同大小的数据。
插入BLOB类型的数据必须使用PreparedStatement,因为BLOB类型的数据无法使用字符串拼接写的。
MySQL的四种BLOB类型(除了在存储的最大信息量上不同外,他们是等同的)
类型 | 大小(单位字节) |
---|---|
TinyBlob | 最大 225 |
Blob | 最大 65k |
MediumBlob | 最大 16M |
LongBlob | 最大 4G |
- 注意:如果储存的文件过大,会导致数据库的性能下降
使用PreparedStatement批量插入数据
public void test(){
Connection conn = null;
PreparedStatement ps = null;
try {
//计时
long start = System.currentTimeMillis();
conn = JDBCUtils.getConnection();
//设置不予许自动提交数据
conn.setAutoCommit(false);
String sql = "INSERT INTO goods(name)VALUES(?)";
ps = conn.prepareStatement(sql);
for (int i = 1; i <= 200000; i++){
ps.setObject(1, "name" + i);
//"攒sql"
ps.addBatch();
if (i % 500 == 0) {
//执行batch
ps.executeBatch();
//清空batch
ps.clearBatch();
}
}
//提交数据
conn.commit();
long end = System.currentTimeMillis();
System.out.println("花费的时间为" + (end - start));
} catch (Exception e) {
e.printStackTrace();
} finally {
JDBCUtils.close(conn, ps);
}
}
数据库事务
数据库事务介绍
- 事务:一组逻辑操作单元,使数据从一种状态变换到另一种状态。
- 事务处理(事务操作):保证所有事务都作为一个工作单元来执行,即使出现了故障,都不能改变这种执行方式。当在一个事务中执行多个操作时,要么所有的事务都被提交(commit),那么这些修改就永久地保存下来;要么数据库管理系统将放弃所作的所有修改,整个事务回滚(rollback)到最初状态。
- 为确保数据库中数据的一致性,数据的操纵应当是离散的成组的逻辑单元:当它全部完成时,数据的一致性可以保持,而当这个单元中的一部分操作失败,整个事务应全部视为错误,所有从起始点以后的操作应全部回退到开始状态。
JDBC事务处理
- 数据一旦提交,就不可回滚。
那些操作会导致数据自动提交?
- DDL操作一旦执行就会自动提交
- DML默认情况下,一旦执行就会自动提交
- 默认关闭连接时,会自动提交数据
JDBC程序中为了让多个SQL语句作为一个事物执行
- 调用Connection对象的
setAutoCommit(false)
以取消自动提交事务 - 在所有的SQL语句确保执行成功后,再调用commit()提交事务
- 在出现异常时,调用
Rollback()
方法回滚事务
- 调用Connection对象的
若此时 Connection 没有被关闭,还可能被重复使用,则需要恢复其自动提交状态 setAutoCommit(true)。尤其是在使用数据库连接池技术时,执行close()方法前,建议恢复自动提交状态。
- 案列:模拟数据库事务提交异常
public void testJDBCTransaction(){
Connection conn = null;
//获取数据库连接
try {
conn = JDBCUtils.getConnection();
//开启事务
conn.setAutoCommit(false);
//进行数据库操作
String sql1 = "UPDATE user_table SET balance = balance - 100 WHERE user = ?";
upDate(conn, sql1, "AA");
//模拟网络异常
// System.out.println(10 / 0);
String sql2 = "UPDATE user_table SET balance = balance + 100 WHERE user = ?";
upDate(conn, sql2, "BB");
//如果没有异常那么就提交事务
conn.commit();
} catch (Exception e) {
e.printStackTrace();
//如果有异常那么就回滚事务
try {
conn.rollback();
} catch (SQLException ex) {
ex.printStackTrace();
}
} finally {
//恢复每次DML操作的自动提交功能
//主要针对于数据库连接池使用
try {
conn.setAutoCommit(true);
} catch (SQLException e) {
e.printStackTrace();
}
//7.关闭连接
JDBCUtils.closeResource(conn, null);
}
}
- 对数据的操作方法
public void upDate(Connection conn, String sql, Object... args){
PreparedStatement ps = null;
try {
//获取PreparedStatement实列
ps = conn.prepareStatement(sql);
//填充占位符
for (int i = 0; i < args.length; i++){
ps.setObject(i + 1, args[i]);
}
//执行SQL语句
ps.execute();
} catch (SQLException e) {
e.printStackTrace();
} finally {
//首先不关闭Connection等确定数据库操作成功后再自行手动关闭资源
JDBCUtils.close(null, ps);
}
}
事务的ACID属性
原子性(Atomicity)
原子性是指事务是一个不可分割的工作单位,事务中的操作要么都发生,要么都不发生。一致性(Consistency)
事务必须使数据库从一个一致性状态变换到另外一个一致性状态。隔离性(Isolation)
事务的隔离性是指一个事务的执行不能被其他事务干扰,即一个事务内部的操作及使用的数据对并发的其他事务是隔离的,并发执行的各个事务之间不能互相干扰。持久性(Durability)
持久性是指一个事务一旦被提交,它对数据库中数据的改变就是永久性的,接下来的其他操作和数据库故障不应该对其有任何影响。
事务并发问题
对于同时运行的多个事务, 当这些事务访问数据库中相同的数据时, 如果没有采取必要的隔离机制, 就会导致各种并发问题:
- 脏读: 对于两个事务 T1, T2, T1 读取了已经被 T2 更新但还没有被提交的字段。
- 不可重复读: 对于两个事务T1, T2, T1 读取了一个字段, 然后 T2 更新了该字段。之后, T1再次读取同一个字段, 值就不同了。
- 幻读: 对于两个事务T1, T2, T1 从一个表中读取了一个字段, 然后 T2 在该表中插入了一些新的行。之后, 如果 T1 再次读取同一个表, 就会多出几行。
数据库事务的隔离性: 数据库系统必须具有隔离并发运行各个事务的能力, 使它们不会相互影响, 避免各种并发问题。
一个事务与其他事务隔离的程度称为隔离级别。数据库规定了多种事务隔离级别, 不同隔离级别对应不同的干扰程度, 隔离级别越高, 数据一致性就越好, 但并发性越弱。
数据库的四种隔离级别
- 数据库提供的四种隔离级别
Oracle 支持的 2 种事务隔离级别:READ COMMITED, SERIALIZABLE。 Oracle 默认的事务隔离级别为: READ COMMITED 。
Mysql 支持 4 种事务隔离级别。Mysql 默认的事务隔离级别为: REPEATABLE READ。
在MySQL中设置隔离级别
每启动一个 mysql 程序, 就会获得一个单独的数据库连接. 每个数据库连接都有一个全局变量 @@tx_isolation, 表示当前的事务隔离级别。
查看当前隔离级别
SELECT @@tx_isolation;
- 设置当前 mySQL 连接的隔离级别:
set transaction isolation level read committed;
- 设置数据库系统的全局的隔离级别:
set global transaction isolation level read committed;
DAO及相关实现类
- DAO:Data Access Object访问数据信息的类和接口,包括了对数据的CRUD(Create、Retrival、Update、Delete),而不包含任何业务相关的信息。有时也称作:BaseDAO
- 作用:为了实现功能的模块化,更有利于代码的维护和升级。
Druid(德鲁伊) 数据库连接池
- Druid是阿里巴巴开源平台上一个数据库连接池实现,它结合了C3P0、DBCP、Proxool等DB池的优点,同时加入了日志监控,可以很好的监控DB池连接和SQL的执行情况,可以说是针对监控而生的DB连接池,可以说是目前最好的连接池之一。
public void test() throws Exception {
Properties pros = new Properties();
InputStream is = ClassLoader.getSystemClassLoader().getResourceAsStream("druid.properties");
pros.load(is);
DataSource source = DruidDataSourceFactory.createDataSource(pros);
Connection conn = source.getConnection();
System.out.println(conn);
QueryRunner runner = new QueryRunner();
}
- src配置文件
url=jdbc:mysql://localhost:3306/test?rewriteBatchedStatements=true
username=root
password=123456
driverClassName=com.mysql.jdbc.Driver
initialSize=10
maxActive=20
- 详细参数 | 配置 | 缺省 | 说明 | | —- | —- | —- | | name | | 配置这个属性的意义在于,如果存在多个数据源,监控的时候可以通过名字来区分开来。 如果没有配置,将会生成一个名字,格式是:”DataSource-” + System.identityHashCode(this) | | url | | 连接数据库的url,不同数据库不一样。例如:mysql : jdbc:mysql://10.20.153.104:3306/druid2 oracle : jdbc:oracle:thin:@10.20.149.85:1521:ocnauto | | username | | 连接数据库的用户名 | | password | | 连接数据库的密码。如果你不希望密码直接写在配置文件中,可以使用ConfigFilter。详细看这里:https://github.com/alibaba/druid/wiki/使用ConfigFilter | | driverClassName | | 根据url自动识别 这一项可配可不配,如果不配置druid会根据url自动识别dbType,然后选择相应的driverClassName(建议配置下) | | initialSize | 0 | 初始化时建立物理连接的个数。初始化发生在显示调用init方法,或者第一次getConnection时 | | maxActive | 8 | 最大连接池数量 | | maxIdle | 8 | 已经不再使用,配置了也没效果 | | minIdle | | 最小连接池数量 | | maxWait | | 获取连接时最大等待时间,单位毫秒。配置了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属性的说明 | | numTestsPerEvictionRun | | 不再使用,一个DruidDataSource只支持一个EvictionRun | | minEvictableIdleTimeMillis | | | | connectionInitSqls | | 物理连接初始化的时候执行的sql | | exceptionSorter | | 根据dbType自动识别 当数据库抛出一些不可恢复的异常时,抛弃连接 | | filters | | 属性类型是字符串,通过别名的方式配置扩展插件,常用的插件有: 监控统计用的filter:stat日志用的filter:log4j防御sql注入的filter:wall | | proxyFilters | | 类型是List,如果同时配置了filters和proxyFilters,是组合关系,并非替换关系 |
使用Apache-DBUtils实现CRUD操作
Apache-DBUtils简介
commons-dbutils 是 Apache 组织提供的一个开源 JDBC工具类库,它是对JDBC的简单封装,学习成本极低,并且使用dbutils能极大简化jdbc编码的工作量,同时也不会影响程序的性能。
先导入DBUtils包
主要AIP使用
DbUtils :提供如关闭连接、装载JDBC驱动程序等常规工作的工具类,里面的所有方法都是静态的。主要方法如下:
public static void close(…) throws java.sql.SQLException: DbUtils类提供了三个重载的关闭方法。这些方法检查所提供的参数是不是NULL,如果不是的话,它们就关闭Connection、Statement和ResultSet。
public static void closeQuietly(…): 这一类方法不仅能在Connection、Statement和ResultSet为NULL情况下避免关闭,还能隐藏一些在程序中抛出的SQLEeception。
public static void commitAndClose(Connection conn)throws SQLException: 用来提交连接的事务,然后关闭连接
public static void commitAndCloseQuietly(Connection conn): 用来提交连接,然后关闭连接,并且在关闭连接时不抛出SQL异常。
public static void rollback(Connection conn)throws SQLException:允许conn为null,因为方法内部做了判断
public static void rollbackAndClose(Connection conn)throws SQLException
rollbackAndCloseQuietly(Connection)
public static boolean loadDriver(java.lang.String driverClassName):这一方装载并注册JDBC驱动程序,如果成功就返回true。使用该方法,你不需要捕捉这个异常ClassNotFoundException。
- 测试
//添加
public void testDBUtils() throws SQLException, IOException, ClassNotFoundException {
QueryRunner runner = new QueryRunner();
Connection conn = JDBCUtils.getConnection();
String sql = "insert into customers(name,email,birth)values(?,?,?)";
int count = runner.update(conn, sql, "张三", "zhangsan@qq.com", "1992-09-08");
JDBCUtils.close(conn, null);
System.out.println("添加了" + count + "条记录");
}
//删除
public void testDelete() throws Exception {
QueryRunner runner = new QueryRunner();
Connection conn = JDBCUtils.getConnection();
String sql = "delete from customers where id < ?";
int count = runner.update(conn, sql,3);
System.out.println("删除了" + count + "条记录");
JDBCUtils.closeResource(conn, null);
}
ResultSetHandler接口及实现类
该接口用于处理 java.sql.ResultSet,将数据按要求转换为另一种形式。
ResultSetHandler 接口提供了一个单独的方法:Object handle (java.sql.ResultSet .rs)。
接口的主要实现类:
ArrayHandler:把结果集中的第一行数据转成对象数组。
ArrayListHandler:把结果集中的每一行数据都转成一个数组,再存放到List中。
BeanHandler:将结果集中的第一行数据封装到一个对应的JavaBean实例中。
BeanListHandler:将结果集中的每一行数据都封装到一个对应的JavaBean实例中,存放到List里。
ColumnListHandler:将结果集中某一列的数据存放到List中。
KeyedHandler(name):将结果集中的每一行数据都封装到一个Map里,再把这些map再存到一个map里,其key为指定的key。
MapHandler:将结果集中的第一行数据封装到一个Map里,key是列名,value就是对应的值。
MapListHandler:将结果集中的每一行数据都封装到一个Map里,然后再存放到List
ScalarHandler:查询单个值对象
测试
/*
* 测试查询:查询一条记录
*
* 使用ResultSetHandler的实现类:BeanHandler
*/
@Test
public void testQueryInstance() throws Exception{
QueryRunner runner = new QueryRunner();
Connection conn = JDBCUtils.getConnection();
String sql = "select id,name,email,birth from customers where id = ?";
//
BeanHandler<Customer> handler = new BeanHandler<>(Customer.class);
Customer customer = runner.query(conn, sql, handler, 23);
System.out.println(customer);
JDBCUtils.closeResource(conn, null);
}
/*
* 测试查询:查询多条记录构成的集合
*
* 使用ResultSetHandler的实现类:BeanListHandler
*/
@Test
public void testQueryList() throws Exception{
QueryRunner runner = new QueryRunner();
Connection conn = JDBCUtils.getConnection();
String sql = "select id,name,email,birth from customers where id < ?";
//
BeanListHandler<Customer> handler = new BeanListHandler<>(Customer.class);
List<Customer> list = runner.query(conn, sql, handler, 23);
list.forEach(System.out::println);
JDBCUtils.closeResource(conn, null);
}