第九章:数据库编程(下)
在上一部分中,我们学习了数据库编程的基础知识,包括数据库基本概念、SQL语言、JDBC基础和JDBC高级特性。在这一部分中,我们将继续深入学习更高级的数据库编程内容,包括数据库连接池和ORM框架等。
1. 数据库连接池
1.1 什么是数据库连接池
数据库连接池是一种用于管理数据库连接的技术,它在应用程序启动时创建一定数量的数据库连接,并将这些连接保存在池中,当应用程序需要访问数据库时,可以从池中获取一个连接,使用完毕后再将连接归还给池。
生活中的例子:
想象一下共享单车系统。如果每次需要骑车,都要去自行车厂商那里购买一辆新车(创建新连接),用完后就丢弃(关闭连接),这显然非常浪费资源。共享单车系统则是预先在各个地点放置一定数量的自行车(连接池),人们需要用车时直接取用(获取连接),用完后归还到停车点(归还连接),供其他人继续使用。
1.2 为什么需要数据库连接池
在没有连接池的情况下,每次数据库操作都需要经历以下步骤:
- 建立与数据库的连接(耗时操作)
- 执行SQL语句
- 关闭连接
这种方式存在以下问题:
- 性能问题:建立数据库连接是一个耗时的操作,涉及到网络通信、认证等过程
- 资源浪费:频繁地创建和销毁连接会消耗大量系统资源
- 连接数限制:数据库服务器能够同时处理的连接数是有限的
数据库连接池通过复用连接解决了这些问题,显著提高了应用程序的性能和可伸缩性。
1.3 常见的数据库连接池
1.3.1 DBCP (Database Connection Pool)
DBCP是Apache Commons项目的一部分,是一个相对简单的数据库连接池实现。
import org.apache.commons.dbcp2.BasicDataSource;
import java.sql.Connection;
import java.sql.SQLException;
public class DBCPDemo {
public static void main(String[] args) {
// 创建数据源
BasicDataSource dataSource = new BasicDataSource();
// 设置数据库连接信息
dataSource.setDriverClassName("com.mysql.cj.jdbc.Driver");
dataSource.setUrl("jdbc:mysql://localhost:3306/mydb");
dataSource.setUsername("root");
dataSource.setPassword("password");
// 设置连接池参数
dataSource.setInitialSize(5); // 初始连接数
dataSource.setMaxTotal(10); // 最大连接数
dataSource.setMaxIdle(5); // 最大空闲连接数
dataSource.setMinIdle(2); // 最小空闲连接数
try {
// 从连接池获取连接
Connection conn = dataSource.getConnection();
// 使用连接执行SQL操作
// ...
// 归还连接到连接池
conn.close();
} catch (SQLException e) {
e.printStackTrace();
} finally {
// 关闭数据源
try {
dataSource.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
}
}
1.3.2 C3P0
C3P0是另一个流行的开源数据库连接池,它提供了自动恢复和语句缓存等高级特性。
import com.mchange.v2.c3p0.ComboPooledDataSource;
import java.sql.Connection;
import java.sql.SQLException;
public class C3P0Demo {
public static void main(String[] args) {
// 创建数据源
ComboPooledDataSource dataSource = new ComboPooledDataSource();
try {
// 设置数据库连接信息
dataSource.setDriverClass("com.mysql.cj.jdbc.Driver");
dataSource.setJdbcUrl("jdbc:mysql://localhost:3306/mydb");
dataSource.setUser("root");
dataSource.setPassword("password");
// 设置连接池参数
dataSource.setInitialPoolSize(5); // 初始连接数
dataSource.setMaxPoolSize(10); // 最大连接数
dataSource.setMinPoolSize(3); // 最小连接数
dataSource.setAcquireIncrement(2); // 当连接池中连接不足时一次性创建的连接数
// 从连接池获取连接
Connection conn = dataSource.getConnection();
// 使用连接执行SQL操作
// ...
// 归还连接到连接池
conn.close();
} catch (Exception e) {
e.printStackTrace();
} finally {
// 关闭数据源
dataSource.close();
}
}
}
1.3.3 HikariCP
HikariCP是目前最快的数据库连接池之一,它被Spring Boot 2.0作为默认的连接池。
import com.zaxxer.hikari.HikariConfig;
import com.zaxxer.hikari.HikariDataSource;
import java.sql.Connection;
import java.sql.SQLException;
public class HikariCPDemo {
public static void main(String[] args) {
// 创建配置对象
HikariConfig config = new HikariConfig();
// 设置数据库连接信息
config.setJdbcUrl("jdbc:mysql://localhost:3306/mydb");
config.setUsername("root");
config.setPassword("password");
// 设置连接池参数
config.setMaximumPoolSize(10); // 最大连接数
config.setMinimumIdle(5); // 最小空闲连接数
config.setIdleTimeout(30000); // 空闲连接超时时间
// 创建数据源
HikariDataSource dataSource = new HikariDataSource(config);
try {
// 从连接池获取连接
Connection conn = dataSource.getConnection();
// 使用连接执行SQL操作
// ...
// 归还连接到连接池
conn.close();
} catch (SQLException e) {
e.printStackTrace();
} finally {
// 关闭数据源
dataSource.close();
}
}
}
1.4 连接池的核心参数
无论使用哪种连接池实现,都需要关注以下核心参数:
- 初始连接数:连接池启动时创建的初始连接数量
- 最大连接数:连接池中允许的最大连接数
- 最小空闲连接数:连接池中保持的最小空闲连接数
- 最大空闲连接数:连接池中允许的最大空闲连接数
- 获取连接超时时间:当没有可用连接时,等待获取连接的最长时间
- 空闲连接超时时间:空闲连接被回收前的最长空闲时间
1.5 在Spring中使用连接池
Spring框架提供了对各种连接池的集成支持,下面是在Spring中配置HikariCP连接池的示例:
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.PlatformTransactionManager;
import com.zaxxer.hikari.HikariConfig;
import com.zaxxer.hikari.HikariDataSource;
import javax.sql.DataSource;
@Configuration
public class DataSourceConfig {
@Bean
public DataSource dataSource() {
HikariConfig config = new HikariConfig();
config.setJdbcUrl("jdbc:mysql://localhost:3306/mydb");
config.setUsername("root");
config.setPassword("password");
config.setMaximumPoolSize(10);
config.setMinimumIdle(5);
return new HikariDataSource(config);
}
@Bean
public PlatformTransactionManager transactionManager(DataSource dataSource) {
return new DataSourceTransactionManager(dataSource);
}
}
1.6 连接池最佳实践
- 合理设置连接池大小:连接池大小应根据应用程序的并发用户数、数据库服务器的性能和可用资源来确定
- 监控连接池状态:定期检查连接池的使用情况,及时调整参数
- 设置合理的超时时间:避免连接长时间占用而不释放
- 使用连接验证:在获取连接前验证连接是否有效,避免使用已失效的连接
- 处理连接泄漏:确保在使用完连接后正确关闭,避免连接泄漏
2. 数据库事务管理
2.1 事务的基本概念
事务是数据库操作的基本单位,它包含一系列的数据库操作,这些操作要么全部成功执行,要么全部不执行。事务具有ACID特性:
- 原子性(Atomicity):事务中的所有操作要么全部完成,要么全部不完成
- 一致性(Consistency):事务执行前后,数据库从一个一致状态转变为另一个一致状态
- 隔离性(Isolation):多个事务并发执行时,一个事务的执行不应影响其他事务的执行
- 持久性(Durability):一旦事务提交,其结果就是永久性的
生活中的例子:
银行转账就是一个典型的事务。假设A要给B转账1000元,这个过程包括两个操作:从A的账户扣除1000元,向B的账户添加1000元。这两个操作必须同时成功或同时失败,否则就会出现钱丢失或凭空多出的情况。
2.2 JDBC中的事务管理
在JDBC中,可以通过以下方式管理事务:
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.SQLException;
public class TransactionDemo {
public static void main(String[] args) {
Connection conn = null;
try {
// 获取数据库连接
conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/mydb", "root", "password");
// 关闭自动提交,开启事务
conn.setAutoCommit(false);
// 执行第一个操作:从A账户扣款
String sql1 = "UPDATE accounts SET balance = balance - ? WHERE account_id = ?";
PreparedStatement pstmt1 = conn.prepareStatement(sql1);
pstmt1.setDouble(1, 1000);
pstmt1.setString(2, "A");
pstmt1.executeUpdate();
// 执行第二个操作:向B账户存款
String sql2 = "UPDATE accounts SET balance = balance + ? WHERE account_id = ?";
PreparedStatement pstmt2 = conn.prepareStatement(sql2);
pstmt2.setDouble(1, 1000);
pstmt2.setString(2, "B");
pstmt2.executeUpdate();
// 提交事务
conn.commit();
System.out.println("转账成功!");
} catch (SQLException e) {
// 发生异常,回滚事务
try {
if (conn != null) {
conn.rollback();
System.out.println("转账失败,事务已回滚!");
}
} catch (SQLException ex) {
ex.printStackTrace();
}
e.printStackTrace();
} finally {
// 恢复自动提交
try {
if (conn != null) {
conn.setAutoCommit(true);
conn.close();
}
} catch (SQLException e) {
e.printStackTrace();
}
}
}
}
2.3 Spring中的事务管理
Spring提供了声明式事务管理,使得事务管理变得更加简单:
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
public class AccountService {
@Autowired
private JdbcTemplate jdbcTemplate;
@Transactional
public void transfer(String fromAccount, String toAccount, double amount) {
// 从fromAccount扣款
jdbcTemplate.update("UPDATE accounts SET balance = balance - ? WHERE account_id = ?",
amount, fromAccount);
// 模拟异常
// if (true) throw new RuntimeException("模拟转账异常");
// 向toAccount存款
jdbcTemplate.update("UPDATE accounts SET balance = balance + ? WHERE account_id = ?",
amount, toAccount);
}
}
在上面的例子中,@Transactional
注解表示该方法在事务中执行。如果方法执行过程中发生异常,Spring会自动回滚事务。
2.4 事务隔离级别
事务隔离级别定义了一个事务可能受其他并发事务影响的程度。SQL标准定义了四个隔离级别:
- READ UNCOMMITTED(读未提交):一个事务可以读取另一个未提交事务的数据
- READ COMMITTED(读已提交):一个事务只能读取另一个已提交事务的数据
- REPEATABLE READ(可重复读):确保在同一个事务中多次读取同一数据时,其值都和事务开始时读到的值一样
- SERIALIZABLE(串行化):最高的隔离级别,完全禁止并发事务
在Spring中,可以通过@Transactional
注解的isolation
属性设置事务隔离级别:
@Transactional(isolation = Isolation.READ_COMMITTED)
public void transfer(String fromAccount, String toAccount, double amount) {
// 转账逻辑
}
2.5 事务传播行为
事务传播行为定义了事务方法和事务方法发生嵌套调用时事务如何传播。Spring定义了7种事务传播行为:
- REQUIRED:如果当前存在事务,则加入该事务;如果当前没有事务,则创建一个新的事务
- SUPPORTS:如果当前存在事务,则加入该事务;如果当前没有事务,则以非事务方式执行
- MANDATORY:如果当前存在事务,则加入该事务;如果当前没有事务,则抛出异常
- REQUIRES_NEW:创建一个新的事务,如果当前存在事务,则挂起当前事务
- NOT_SUPPORTED:以非事务方式执行,如果当前存在事务,则挂起当前事务
- NEVER:以非事务方式执行,如果当前存在事务,则抛出异常
- NESTED:如果当前存在事务,则创建一个事务作为当前事务的嵌套事务来执行;如果当前没有事务,则等价于REQUIRED
在Spring中,可以通过@Transactional
注解的propagation
属性设置事务传播行为:
@Transactional(propagation = Propagation.REQUIRED)
public void transfer(String fromAccount, String toAccount, double amount) {
// 转账逻辑
}
3. ORM框架基础
3.1 什么是ORM
ORM(Object-Relational Mapping,对象关系映射)是一种编程技术,用于实现面向对象编程语言里的对象与关系型数据库中的数据之间的转换。ORM可以把对象模型表示的对象映射到基于SQL的关系模型数据库结构中去。
生活中的例子:
想象一下ORM就像是一位翻译官。假设你(Java程序)只会说中文,而数据库只懂英文。当你想与数据库交流时,你需要一位翻译官(ORM框架)来帮助你。你用中文(Java对象)表达你的意思,翻译官将其翻译成英文(SQL语句)传达给数据库;同样,当数据库用英文(查询结果)回应时,翻译官又会将其翻译成中文(Java对象)让你理解。这样,你就不需要学习英文(SQL)也能与数据库顺畅交流了。
3.2 为什么需要ORM
在没有ORM的情况下,开发者需要编写大量的SQL语句和JDBC代码来实现对象与数据库之间的交互,这存在以下问题:
- 编码繁琐:需要手动编写SQL语句和处理结果集
- 维护困难:SQL语句分散在代码中,难以统一维护
- 数据库依赖:代码与特定数据库紧密耦合,难以切换数据库
- 性能优化复杂:手动优化SQL语句和连接管理较为复杂
ORM框架通过以下方式解决了这些问题:
- 简化开发:开发者只需操作对象,无需编写SQL
- 提高可维护性:数据访问逻辑集中管理
- 数据库无关性:可以较容易地切换不同的数据库
- 自动优化:框架内置了许多性能优化策略
3.3 ORM的核心概念
3.3.1 实体类(Entity)
实体类是与数据库表对应的Java类,类中的属性对应表中的列。
@Entity
@Table(name = "users")
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "username")
private String username;
@Column(name = "email")
private String email;
// 构造函数、getter和setter方法
}
3.3.2 映射(Mapping)
映射定义了Java对象与数据库表之间的对应关系,包括:
- 类-表映射:Java类映射到数据库表
- 属性-列映射:类的属性映射到表的列
- 关联关系映射:对象之间的关联关系映射到表之间的外键关系
3.3.3 会话(Session)
会话是应用程序与数据库之间的交互桥梁,负责执行数据库操作。
3.3.4 事务(Transaction)
ORM框架通常提供了事务管理功能,确保数据库操作的原子性、一致性、隔离性和持久性。
3.4 常见的ORM框架
Java生态系统中常见的ORM框架包括:
- Hibernate:最流行的ORM框架之一,功能强大,支持丰富的映射关系
- MyBatis:轻量级的ORM框架,支持自定义SQL,更加灵活
- Spring Data JPA:基于JPA规范的抽象层,简化数据访问层的开发
4. Hibernate框架
4.1 Hibernate简介
Hibernate是一个开源的对象关系映射框架,它对JDBC进行了非常轻量级的对象封装,使得Java程序员可以使用面向对象的编程思想来操作数据库。
4.2 Hibernate的核心组件
4.2.1 Configuration
Configuration类负责加载Hibernate配置文件和映射文件,创建SessionFactory。
Configuration configuration = new Configuration().configure();
SessionFactory sessionFactory = configuration.buildSessionFactory();
4.2.2 SessionFactory
SessionFactory是Session的工厂,负责创建Session对象。它是线程安全的,通常在应用程序中只需要一个实例。
Session session = sessionFactory.openSession();
4.2.3 Session
Session是应用程序与数据库之间交互的主要接口,负责执行CRUD(创建、读取、更新、删除)操作。它是非线程安全的,每个线程应该有自己的Session实例。
Session session = sessionFactory.openSession();
try {
// 开启事务
session.beginTransaction();
// 执行操作
User user = new User();
user.setUsername("张三");
user.setEmail("zhangsan@example.com");
session.save(user);
// 提交事务
session.getTransaction().commit();
} catch (Exception e) {
// 回滚事务
session.getTransaction().rollback();
e.printStackTrace();
} finally {
// 关闭Session
session.close();
}
4.2.4 Transaction
Transaction接口封装了事务操作,确保数据库操作的原子性。
Transaction tx = session.beginTransaction();
try {
// 执行操作
tx.commit();
} catch (Exception e) {
tx.rollback();
throw e;
}
4.3 Hibernate映射
4.3.1 XML映射
传统的Hibernate使用XML文件定义对象与表之间的映射关系。
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE hibernate-mapping PUBLIC "-//Hibernate/Hibernate Mapping DTD 3.0//EN"
"http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd">
<hibernate-mapping>
<class name="com.example.User" table="users">
<id name="id" column="id">
<generator class="native"/>
</id>
<property name="username" column="username" type="string"/>
<property name="email" column="email" type="string"/>
</class>
</hibernate-mapping>
4.3.2 注解映射
现代Hibernate应用通常使用注解来定义映射关系,更加简洁和直观。
@Entity
@Table(name = "users")
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "username")
private String username;
@Column(name = "email")
private String email;
// 构造函数、getter和setter方法
}
4.4 Hibernate关联关系映射
4.4.1 一对一关系(One-to-One)
@Entity
public class Person {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
@OneToOne(cascade = CascadeType.ALL)
@JoinColumn(name = "address_id")
private Address address;
// 构造函数、getter和setter方法
}
@Entity
public class Address {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String street;
private String city;
@OneToOne(mappedBy = "address")
private Person person;
// 构造函数、getter和setter方法
}
4.4.2 一对多关系(One-to-Many)
@Entity
public class Department {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
@OneToMany(mappedBy = "department", cascade = CascadeType.ALL)
private List<Employee> employees = new ArrayList<>();
// 构造函数、getter和setter方法
}
@Entity
public class Employee {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
@ManyToOne
@JoinColumn(name = "department_id")
private Department department;
// 构造函数、getter和setter方法
}
4.4.3 多对多关系(Many-to-Many)
@Entity
public class Student {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
@ManyToMany(cascade = {CascadeType.PERSIST, CascadeType.MERGE})
@JoinTable(
name = "student_course",
joinColumns = @JoinColumn(name = "student_id"),
inverseJoinColumns = @JoinColumn(name = "course_id")
)
private Set<Course> courses = new HashSet<>();
// 构造函数、getter和setter方法
}
@Entity
public class Course {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
@ManyToMany(mappedBy = "courses")
private Set<Student> students = new HashSet<>();
// 构造函数、getter和setter方法
}
4.5 Hibernate查询方式
4.5.1 HQL(Hibernate Query Language)
HQL是Hibernate特有的查询语言,类似于SQL,但操作的是对象而非表。
String hql = "FROM User WHERE username = :username";
Query<User> query = session.createQuery(hql, User.class);
query.setParameter("username", "张三");
List<User> users = query.list();
4.5.2 Criteria API
Criteria API提供了一种面向对象的查询方式,避免了字符串拼接的问题。
CriteriaBuilder builder = session.getCriteriaBuilder();
CriteriaQuery<User> criteriaQuery = builder.createQuery(User.class);
Root<User> root = criteriaQuery.from(User.class);
criteriaQuery.select(root).where(builder.equal(root.get("username"), "张三"));
List<User> users = session.createQuery(criteriaQuery).getResultList();
4.5.3 原生SQL
Hibernate也支持使用原生SQL进行查询。
String sql = "SELECT * FROM users WHERE username = ?";
NativeQuery<User> query = session.createNativeQuery(sql, User.class);
query.setParameter(1, "张三");
List<User> users = query.list();
4.6 Hibernate缓存机制
Hibernate提供了多级缓存机制,用于提高应用程序的性能。
4.6.1 一级缓存(Session缓存)
一级缓存是Session级别的缓存,默认开启,不可关闭。当我们通过Session加载一个对象后,该对象会被缓存在Session中,后续对该对象的查询将直接从缓存中获取,而不会再次访问数据库。
// 第一次查询,会访问数据库
User user1 = session.get(User.class, 1L);
// 第二次查询,直接从一级缓存获取,不会访问数据库
User user2 = session.get(User.class, 1L);
4.6.2 二级缓存(SessionFactory缓存)
二级缓存是SessionFactory级别的缓存,可以在多个Session之间共享数据。二级缓存需要显式配置才能启用。
<!-- 在hibernate.cfg.xml中配置二级缓存 -->
<property name="hibernate.cache.use_second_level_cache">true</property>
<property name="hibernate.cache.region.factory_class">org.hibernate.cache.ehcache.EhCacheRegionFactory</property>
// 在实体类上启用二级缓存
@Entity
@Cacheable
@Cache(usage = CacheConcurrencyStrategy.READ_WRITE)
public class User {
// ...
}
4.7 Hibernate最佳实践
- 使用懒加载:默认情况下,Hibernate使用懒加载策略加载关联对象,只有在实际访问关联对象时才会从数据库加载,这可以提高性能
- 合理使用缓存:根据应用场景合理配置和使用缓存,可以显著提高性能
- 避免N+1查询问题:使用join fetch或批量加载策略避免N+1查询问题
- 使用命名查询:将常用查询定义为命名查询,提高代码可维护性
- 使用批量操作:对于大批量数据操作,使用批量插入、更新或删除
总结
在本章的这一部分中,我们学习了数据库连接池和数据库事务管理的相关知识。数据库连接池通过复用数据库连接,显著提高了应用程序的性能和可伸缩性。数据库事务管理则确保了数据库操作的原子性、一致性、隔离性和持久性。
我们学习了ORM框架的基础知识和Hibernate框架的使用。ORM框架通过对象关系映射技术,简化了数据库编程,提高了开发效率。Hibernate作为一种流行的ORM框架,提供了强大的对象映射和查询功能。
在下一部分中,我们将继续学习MyBatis和Spring Data JPA等其他ORM框架。