一、JDBC概念
1.JDBC(Java 数据库连接)是用于执行SQL语句的API,可以为多种关系型数据库提供统一访问,由一组Java语言编写的类和接口组成。JDBC为工具/数据库开发人员提供了一个标准的API,据此可以构建更高级的工具和接口,使开发人员能够用纯JavaAPI编写数据库应用程序。JDBC驱动程序共分四种类型:
(1)JDBC-ODBC桥
把所有JDBC的调用传递给ODBC,再让后者调用数据库本地驱动代码。
(2)本地API驱动
通过客户端加载数据库厂商提供的本地代码库来访问数据库,而在驱动程序中则包含了Java代码。
(3)网络协议驱动
给客户端提供了一个网络API,客户端上的JDBC驱动程序使用套接字socket来调用服务器上的中间件程序,后者将其请求转化为所需的具体API调用。
(4)本地协议驱动
使用套接字socket,直接在客户端和数据库间通信。
2.JDBC API主要位于JDK中的java.sql包中,主要包括:
(1)DriverManager类:负责加载各种不同的驱动程序(Driver),并根据不同的请求,向调用者返回相应的数据库连接(Connection).
(2)Driver类:驱动程序,会将自身加载到Driver Manager类中,并处理相应的请求,返回相应的数据库连接(Connection).
(3)Connection:数据库连接,负责与数据库间进行通信,SQL执行以及事务处理都是在某一个特定Connection环境中进行的,可以产生用以执行SQL的Statement对象。
(4)Statement:用以执行不带参数的简单SQL查询和更新(针对静态SQL语句和单次执行)。
(5)PreparedStatement:用以执行包含动态参数的SQL查询和更新(在服务器端编译,允许重复执行以提高效率)。
(6)CallableStatement:用以调用数据库中的存储过程
(7)SQLException:代表在数据库连接的创建和关闭以及在SQL语句执行过程中发生了例外情况(即错误)。
简单的说,JDBC可以做三件事:
① 与数据库建立连接
② 发送操作数据库的SQL语句(SQL语句的具体执行不在Java中,而是在数据库引擎中,因为SQL语句和Java代码本身是不一样的,它只有数据库引擎认识,才能够去执行)。在java代码中是通过以下的方法发送SQL语句的:
executeQuery():执行查询语句,返回结果集ResultSet(默认每次返回10条记录到该ResultSet中(缓存)),我们也可以通过ResultSet的setFetchSize方法来设置返回的记录的个数;
executeUpdate():执行DML和DDL语句,返回int类型
execute():执行任何SQL语句,返回boolean类型,指示是否有ResultSet返回
③ 处理结果
三种Statement的比较:
一般使用PreparedStatement来对数据库做增删改查操作,此外PreparedStatement和CallableStatement称为同构的Statement,而Statement本身称为异构的Statement。
3.创建数据库连接(以MySQL为例)
(1)要使Java程序能够连接到数据库,需要获得相应的JDBC驱动程序,一般使用驱动jar文件,也就是第二类驱动的方式,即将相应的驱动jar引入项目的path路径下即可。(驱动jar:mysql-connector-java-8.0.13.jar)
(2)在java程序中完成以下两步操作即可获得与数据库的Connection连接对象:
① 第一步:加载数据库驱动程序:Class.forName("com.mysql.jdbc.Driver");
②第二步:建立连接:
Connection conn = DriverManager.getConnection("jdbc:MySql://localhost/databaseName","root","***"); // 其中第一个参数是:协议URL;第二个参数是:用户名;第三个参数是:密码。
4.发送操作数据库的SQL语句
连接一旦建立,就可以用来向数据库发送SQL语句,JDBC提供了三个类用于向数据库发送SQL语句,Connection接口中的三个方法可用于创建这些类的实例:
(1)Statement:由Connection对象的createStatement()方法创建,该对象用于发送简单的SQL语句。
(2)PreparedStatement:由PrepareStatement()方法创建,该对象用于发送带有参数的SQL语句。
(3)CallableStatement:由prepareCall()方法创建,该对象用于执行SQL存储过程。
5.处理结果
通过Statement、PreparedStatement、CallableStatement对象的execute/executeXXX()方法完成向数据库发送操作语句,数据库引擎执行后会返回一个结果,这个结果主要有两种形式:
(1)对于更新语句(插入、更新、删除操作)完成后返回一个受操作影响的行数。
(2)对于查询语句,返回查询的结果集ResultSet对象。ResultSet对象的next()方法下移结果集中记录指针,并判断当前记录指针是否指向空(ResultSet光标最初位于结果集第一行之前);ResultSet对象的getXXX()方法用来读取结果集中当前记录的字段信息。
二、JDBC的高级使用
1.ConnectionFactory
(1)ConnectionFactory的作用:
利用简单工厂模式提升代码的重用性;封装注册数据库的驱动和获得与数据库的连接;利用配置文件的方式减少硬编码,便于维护。
(2)ConnectionFactory的开发:
① 创建配置文件:jdbcinfo.properties
mysql.driver=com.mysql.cj.jdbc.Driver
mysql.url=jdbc:mysql://localhost:3306/test
mysql.user=root
mysql.password=0303
②配置文件信息的获取:ConnectionFactory.class.getResourceAsStream("/jdbcinfo.properties");
③获取数据库连接Connection
import java.io.IOException;
import java.io.InputStream;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.util.Properties;
public class ConnectionFactory {
private static String DRIVER;
private static String URL;
private static String USER;
private static String PASSWORD;
static {
// 创建一个无默认值的空属性列表
Properties properties = new Properties();
// 获取配置文件的信息
InputStream resourceAsStream = ConnectionFactory.class.getResourceAsStream("/jdbcinfo.properties");
try {
// 从输入流中获取属性列表(键值元素对)
properties.load(resourceAsStream);
DRIVER = properties.getProperty("mysql.driver");
URL = properties.getProperty("mysql.url");
USER = properties.getProperty("mysql.user");
PASSWORD = properties.getProperty("mysql.password");
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* 获取数据库连接
* @return
*/
public static Connection getConnection() {
Connection conn = null;
try {
// 加载数据库驱动程序
Class.forName(DRIVER);
// 建立连接
conn = DriverManager.getConnection(URL, USER, PASSWORD);
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (SQLException e) {
e.printStackTrace();
}
return conn;
}
}
示例:
(1)test.domain.Student
public class Student implements Serializable {
private static final long serialVersionUID = -4387659127666372278L;
private int id;
private String name;
private String address;
private String gender;
private int age;
public Student() {
}
public Student(int id, String name, String address, String gender, int age) {
this.id = id;
this.name = name;
this.address = address;
this.gender = gender;
this.age = age;
}
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 getAddress() {
return address;
}
public void setAddress(String address) {
this.address = address;
}
public String getGender() {
return gender;
}
public void setGender(String gender) {
this.gender = gender;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
@Override
public String toString() {
return "Student [id=" + id + ", name=" + name + ", address=" + address
+ ", gender=" + gender + ", age=" + age + "]";
}
}
(2)test.util.StudentJdbc
package test.util;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;
import test.domain.Student;
import test.jdbc.ConnectionFactory;
public class StudentJdbc {
private Connection conn = null;
PreparedStatement pstmt = null;
ResultSet rs = null;
/**
* <p>添加</p>
* @param student
* @throws SQLException
*/
public void save(Student student) throws SQLException {
// 建立连接
conn = ConnectionFactory.getConnection();
// 此时已经开启了一个事务,设置手动提交事务
conn.setAutoCommit(false);
// 插入一条记录
String insertSql = "insert into student values(?,?,?,?,?)";
// 创建发送SQL语句的PreparedStatement对象
pstmt = conn.prepareStatement(insertSql);
// 替换占位符
pstmt.setInt(1, student.getId());
pstmt.setString(2, student.getName());
pstmt.setString(3, student.getAddress());
pstmt.setString(4, student.getGender());
pstmt.setInt(5, student.getAge());
// 发送操作的SQL语句
int rows = pstmt.executeUpdate();
// 提交事务
conn.commit();
System.out.println("成功插入" + rows + "条记录。");
// 关闭连接
conn.close();
}
/**
* <p>更新</p>
* @param stu
* @throws SQLException
*/
public void update(Student stu) throws SQLException {
// 建立连接
conn = ConnectionFactory.getConnection();
// 设置手动提交事务
conn.setAutoCommit(false);
// 更新一条记录
String updateSql = "update student "
+ "set name = ?, address = ?, gender = ?, age = ? "
+ "where id = ?";
// 创建发送SQL的PreparedStatement对象
pstmt = conn.prepareStatement(updateSql);
// 替换占位符
pstmt.setString(1, stu.getName());
pstmt.setString(2, stu.getAddress());
pstmt.setString(3, stu.getGender());
pstmt.setInt(4, stu.getAge());
pstmt.setInt(5, stu.getId());
// 发送更新的SQL语句
int rows = pstmt.executeUpdate();
// 提交事务
conn.commit();
System.out.println("成功更新记录" + rows + "条");
}
/**
* <p>删除</p>
* @param id
* @throws SQLException
*/
public void delete(int id) throws SQLException {
// 建立连接
conn = ConnectionFactory.getConnection();
// 此时已经开启了一个事务,设置手动提交事务
conn.setAutoCommit(false);
// 删除一条记录
String deleteSql = "delete from student where id = ?";
// 创建发送SQL语句的PreparedStatement对象
pstmt = conn.prepareStatement(deleteSql);
// 替换占位符
pstmt.setInt(1, 3);
// 发送操作的SQL语句
int rows = pstmt.executeUpdate();
// 提交事务
conn.commit();
System.out.println("成功删除" + rows + "条记录。");
// 关闭连接
conn.close();
}
/**
* <p>根据主键,查询一条数据</p>
* @param id
* @return
* @throws SQLException
*/
public Student load(int id) throws SQLException {
Student student = new Student();
conn = ConnectionFactory.getConnection();
conn.setAutoCommit(false);
String loadSql = "select * from student where id = ?";
pstmt = conn.prepareStatement(loadSql);
pstmt.setInt(1, id);
rs = pstmt.executeQuery();
while(rs.next()) {
student.setId(rs.getInt(1));
student.setName(rs.getString(2));
student.setAddress(rs.getString(3));
student.setGender(rs.getString(4));
student.setAge(rs.getInt(5));
conn.commit();
}
return student;
}
/**
* <p>查詢所有學生信息</p>
*
* @return
* @throws SQLException
*/
public List<Student> find() throws SQLException {
// 建立一個空List
List<Student> stus = new ArrayList<>();
// 建立連接
conn = ConnectionFactory.getConnection();
// 設置手動提交事務
conn.setAutoCommit(false);
// 查詢SQL
String findSql = "select * from student";
// 創建發送SQL的PreparedStatement對象
pstmt = conn.prepareStatement(findSql);
// 發送SQL語句
rs = pstmt.executeQuery();
// 遍歷結果集
while(rs.next()) {
Student student = new Student();
student.setId(rs.getInt(1));
student.setName(rs.getString(2));
student.setAddress(rs.getString(3));
student.setGender(rs.getString(4));
student.setAge(rs.getInt(5));
stus.add(student);
// 提交事務
conn.commit();
}
return stus;
}
}
(3)test.TestDemo
package test;
import java.sql.SQLException;
import java.util.List;
import test.domain.Student;
import test.util.StudentJdbc;
public class TestDemo {
public static void main(String[] args) {
Student student = new Student();
student.setId(3);
student.setName("王五");
student.setAddress("北京市朝阳区");
student.setGender("男");
student.setAge(25);
StudentJdbc studentJdbc = new StudentJdbc();
try {
// 添加一条记录
// studentJdbc.save(student);
// 删除一条记录
// studentJdbc.delete(3);
// 修改一条记录
// student.setAddress("廣東省深圳市");
// studentJdbc.update(student);
// 查询一条记录
// System.out.println(studentJdbc.load(3));
// 查詢所有記錄
List<Student> students = studentJdbc.find();
for (Student stu : students) {
System.out.println(stu);
}
} catch (SQLException e) {
e.printStackTrace();
}
}
}
三、事务提交模式
(1)自动提交模式
默认方式,每执行完一条SQL语句就自动提交事务,每条SQL语句的执行都被单独提交,因此一个事务只有一条SQL语句组成。
(2)手动提交模式
这种模式是通过Connection对象的setAutoCommit(false)的方式来设置的,手动提交模式允许一个事务中包含若干条SQL语句,这样会将SQL操作保存到数据库中。
(3)事务并发引发的问题:
① 脏读(dirty read):一个事务读取另一个事务未提交的数据(脏数据)
② 不可重复读(UNrepeatable read):一个事务运行期间两次读取相同数据,但内容不一致(原因就是在这期间另一个事务对数据做了修改)
③ 虚读/幻影读(phantom read):一个事务运行期间两次读取相同统计数据但内容不一致,原因是两次读取期间有其他事务执行了插入或删除符合统计条件的数据并提交。
(4)事务隔离级别
① 通过设置事务的隔离级别,来解决脏读、不可重复读、幻影读问题
② 五种事务隔离级别:
a. TRANSACTION_NONE(0):不支持事务
b. TRANSACTION_READ_ONCOMMITTED(1):可读未提交数据
c. TRANSACTION_READ_COMMITTED(2):避免脏读
d. TRANSACTION_REPEATABLE_READ(4):避免不可重复读
e. TRANSACTION_SERIALIABLE(8):避免幻影读
说明:
① 事务的隔离级别越高,能避免的问题越多,但是并发性能越差,一般选择级别2,也就是避免脏读这个问题。
② Oracle的默认事务隔离级别是TRANSACTION_READ_COMMITTED和TRANSACTION_SERIALIABLE
③ 在事务启动下我们可以获取当前数据库的事务隔离级别和设置当前事务的隔离级别:
Connection.getTransactionIsolation(); // 获取事务隔离级别
Connection.setTransactionIsolation(Connection.TRANSACTION_SERIALIABLE); // 设置事务隔离级别
四、批处理
(1)批处理的作用
一次和数据库交互过程中传输多条SQL语句或参数内容,减少和数据库交互的次数,提升性能。
(2)批处理的使用
①Statement
a.addBatch(); // 将当前SQL语句交给Statement,保存到Statement维护的缓存区中
b.executeBacth(); // 将多条SQL语句在一次和数据库交互过程中执行
② PreparedStatement
a.addBacth(); // 将当前参数内容交给PreparedStatement
b.executeBatch(); // 将多条记录的参数内容在一次和数据库交互过程中传输给数据库
public class BatchTest {
public static void main(String[] args) {
PreparedStatement pstmt = null;
Connection conn = null;
try {
// 获取数据库连接
conn = ConnectionFactory.getConnection();
String sql = "insert into student values(?,?,?,?,?)";
// 在批处理中要采用手动提交事务
conn.setAutoCommit(false);
pstmt = conn.prepareStatement(sql);
// 保存插入的记录数
int count = 0;
long t1 = System.currentTimeMillis();
for (int i = 1; i <= 10000; i++) {
// 替换占位符
pstmt.setInt(1, i);
pstmt.setString(2, "testName" + i);
pstmt.setString(3, "testAddress" + i);
pstmt.setString(4, "男");
pstmt.setInt(5, 25);
// 将替换占位符的参数(或者SQL语句)暂时保存到PreparedStatement维护的缓存中
pstmt.addBatch();
// 记录数加1
count++;
}
// 批处理执行,将替换占位符的所有参数或者SQL一次性发送到数据库端
pstmt.executeBatch();
// 批处理执行后再提交事务
conn.commit();
long t2 = System.currentTimeMillis();
System.out.println("共插入记录" + count + "条,耗时:" + (t2 - t1) / 1000 + "s");
// 关闭连接
conn.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
}
五、DAO模式
(1)DAO模式的使用目的
① 封装和隐藏数据库的访问细节
② 将业务逻辑和数据库的访问细节实现隔离
③ 降低耦合度
(2)DAO模式的设计
① 提供一个DAO接口
② 提供DAO接口的实现类
六、MD5加密
MD5的典型应用是对一段信息(Message)产生信息摘要(Message-Digest),即把一个任意长度的字符串变换成一定长度的大整数,以防止被篡改。
import java.io.UnsupportedEncodingException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Base64;
import java.util.Base64.Encoder;
public class MD5Demo {
public static void main(String[] args) {
String str = "12345";
try {
// 返回实现指定摘要算法的MessageDigest对象(获取一个MD5的转换器)
MessageDigest md5 = MessageDigest.getInstance("MD5");
// 使用指定的byte数组对摘要进行最后的更新,然后完成摘要的计算
byte[] digest = md5.digest(str.getBytes("UTF-8"));
// Base64是一种常用的字符编码,其算法主要用于将二进制数据转换为ASCII字符串格式
Encoder encoder = Base64.getEncoder();
String encodeToString = encoder.encodeToString(digest);
// gnzLDuqKcGxMNKFokfhOew== :字符串中的最后的=,代表字符串编码的结束
System.out.println(encodeToString);
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
}
}
说明:
MessageDigest类为应用程序提供信息摘要算法的功能,如MD5或SHA算法。信息摘要是安全的单向哈希函数,它接收任意大小的数据,并输出固定长度的哈希值(24位),该长度等于Unicode代码单元中的字符串的数目。
七、解析properties文件
package test;
import java.io.IOException;
import java.io.InputStream;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Properties;
public class ParsePropertiesFile {
public static void main(String[] args) {
MyConfig myConfig = new MyConfig();
myConfig.config();
String name = myConfig.getPrperties("name");
String password = myConfig.getPrperties("password");
System.out.println(name);
System.out.println(password);
}
}
class MyConfig {
private Map<String, String> propertiesMap;
public MyConfig() {
propertiesMap = new HashMap<>();
}
public void config() {
// 创建一个无默认值的空属性列表
Properties properties = new Properties();
// 获取配置文件信息
InputStream inputStream = MyConfig.class.getResourceAsStream("config.properties");
try {
// 读取输入流中的配置文件到属性列表
properties.load(inputStream);
// 将配置文件以键值对的形式存储到Map容器中
Iterator<String> iterator = properties.stringPropertyNames().iterator();
while (iterator.hasNext()) {
String str = iterator.next();
propertiesMap.put(str, properties.getProperty(str));
}
} catch (IOException e) {
System.out.println("文件不存在");
e.printStackTrace();
} finally {
try {
inputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
public String getPrperties(String key) {
return propertiesMap.get(key);
}
}