持久化(persistence):把数据保存到可掉电式存储设备中以供之后使用。大多数情况下,特别是企业级应用, 数据持久化意味着将内存中的数据保存到硬盘上加以”固化”,而持久化的实现过程大多通过各种关系数据库来完成
持久化的主要应用是将内存中的数据存储在关系型数据库中,当然也可以存储在磁盘文件、XML数据文件中
在Java中,数据库存取数据的技术可分为如下几类:
JDBC是Java访问数据库的基石,Hibernate、MyBatis、Spring Data JPA等只是更好的封装了JDBC
全称:(Java DataBase Connectivity)Java数据库连接
JDBC就是使用Java语言操作关系型数据库的一套API
JDBC本质
同一套Java代码,操作不同的关系型数据库
JDBC的好处
mysql-connector-java-8.0.28.jar
public static void main(String[] args) throws ClassNotFoundException, SQLException {
// 1.注册驱动
Class.forName("com.mysql.cj.jdbc.Driver");
// 2.获取数据库连接对象
String url = "jdbc:mysql://localhost:3306/front_db?useSSL=true&useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai";
String username = "root";
String password = "root";
Connection conn = DriverManager.getConnection(url, username, password);
// 3.定义sql
String sql = "insert front_db.users values (1005,'小黑','1314',19,1)";
// 4.获取执行sql的对象 Statement
Statement stmt = conn.createStatement();
// 5.执行sql
int affectRow = stmt.executeUpdate(sql);
// 6.处理结果
System.out.println("affectRow = " + affectRow);
// 7.释放资源
if (stmt != null) {
stmt.close();
}
if (conn != null) {
conn.close();
}
}
编写jdbc.properties
配置文件
# 驱动包路径
driver=com.mysql.cj.jdbc.Driver
# 协议://主机地址:端口号/数据库名?参数1&参数2&参数3&参数n
url=jdbc:mysql://localhost:3306/test?useSSL=true&useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai
# 用户名
username=root
# 密码
password=root
使用配置文件的方式保存配置信息,在代码中加载配置文件
public static void main(String[] args) throws Exception {
// 获取连接
Connection conn = getConnection();
// 获取执行SQL对象
Statement stmt = conn.createStatement();
// 定义SQL
String sql = "select id, username, password, age, status from front_db.users";
// 执行SQL
ResultSet rs = stmt.executeQuery(sql);
List<User> userList = new ArrayList<>();
// 处理结果
while (rs.next()) {
int id = rs.getInt("id");
String username = rs.getString("username");
String password = rs.getString("password");
int age = rs.getInt("age");
int status = rs.getInt("status");
userList.add(new User(id, username, password, age, status));
}
System.out.println(userList);
if (rs != null) rs.close();
if (stmt != null) stmt.close();
if (conn != null) conn.close();
}
public static Connection getConnection() throws Exception {
// 1.读取配置文件中4个连接数据库的基本信息
InputStream is = ClassLoader.getSystemClassLoader().getResourceAsStream("jdbc.properties");
Properties prop = new Properties();
prop.load(is);
String driver = prop.getProperty("driver");
String url = prop.getProperty("url");
String username = prop.getProperty("username");
String password = prop.getProperty("password");
System.out.println(driver + "\n" + url + "\n" + username + "\n" + password);
// 2.加载驱动
Class.forName(driver);
// 3.获取连接
return DriverManager.getConnection(url, username, password);
}
使用配置文件的好处:
①实现了代码和数据的分离,如果需要修改配置信息,直接在配置文件中修改,无需深入代码
②如果修改了配置信息,省去重新编译的过程
SQL注入是通过操作输入来修改事先定义好的SQL语句,用以达到执行代码对服务器进行攻击的方法
使用Statement操作数据库表存在弊端:
public static void main(String[] args) throws Exception {
Scanner input = new Scanner(System.in);
Class.forName("com.mysql.cj.jdbc.Driver");
String url = "jdbc:mysql://localhost:3306/front_db?useSSL=true&useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai";
String username = "root";
String password = "root";
Connection conn = DriverManager.getConnection(url, username, password);
System.out.print("请输入用户名:");
String uName = input.nextLine();
System.out.print("请输入密码:");
String uPwd = input.nextLine(); // ' or '1' = '1
String sql = "select * from front_db.users where username = '" + uName + "' and password = '" + uPwd + "'";
Statement stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery(sql);
if (rs.next()) {
System.out.println("登录成功");
} else {
System.out.println("登录失败");
}
if (rs != null) rs.close();
if (stmt != null) stmt.close();
if (conn != null) conn.close();
}
对于 Java 而言,要防止 SQL 注入,只要用 PreparedStatement(从Statement扩展而来) 取代 Statement 就可以了
public static void main(String[] args) throws Exception {
Scanner input = new Scanner(System.in);
// MySQL5之后的驱动包,可以省略注册驱动的步骤
// Class.forName("com.mysql.cj.jdbc.Driver");
// useServerPrepStmts=true 参数开启预编译功能
String url = "jdbc:mysql://localhost:3306/front_db?useSSL=true&useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai&useServerPrepStmts=true";
String username = "root";
String password = "root";
Connection conn = DriverManager.getConnection(url, username, password);
// 预编译SQL
String sql = "select * from front_db.users where username = ? and password = ?";
PreparedStatement ps = conn.prepareStatement(sql);
System.out.print("请输入用户名:");
String uName = input.nextLine();
System.out.print("请输入密码:");
String uPwd = input.nextLine(); // ' or '1' = '1
// 填充占位符
ps.setString(1, uName);
ps.setString(2, uPwd);
ResultSet rs = ps.executeQuery();
if (rs.next()) {
System.out.println("登录成功");
} else {
System.out.println("登录失败");
}
if (rs != null) rs.close();
if (ps != null) ps.close();
if (conn != null) conn.close();
}
可以通过调用 Connection 对象的 preparedStatement(String sql) 方法获取 PreparedStatement 对象,PreparedStatement接口是Statement的子接口,它表示一条预编译过的SQL语句
PreparedStatement 对象所代表的 SQL 语句中的参数用问号(?)来表示,调用PreparedStatement 对象的setXxx() 方法来设置这些参数,setXxx() 方法有两个参数,第一个参数是要设置的 SQL 语句中的参数的索引(从1开始),第二个是设置的 SQL语句中参数的值
Query
public static void main(String[] args) throws Exception {
Class.forName("com.mysql.cj.jdbc.Driver");
String url = "jdbc:mysql://localhost:3306/front_db?useSSL=true&useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai";
String username = "root";
String password = "root";
Connection conn = DriverManager.getConnection(url, username, password);
String sql = "select * from front_db.users";
PreparedStatement ps = conn.prepareStatement(sql);
ResultSet rs = ps.executeQuery();
List<User> userList = new ArrayList<>();
while (rs.next()) {
int id = rs.getInt("id");
String uName = rs.getString("username");
String uPwd = rs.getString("password");
int age = rs.getInt("age");
int status = rs.getInt("status");
User user = new User(id, uName, uPwd, age, status);
userList.add(user);
}
System.out.println(userList);
if (rs != null) rs.close();
if (ps != null) ps.close();
if (conn != null) conn.close();
}
Insert、Update、Delete 只需修改对应的SQL语句,以及填充对应占位符
public static void main(String[] args) throws Exception {
Class.forName("com.mysql.cj.jdbc.Driver");
String url = "jdbc:mysql://localhost:3306/front_db?useSSL=true&useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai";
String username = "root";
String password = "root";
Connection conn = DriverManager.getConnection(url, username, password);
// 预编译SQL
String sql = "update front_db.users set username = ? ,password = ?,age = ?,status = ? where id = ?";
PreparedStatement ps = conn.prepareStatement(sql);
// 填充占位符
ps.setString(1, "小萌");
ps.setString(2, "meng@.");
ps.setInt(3, 18);
ps.setInt(4, 1);
ps.setInt(5, 1001);
int affectRow = ps.executeUpdate();
if (affectRow == 0) System.out.println("修改失败");
if (ps != null) ps.close();
if (conn != null) conn.close();
}
连接成功后,选择数据库
双击数据库,查看相应的表
修改表中数据
打开编写代码控制台
连接失败,检查原因
数据库连接—>执行完毕—>释放
连接、释放十分浪费系统资源
好处
池化技术:准备一些预先的资源,过来就连接预先准备好的
标准接口:DataSource
常见的数据库连接池
C3P0是一个开源组织提供的一个数据库连接池,**速度相对较慢,稳定性还可以。**hibernate官方推荐使用
需要用到的jar包:c3p0-0.9.5.5.jar
编写c3p0-config.xml
文件
<c3p0-config>
<named-config name="helloC3P0">
<property name="driverClass">com.mysql.cj.jdbc.Driverproperty>
<property name="jdbcUrl">jdbc:mysql://localhost:3306/front_db?useSSL=true&useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghaiproperty>
<property name="user">rootproperty>
<property name="password">rootproperty>
<property name="acquireIncrement">5property>
<property name="initialPoolSize">10property>
<property name="minPoolSize">10property>
<property name="maxPoolSize">100property>
<property name="maxStatements">50property>
<property name="maxStatementsPerConnection">2property>
named-config>
c3p0-config>
获取连接
@Test
public void testC3p0Connection() throws SQLException {
ComboPooledDataSource cpds = new ComboPooledDataSource("helloC3P0");
Connection conn = cpds.getConnection();
System.out.println(conn);
}
DBCP是Apache软件基金组织下的开源连接池实现,tomcat 服务器自带dbcp数据库连接池。速度相对c3p0较快,但因自身存在BUG,Hibernate3已不再提供支持
需要用到的jar包
编写dbcp.properties
配置文件
# 连接四要素
driverClassName=com.mysql.cj.jdbc.Driver
url=jdbc:mysql://localhost:3306/front_db?useSSL=true&useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai
username=root
password=root
# 其他设置
initialSize=10
maxActive=20
# ...
获取连接
@Test
public void testDBCPConnection() throws Exception {
Properties pros = new Properties();
InputStream is = ClassLoader.getSystemClassLoader().getResourceAsStream("dbcp.properties");
pros.load(is);
DataSource ds = BasicDataSourceFactory.createDataSource(pros);
Connection conn = ds.getConnection();
System.out.println(conn);
}
Druid是阿里巴巴开源平台上一个数据库连接池实现,它结合了C3P0、DBCP、Proxool等DB池的优点,同时加入了日志监控,可以很好的监控DB池连接和SQL的执行情况,可以说是针对监控而生的DB连接池,且功能强大,性能优秀,是Java语言最好的数据库连接池之一
需要用到的jar包:druid-1.2.8.jar
编写druid.properties
配置文件
# 连接四要素
driverClassName=com.mysql.cj.jdbc.Driver
url=jdbc:mysql://localhost:3306/front_db?useSSL=true&useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai
username=root
password=root
# 其他设置
# 初始化连接数量
initialSize=10
# 最大连接数
maxActive=15
# 最大等待时长
maxWait=3000
获取连接
@Test
public void testDruidConnection() throws Exception {
Properties prop = new Properties();
InputStream is = ClassLoader.getSystemClassLoader().getResourceAsStream("druid.properties");
prop.load(is);
DataSource ds = DruidDataSourceFactory.createDataSource(prop);
Connection conn = ds.getConnection();
System.out.println(conn);
}
结论:无论使用什么数据源,本质还是一样的,DataSource接口不会变,方法就不会变!
为什么进行JDBC封装?
业务代码和数据访问代码耦合
什么是DAO?
非常流行的数据访问模式——DAO模式
DAO模式的组成部分
DAO的优势
为解决业务代码和数据访问代码的紧耦合,给修改和维护代码带来的不便,推荐使用DAO模式封装JDBC
// DAO:Data Access Object(数据访问对象)
public abstract class BaseDao<T> {
private Class<T> clazz;
{
// 获取当前BaseDao的子类继承父类中的泛型
Type genericSuperclass = this.getClass().getGenericSuperclass();
ParameterizedType paramType = (ParameterizedType) genericSuperclass;
Type[] typeArguments = paramType.getActualTypeArguments(); // 获取父类的泛型参数
clazz = (Class<T>) typeArguments[0]; // 泛型的第一个参数
}
/**
* 获取连接
*/
public Connection getConnection() {
DataSource dataSource = null;
try {
// 1.读取配置文件中连接数据库的基本信息
InputStream is = ClassLoader.getSystemClassLoader().getResourceAsStream("druid.properties");
Properties prop = new Properties();
prop.load(is);
// 获取数据库连接池
dataSource = DruidDataSourceFactory.createDataSource(prop);
return dataSource.getConnection();
} catch (Exception e) {
throw new RuntimeException(e);
}
}
/**
* 关闭资源 查询
*/
public void closeResource(Connection conn, PreparedStatement ps, ResultSet rs) {
try {
if (rs != null) rs.close();
if (ps != null) ps.close();
if (conn != null) conn.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
/**
* 关闭资源 增、删、改
*
* @param conn 连接对象
* @param ps 执行SQL语句对象
*/
public void closeResource(Connection conn, PreparedStatement ps) {
try {
if (ps != null) ps.close();
if (conn != null) conn.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
/**
* 通用增、删、改 version 2.0(考虑到事务)
*
* @param sql
* @param args
* @return
*/
public int modifyTbTx(Connection conn, String sql, Object... args) {
PreparedStatement ps = null;
try {
// 1.预编译sql 返回prepareStatement对象
ps = conn.prepareStatement(sql);
// 2.填充占位符
for (int i = 0; i < args.length; i++) {
// 从1开始
ps.setObject(i + 1, args[i]);
}
// 3.执行操作
return ps.executeUpdate();
} catch (Exception e) {
e.printStackTrace();
} finally {
// 4.关闭资源
closeResource(null, ps);
}
return 0;
}
/**
* 通用的查询操作,返回数据表的一条记录 version 2.0(考虑到事务)
*
* @param conn 连接对象
* @param sql sql语句
* @param args 执行sql所需的填充参数
* @return 单条记录
*/
public T getDataOneWithTx(Connection conn, String sql, Object... args) {
PreparedStatement ps = null;
ResultSet rs = null;
try {
// 预编译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();
// 获取列数
int columnCount = rsmd.getColumnCount();
if (rs.next()) {
T t = clazz.newInstance();
for (int i = 0; i < columnCount; i++) {
// 获取每一个列的列值:通过ResultSet
Object columnValue = rs.getObject(i + 1);
// 获取每一个列的列名:通过ResultSetMetaData
// getColumnLabel:获取列的别名 推荐使用
String columnLabel = rsmd.getColumnLabel(i + 1);
// 通过反射,将对象的指定columnLabel属性名的属性赋值为指定值columnValue
Field field = clazz.getDeclaredField(columnLabel);
field.setAccessible(true);
field.set(t, columnValue);
}
return t;
}
} catch (Exception e) {
e.printStackTrace();
} finally {
closeResource(null, ps, rs);
}
return null;
}
/**
* 通用查询方法 返回多条记录的查询操作 version 2.0(考虑到事务)
*
* @param sql sql语句
* @param args 执行sql所需的填充参数
* @return 多条记录
*/
public List<T> getDataListWithTx(Connection conn, String sql, Object... args) {
PreparedStatement ps = null;
ResultSet rs = null;
try {
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();
int columnCount = rsmd.getColumnCount();
// 创建集合对象
ArrayList<T> list = new ArrayList<>();
while (rs.next()) {
T t = clazz.newInstance();
for (int i = 0; i < columnCount; i++) {
Object columnValue = rs.getObject(i + 1);
String columnLabel = rsmd.getColumnLabel(i + 1);
// 通过反射,将对象的指定columnLabel属性名的属性赋值为指定值columnValue
Field field = clazz.getDeclaredField(columnLabel);
field.setAccessible(true);
field.set(t, columnValue);
}
list.add(t);
}
return list;
} catch (Exception e) {
e.printStackTrace();
} finally {
closeResource(null, ps, rs);
}
return null;
}
/**
* 用于查询特殊值的通用方法
*
* @param conn 连接对象
* @param sql sql语句
* @param args 执行sql所需的填充参数
*/
public <E> E getValue(Connection conn, String sql, Object... args) {
PreparedStatement ps = null;
ResultSet rs = null;
try {
ps = conn.prepareStatement(sql);
for (int i = 0; i < args.length; i++) {
ps.setObject(i + 1, args[i]);
}
rs = ps.executeQuery();
if (rs.next()) {
return (E) rs.getObject(1);
}
} catch (SQLException e) {
e.printStackTrace();
} finally {
closeResource(null, ps, rs);
}
return null;
}
}