当我们开发应用时,访问数据库是一种常见的需求。 基本上所有需要持久化的数据,一般都存储在数据库中,例如常用的开源数据库 MySQL。 在今天的文章中,我将盘点一下 Java 应用访问数据的几种方式。
在 Java 开发中,常见的访问数据库的方式有如下几种:
JDBC (Java Database Connectivity) 是一种 API,用来访问数据库和执行 SQL 操作。 JDBC 的核心是一系列的驱动(driver),例如连接 MySQL 的驱动类 com.mysql.cj.jdbc.Driver,连接 H2DB 的驱动类 org.h2.Driver。 JDBC 驱动根据实现方式,有四种类型 [1]:
JDBC 原生 API 中核心的类或接口包括:Connection、Statement、PreparedStatement、CallableStatement 以及 ResultSet。
Spring JDBC 对原生 JDBC 进行了封装,旨在减少进行数据库访问时的样板代码。 根据封装程度的不同,Spring 中提供了四种访问数据库的方式:
Spring Data 项目是对不同数据访问方式(例如 JDBC、JPA、Redis、REST 等)统一封装,旨在提供一个相近、一致的编程模型,从而屏蔽不同数据源、不同访问方式的差异。 Spring Data JDBC 是 Spring Data 中的一个子项目,用来提供一致的、基于 JDBC 的数据访问层。 它最主要的目标之一是,解决使用原生 JDBC 实现数据访问层时需要编写大量样板代码的问题。
Spring Data 中的一个核心抽象接口是 Repository,是 DDD(领域模型驱动)开发模式中的一种模式。 它与 Entity(实体)模型、DAO(数据访问对象)模式略有不同。
[2] 殷浩详解DDD系列 第三讲 - Repository模式
在计算机软件中,对象关系映射(Object Relational Mapping,简称ORM)指的是面向对象语言中对象与关系型数据库中表之间的映射。 这种映射关系是双向的,包含了从面向对象语言中对象到关系型数据库表中字段的转换,以及从查询结果中字段到对象的转换。 ORM 框架的出现,极大地减少了手动转换等样板代码的编写,提高了开发的效率。 常见的 ORM 框架有:
Hibernate 位于应用与关系数据库之间,如下图所示:
Hibernate 提供了两组风格的 API,一组是对 JPA 的实现,一组是原生 API 的实现。 这两组 API 的关系如下:
其中,SessionFactory & Session & Transaction 是原生 API 中的概念; EntityManagerFactory & EntityManager & EntityTransaction 是 JPA 中的概念,它们的定义在
jakarta.persistence-api-2.2.3.jar/javax.persistence 中。
Hibernate 中的 Session 或 JPA 中的 EntityManager,也被称为是处理 persistence data 的上下文(persistence context)。 Persistence data 有不同的状态(与 context 和底层的数据库有关):
Session 或 EntityManager 负责管理数据的状态,在上述几种之间迁移。
如何将 Entity(数据实例)与某个 context 关联?
Flushing 指同步 context 与底层数据库之间状态的过程。 context (or session) 有点像 write-behind 缓存,先更新内存,然后一段时间后再同步到数据库 [3][4]。 Flushing 时,会将 entity 状态的变化映射为 UPDATE\INSERT\DELETE 语句。 Flushing 有几种不同的模式,通过 flushMode 控制:
注:JPA 只定义了 AUTO 和 COMMIT 两种,剩余的是 Hibernate 定义的。
AUTO 模式下,刷新发生在以下几种情况:
prior to committing a Transaction prior to executing a JPQL/HQL query that overlaps with the queued entity actions before executing any native SQL query that has no registered synchronization
使用 Hibernate 的几种方式:
在了解 Hibernate 如何与 Spring 应用进行集成之前,首先花点时间理解 Hibernate 中与其他框架集成相关的内容。 以下的内容来自于对 Hibernate 官方文档的理解,更详细的内容可以参考该文档([5] Hibernate ORM 5.6 Integration Guide)。
在集成 Hibernate 时,需要对它的底层实现有基本的了解。 简单来说,可以把 Hibernate 看作是一个由若干服务(Service,接口和实现)和 一个服务容器(ServiceRegistry)组成。 ServiceRegistry 可以类比 Spring 中 IoC 容器(BeanFactory)理解,不同的是 ServiceRegistry 中存放的是不同 Service 的实现,而 BeanFactory 中存放的是各种 Bean。
ServiceRegistry 与 Service 的关联关系,称为 binding,并通过
org.hibernate.service.spi.ServiceBinding 表示。 Service 与 ServiceRegistry 关联后,称 Service bound to ServiceRegistry。 有两种方式:
ServiceRegistry 通过 createServiceBinding 方法注册关联关系,该方法接受 Service 实例或 ServiceInitiator 实例作为参数。
Hibernate 中有三种类型的 ServiceRegistry 实现,它们一起形成了 Hibernate 中的 ServiceRegistry 层次结构。
native bootstrap
有了上面对 Hibernate 的基本理解后,我们来看下如何对 ServiceRegistry 进行实例化(这个过程在 Hibernate 中称为是 bootstrap)。 根据 ServiceRegistry 类型的不同,分为两类:
对 ServiceRegistry 进行实例化后,就可以通过它来获得 SessionFactory 对象,从而获得 Session 来完成与数据库的交互。 要获得 SessionFactory,需要先在 ServiceRegistry 基础上创建 MetadataSources,然后可以对 MetadataSources 进行相关的自定义配置。
java复制代码MetadataSources sources = new MetadataSources( standardRegistry )
.addAnnotatedClass( MyEntity.class )
.addAnnotatedClassName( "org.hibernate.example.Customer" )
.addResource( "org/hibernate/example/Order.hbm.xml" )
.addResource( "org/hibernate/example/Product.orm.xml" );
或者,通过 MetadataSources 获得 MetadataBuilder,然后再进行设置:
java复制代码MetadataSources sources = new MetadataSources( standardRegistry );
MetadataBuilder metadataBuilder = sources.getMetadataBuilder();
// Use the JPA-compliant implicit naming strategy
metadataBuilder.applyImplicitNamingStrategy( ImplicitNamingStrategyJpaCompliantImpl.INSTANCE );
// specify the schema name to use for tables, etc when none is explicitly specified
metadataBuilder.applyImplicitSchemaName( "my_default_schema" );
// specify a custom Attribute Converter
metadataBuilder.applyAttributeConverter( myAttributeConverter );
Metadata metadata = metadataBuilder.build();
最后,通过 Metadata 能够获得 SessionFactoryBuilder。 此时也可以对 SessionFactory 进行进一步地配置。
java复制代码SessionFactory sessionFactory = metadata.getSessionFactoryBuilder()
.applyBeanManager( getBeanManager() )
.build();
获得 SessionFactory 之后,就可以通过它来创建 Session 对象,然后进行 SQL 操作。
bootstrap with spring
如果在 Spring 应用中使用 Hibernate 作为数据库 ORM 访问框架时,LocalSessionFactoryBean 负责创建 ServiceRegistry,并配置 SessionFactory 等。 LocalSessionFactoryBean 是一个 FactoryBean,即它包含了 getObject 方法,用来返回 SessionFactory 对象。
java复制代码@Override
@Nullable
public SessionFactory getObject() {
return this.sessionFactory;
}
而且,LocalSessionFactoryBean 实现了 InitializingBean 接口,即在 Spring 创建 Bean 的 initializeBean 阶段,会回调它的 afterPropertiesSet 方法。 在 LocalSessionFactoryBean#afterPropertiesSet 中使用 Hibernate 原生 API 完成了对 ServiceRegistry 的初始化。
java复制代码// org.hibernate.cfg.Configuration#buildSessionFactory(org.hibernate.service.ServiceRegistry)
final Metadata metadata = metadataBuilder.build();
final SessionFactoryBuilder sessionFactoryBuilder = metadata.getSessionFactoryBuilder();
// ...
return sessionFactoryBuilder.build();
从严格意义上讲,MyBatis 不是一个 ORM 框架,只能算作是一个半自动的 SQL 映射框架。 它需要手写 SQL语句,以及自定义 ResultSet 与对象之间的映射关系(例如 Mapper.xml)。 不过,从提升开发效率方面,MyBatis 是一款开发利器。
与前面介绍 Hibernate 一样,我先来介绍 MyBatis 中的核心概念(可以参照、对比 Hibernate 一起理解)。
SqlSession & SqlSessionFactory & SqlSessionFactoryBuilder
上述三个类的作用范围和生命周期:
使用 xml 配置文件创建 SqlSessionFactory
java复制代码String resource = "org/mybatis/example/mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
xml复制代码<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"https://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<environments default="development">
<environment id="development">
<transactionManager type="JDBC"/>
<dataSource type="POOLED">
<property name="driver" value="com.mysql.cj.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://localhost:3306/mydb?serverTimezone=UTC"/>
<property name="username" value="samson"/>
<property name="password" value="samson123"/>
</dataSource>
</environment>
</environments>
<mappers>
<mapper resource="mappers/self/samson/example/entity/Account.xml"/>
</mappers>
</configuration>
程序化方式创建 SqlSessionFactory
java复制代码TransactionFactory transactionFactory = new JdbcTransactionFactory();
Environment environment = new Environment("development", transactionFactory, dataSource);
Configuration configuration = new Configuration(environment);
configuration.addMapper(AccountMapper.class); // 特别注意
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(configuration);
注:*Mapper.class 类(接口)中,方法上添加了 SQL Mapping 注解,例如 @Insert("insert into table3 (id, name) values(#{nameId}, #{name})") 但是这种方式表达能力有限,特别复杂的查询是无法通过这种方式实现的。 *Mapper.xml 配置文件并不能完全消除,复杂类型的 SQL 还是应该写在 xml 文件中。 所以,当通过 Configuration#addMapper 时,Mybatis 会尝试加载同名的 *Mapper.xml 文件。 源码如下所示:
java复制代码public <T> void addMapper(Class<T> type) {
mapperRegistry.addMapper(type);
}
public <T> void addMapper(Class<T> type) {
if (type.isInterface()) {
boolean loadCompleted = false;
try {
knownMappers.put(type, new MapperProxyFactory<>(type));
MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type);
// 注意这里,parse 方法中会调用 loadXmlResource 方法
parser.parse();
loadCompleted = true;
} finally {
if (!loadCompleted) {
knownMappers.remove(type);
}
}
}
}
private void loadXmlResource() {
if (!configuration.isResourceLoaded("namespace:" + type.getName())) {
// 注意这里
String xmlResource = type.getName().replace('.', '/') + ".xml";
// #1347
InputStream inputStream = type.getResourceAsStream("/" + xmlResource);
if (inputStream == null) {
// Search XML mapper that is not in the module but in the classpath.
try {
inputStream = Resources.getResourceAsStream(type.getClassLoader(), xmlResource);
} catch (IOException e2) {
// ignore, resource is not required
}
}
if (inputStream != null) {
XMLMapperBuilder xmlParser = new XMLMapperBuilder(inputStream, assistant.getConfiguration(), xmlResource, configuration.getSqlFragments(), type.getName());
xmlParser.parse();
}
}
}
举例来说明,前面通过 Configureation#addMapper 添加了
self.samson.example.orm.mybatis.mapper.AccountMapper 类。 那么,它对应的会尝试加载 /self/samson/example/orm/mybatis.mapper/AccountMapper.xml 文件。 第一个 “/” 表示应用的 classpath。如果是一个 Maven 工程,那么 classpath 就包括 ${project.dir}/target/classes 目录。
在 Spring 应用中集成 MyBatis 也是非常方便的,如下所示:
java复制代码@Bean
public SqlSessionFactory sqlSessionFactory() throws Exception {
SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean();
factoryBean.setDataSource(dataSource());
return factoryBean.getObject();
}
SqlSessionFactoryBean 实现了 FactoryBean、InitializingBean 接口。 当 Spring 完成 SqlSessionFactoryBean 的实例创建后,在初始化阶段会调用 InitializingBean#afterPropertiesSet 方法,来完成 SqlSessionFactory 对象的创建。 并且,当其它 Bean 向 Spring 容器请求 SqlSessionFactory 对象时,SqlSessionFactoryBean 将创建好的 SqlSessionFactory 对象返回,注入到依赖它的对象中。
创建 SqlSessionFactory 对象的过程在 SqlSessionFactoryBean#buildSqlSessionFactory 中实现,简化后的过程如下:
java复制代码Configuration targetConfiguration = new Configuration();
targetConfiguration.setEnvironment(new Environment(this.environment,
this.transactionFactory == null ? new SpringManagedTransactionFactory() : this.transactionFactory,
this.dataSource));
this.sqlSessionFactoryBuilder.build(targetConfiguration);
添加 Mapper 的方式
为了方便开发者向容器中添加 Mapper,Spring 提供了三种更灵活的方式:通过 MapperFactoryBean 、MapperScannerConfigurer 以及 @MapperScan。
MapperFactoryBean 向容器中添加一个特定类型的 FactoryBean。
java复制代码public MapperFactoryBean<AccountMapper> accountMapper1(SqlSessionFactory sessionFactory) throws Exception {
MapperFactoryBean<AccountMapper> factoryBean = new MapperFactoryBean<>(AccountMapper.class);
factoryBean.setSqlSessionFactory(sessionFactory);
return factoryBean;
}
MapperScannerConfigurer 可以通过指定路径的方式,扫描并发现路径下的 Mapper。
java复制代码@Configuration
public class Configurer {
@Bean
public MapperScannerConfigurer mapperScannerConfigurer() {
MapperScannerConfigurer configurer = new MapperScannerConfigurer();
configurer.setBasePackage("self.samson.example.orm.mybatis.mapper");
configurer.setSqlSessionFactoryBeanName("sqlSessionFactory");
return configurer;
}
// 创建 MapperScannerConfigurer 时,datasource 为空
// 原因是负责处理 @Autowired 的 BeanPostProccessor 还没有被创建
@Autowired DataSource datasource;
}
注:需要提醒一下,MapperScannerConfigurer 是一个
BeanDefinitionRegistryPostProcessor 接口的实现。 这就意味着 Spring 创建它的时机非常早,甚至早于常用的 BeanPostProcessor,例如处理 @Autowired 注解的 AutowiredAnnotationBeanPostProcessor。 这也是为什么官网会有如下的提示:
@MapperScan
java复制代码@MapperScan("self.samson.example.orm.mybatis.mapper")
@Configuration
public class Config {
}
MyBatis Mapper 是一个开源工具,旨在提供众多开箱即用的功能,例如一个可供继承的、拥有大量通用方法的基类 Mapper。 它的使用效果如下所示(内容来自工具官网):
java复制代码// 应用程序中,通过继承通用 Mapper 基类,获得大量通用方法
public interface UserMapper extends Mapper<User, Long> {
// 可以按需增加特有方法
// 需要提供实现,可以使用 MyBatis 方式,使用 XML 文件或注解方式
}
// 使用时
User user = new User();
user.setUserName("测试");
// userMapper 中包含了继承来的 insert 方法
userMapper.insert(user);
//保存后自增id回写,不为空
Assert.assertNotNull(user.getId());
//根据id查询
user = userMapper.selectByPrimaryKey(user.getId());
//删除
Assert.assertEquals(1, userMapper.deleteByPrimaryKey(user.getId()));
MyBatis Mapper 1.2.0 版本后,提供了 wrapper 用法,能够使开发者在开发过程中,使用链式调用风格:
java复制代码mapper.wrapper()
.eq(User::getSex, "女")
.or(c -> c.gt(User::getId, 40), c -> c.lt(User::getId, 10))
.or()
.startsWith(User::getUserName, "张").list();
上述代码等价于下述 SQL:
sql复制代码SELECT id,name AS userName,sex FROM user
WHERE
( sex = ? AND ( ( id > ? ) OR ( id < ? ) ) )
OR
( name LIKE ? )
注:MyBatis Mapper 这种方式,优点类似于 Spring Data JPA 中的 Repository,例如 JpaRepository,CurdRepository 等等。
Java Persistence API(简称 JPA)使开发者通过 object/releational mapping 功能来管理关系型数据(这与 ORM 框架的目的是一样的,我认为 JPA 就是对 ORM 的规范化)。 前面提到的 Hibernate,其实是 JPA 的实现之一。 除了它之外,EclipseLink 也是被广泛使用的一个 JPA 实现。
JPA 中定义的核心概念包括:Entity & EntityManager & EntityManagerFactory 这些概念也可以类比之前的 Hibernate、MyBatis 进行理解。 JPA 定义的配置文件默认位置为 /META-INF/persistence.xml。 JPA 支持两种类型的映射方案:
Hibernate 对 JPA 的实现中,启动方式(bootstrap)分为两类:
Spring 提供了三种方式来设置 EntityManagerFactory。
Data Access Object(数据访问对象,简称 DAO)是一种设计模式,它将对数据库的访问等操作封装在 DAO 接口及其实现中,屏蔽了应用层对数据库操作,是一种“解耦”设计。 在 DAO 模式中,一般由三部分组成:
下面,我给出一个简单的实例。
注意,在上面的第2步里,我并没有直接访问数据库,而是模拟了一个内存数据库,应用层通过 DAO 对象存储数据,它并不关心数据最终存在数据库、内存还是文件中。 通过上面的例子,你可以发现 DAO 带来的一个好处就是将应用层与底层的数据存储隔离、解耦,是一种好的设计模式。
如果我们想替换上面的 EventMemDao 实现,将数据持久化到关系数据库中,就可以通过提供一个新的 DAO 实现来完成。
java复制代码public class EventJdbcDaoImpl implements IDao<Event> {
private JdbcTemplate jdbcTemplate;
public EventJdbcDaoImpl(JdbcTemplate jdbcTemplate) {
this.jdbcTemplate = jdbcTemplate;
}
public JdbcTemplate getJdbcTemplate() {
return jdbcTemplate;
}
@Override
public Event get(Long id) {
return getJdbcTemplate().queryForObject("SELECT * FROM EVENT WHERE id = ?", new Object[]{id}, Event.class);
}
@Override
public List<Event> getAll() {
return getJdbcTemplate().queryForList("SELECT * FROM EVENT", Event.class);
}
// 省略其他
}
如果要将访问数据库的方式替换为通过 Hibernate API,在 DAO 模式下也很简单:
本文系作者在时代Java发表,未经许可,不得转载。
如有侵权,请联系nowjava@qq.com删除。