当我们开发应用时,访问数据库是一种常见的需求。 基本上所有需要持久化的数据,一般都存储在数据库中,例如常用的开源数据库 MySQL。 在今天的文章中,我将盘点一下 Java 应用访问数据的几种方式。
在 Java 开发中,常见的访问数据库的方式有如下几种:
- 通过 JDBC 访问数据库。
- 通过 ORM 框架访问数据库,例如 Hibernate、Mybatis 等(严格意义上 MyBatis 并不能算是一个 ORM 框架,它底层通过 JDBC 完成数据库操作)。
- 通过 JPA 访问数据库。
01-通过 JDBC 进行数据访问
JDBC (Java Database Connectivity) 是一种 API,用来访问数据库和执行 SQL 操作。 JDBC 的核心是一系列的驱动(driver),例如连接 MySQL 的驱动类 com.mysql.cj.jdbc.Driver,连接 H2DB 的驱动类 org.h2.Driver。 JDBC 驱动根据实现方式,有四种类型 [1]:
- 第一种,例如 JDBC-ODBC 这种,将 JDBC 映射到另外一种数据库访问 API;
- 第二种,基于目标数据的客户端库,开发的 JDBC 驱动,也称为是原生驱动;
- 第三种,利用中间件,将 JDBC 转换成目标数据库调用,也称为是网络协议驱动;
- 第四种,将 JDBC 直接转换成目标数据库调用,也被称为是数据库协议驱动或(thin 驱动)。这也是最常见的类型。
JDBC 原生 API 中核心的类或接口包括:Connection、Statement、PreparedStatement、CallableStatement 以及 ResultSet。
01.1-Spring JDBC
Spring JDBC 对原生 JDBC 进行了封装,旨在减少进行数据库访问时的样板代码。 根据封装程度的不同,Spring 中提供了四种访问数据库的方式:
- org.springframework.jdbc.core.JdbcTemplate,是最普遍、常用的方法。
- java复制代码
- // query String lastName = jdbcTemplate.queryForObject( "select last_name from t_actor where id = ?", String.class, 1212L); // update jdbcTemplate.update( "update t_actor set last_name = ? where id = ?", "Banjo", 5276L); // execute jdbcTemplate.execute("create table mytable (id integer, name varchar(100))");
- org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate,通过代理方式,封装了 JdbcTemplate,并提供了额外的访问接口。 主要区别在于 JdbcTemplate 中的 '?' 表示参数,NamedParameterJdbcTemplate 中使用变量名表示参数 ':xxx'
- java复制代码
- String sql = "select count(*) from T_ACTOR where first_name = :first_name"; SqlParameterSource namedParameters = new MapSqlParameterSource("first_name", firstName); namedParameterJdbcTemplate.queryForObject(sql, namedParameters, Integer.class);
- org.springframework.jdbc.core.simple.SimpleJdbcInsert 和 org.springframework.jdbc.core.simple.SimpleJdbcCall 是针对插入、存储过程调用进行的优化、简化。 实际的底层操作,仍然要靠 JdbcTemplate 来完成。 它们通过 JDBC 驱动提供的元数据信息 java.sql.DatabaseMetaData 来自动检测表中的列信息、存储过程中的入参和出参信息。
- 通过继承 org.springframework.jdbc.object.MappingSqlQuery、org.springframework.jdbc.object.SqlUpdate 和 org.springframework.jdbc.object.StoredProcedure 等方式,以对象方式操作、访问关系型数据库(类似于后面的 Hibernate、MyBatis)。
01.2-Spring Data JDBC
Spring Data 项目是对不同数据访问方式(例如 JDBC、JPA、Redis、REST 等)统一封装,旨在提供一个相近、一致的编程模型,从而屏蔽不同数据源、不同访问方式的差异。 Spring Data JDBC 是 Spring Data 中的一个子项目,用来提供一致的、基于 JDBC 的数据访问层。 它最主要的目标之一是,解决使用原生 JDBC 实现数据访问层时需要编写大量样板代码的问题。
Spring Data 中的一个核心抽象接口是 Repository,是 DDD(领域模型驱动)开发模式中的一种模式。 它与 Entity(实体)模型、DAO(数据访问对象)模式略有不同。
[2] 殷浩详解DDD系列 第三讲 - Repository模式
02-通过 ORM 框架进行数据访问
在计算机软件中,对象关系映射(Object Relational Mapping,简称ORM)指的是面向对象语言中对象与关系型数据库中表之间的映射。 这种映射关系是双向的,包含了从面向对象语言中对象到关系型数据库表中字段的转换,以及从查询结果中字段到对象的转换。 ORM 框架的出现,极大地减少了手动转换等样板代码的编写,提高了开发的效率。 常见的 ORM 框架有:
- Hibernate
- MyBatis
- EclipseLink
02.1-Hibernate 中的核心概念
Hibernate 位于应用与关系数据库之间,如下图所示:
Hibernate 提供了两组风格的 API,一组是对 JPA 的实现,一组是原生 API 的实现。 这两组 API 的关系如下:
其中,SessionFactory & Session & Transaction 是原生 API 中的概念; EntityManagerFactory & EntityManager & EntityTransaction 是 JPA 中的概念,它们的定义在
jakarta.persistence-api-2.2.3.jar/javax.persistence 中。
- SessionFactory,顾名思义,用来创建 Session 的工厂类。 它是一个线程间安全对象,实例化的代价比较高,通常一个应用中只有一个实例。
- Session 是一个非线程安全的类。一般来说,每个线程需要持有各自的 Session 实例。 Session 对 JDBC 中的连接进行了封装,并且可以认为是 Transaction 的工厂类。
- Transaction 是一个非线程安全的类,它用来声明事务的边界。
Hibernate 中的 Session 或 JPA 中的 EntityManager,也被称为是处理 persistence data 的上下文(persistence context)。 Persistence data 有不同的状态(与 context 和底层的数据库有关):
- transient,指数据对象已创建,但尚未与 context 关联。因此,它在数据库中没有持久化表示,通常也没有被分配 identifier 值。
- managed or persistent,已与 context 关联,并且具有 identifier。但是,是否在底层数据库中有持久化表示,不确定。
- detached,有 identifier,但是已不再与 context 关联。主要由于 context 关闭,或 data 从 context 中被移除。
- removed,有 identifier,且与 context 关联,只不过计划从数据库中移除。
Session 或 EntityManager 负责管理数据的状态,在上述几种之间迁移。
如何将 Entity(数据实例)与某个 context 关联?
- JPA 方式,entityManager.persist(entity)
- 原生方式,session.save(entity)
Flushing 指同步 context 与底层数据库之间状态的过程。 context (or session) 有点像 write-behind 缓存,先更新内存,然后一段时间后再同步到数据库 [3][4]。 Flushing 时,会将 entity 状态的变化映射为 UPDATE\INSERT\DELETE 语句。 Flushing 有几种不同的模式,通过 flushMode 控制:
- ALWAYS,在每次查询前都需要刷新;
- AUTO,默认,必要时才刷新;
- COMMIT,尝试延迟刷新到本次事务结束时。仍有可能会提前刷新。
- MANUAL,应用调用 session.flush() 时刷新。
注: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
02.2-使用 Hibernate 进行数据访问
使用 Hibernate 的几种方式:
- Hibernate 原生 API 搭配 hibernate.cfg.xml(用来配置 SessionFactory)、xx.hbm.xml(配置 Object Mapping,一般一个 Entity 对应一个 hbm.xml 文件,hibernate.cfg.xml 中通过 <mapping resource="xx" /> 指定)。
- java复制代码
- final StandardServiceRegistry registry = new StandardServiceRegistryBuilder() .configure() // configures settings from hibernate.cfg.xml .build(); try { sessionFactory = new MetadataSources( registry ).buildMetadata().buildSessionFactory(); } catch (Exception e) { // The registry would be destroyed by the SessionFactory, but we had trouble building the SessionFactory // so destroy it manually. StandardServiceRegistryBuilder.destroy( registry ); } Session session = sessionFactory.openSession(); session.beginTransaction(); session.save( new Event( "Our very first event!", new Date() ) ); session.save( new Event( "A follow up event", new Date() ) ); session.getTransaction().commit(); session.close();
- Hibernate 原生 API 搭配 hibernate.cfg.xml、@Entity(hibernate.cfg.xml 中通过 <mapping class="xx" /> 指定) 等注解。
- 使用 JPA 中的 persistence.xml 搭配 @Entity 等注解。
- xml复制代码
- <persistence xmlns="http://java.sun.com/xml/ns/persistence" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/persistence http://java.sun.com/xml/ns/persistence/persistence_2_0.xsd" version="2.0"> <persistence-unit name="org.hibernate.tutorial.jpa"> ... </persistence-unit> </persistence> java复制代码sessionFactory = Persistence.createEntityManagerFactory( "org.hibernate.tutorial.jpa" ); EntityManager entityManager = sessionFactory.createEntityManager(); entityManager.getTransaction().begin(); entityManager.persist( new Event( "Our very first event!", new Date() ) ); entityManager.persist( new Event( "A follow up event", new Date() ) ); entityManager.getTransaction().commit(); entityManager.close();
02.3-Hibernate 与 Spring 集成
在了解 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。 有两种方式:
- 直接创建 Service 对象,并关联到 ServiceRegistry
- 创建 ServiceInitiator 对象,并将其交给 ServiceRegistry,在需要时,会通过 ServiceInitiator 创建 Service(某种形式的 lazy instantiation)
ServiceRegistry 通过 createServiceBinding 方法注册关联关系,该方法接受 Service 实例或 ServiceInitiator 实例作为参数。
Hibernate 中有三种类型的 ServiceRegistry 实现,它们一起形成了 Hibernate 中的 ServiceRegistry 层次结构。
- 第一种,BootstrapServiceRegistry,它包含(且仅包含)三个 binding。
- ClassLoaderService,该服务定义了 Hibernate 与 ServiceLoader 交互的能力。 Hibernate 可能运行在不同的环境中,例如应用服务器、OSGi 容器、或者其他的模块化类加载系统,而导致类加载过程存在差异性。 ClassLoaderService 提供了一个抽象的接口,屏蔽了不同环境的实现差异而导致的复杂性。 它的主要功能包括:根据名称定位类、定位资源文件、集成 JDK 原生的 ServiceLoader 等。
- IntegratorService,负责管理所有的 Integrator。 Integrator 有三种来源:第一种是 IntegratorServiceImpl 中注册的,例如 BeanValidationIntegrator; 第二种是通过 BootstrapServiceRegistryBuilder#with 注册的; 第三种是通过 ClassLoaderService 在 /META-INF/services/org.hibernate.integrator.spi.Integrator 中,通过 SPI 发现机制发现的。
- StrategySelector,是 short naming(简称)服务。提供简称到全量名的转换,使配置时可以使用简称,减少心智负担。
- 在使用过程中,BootstrapServiceRegistry 没有父 ServiceRegistry,而且它包含的 binding 不允许修改(增加、删除)。
- StandardServiceRegistry 是 Hibernate 中主要的 ServiceRegistry。在使用过程中,它通常作为 BootstrapServiceRegistry 的“子容器”。 它持有了 Hibernate 使用大多数 Service。常见的 Service 列表可以在 org.hibernate.service.StandardServiceInitiators 中查看。 与 BootstrapServiceRegistry 不同的是,它维护的 Service 是可以修改的(增加或替换)。
- SessionFactoryServiceRegistry 由 StandardServiceRegistry 中的 org.hibernate.service.spi.SessionFactoryServiceRegistryFactory 服务负责创建。
native bootstrap
有了上面对 Hibernate 的基本理解后,我们来看下如何对 ServiceRegistry 进行实例化(这个过程在 Hibernate 中称为是 bootstrap)。 根据 ServiceRegistry 类型的不同,分为两类:
- 对 BootstrapServiceRegistry 的实例化,通过 BootstrapServiceRegistryBuilder 完成。 根据前面的介绍,BootstrapServiceRegistry 包含三个 Service,它的 Builder 可以对这些 Service 进行定制化。
- java复制代码
- BootstrapServiceRegistryBuilder bootstrapRegistryBuilder = new BootstrapServiceRegistryBuilder(); // add a custom ClassLoader bootstrapRegistryBuilder.applyClassLoader( customClassLoader ); // manually add an Integrator bootstrapRegistryBuilder.applyIntegrator( customIntegrator ); BootstrapServiceRegistry bootstrapRegistry = bootstrapRegistryBuilder.build();
- 对 StandardServiceRegistry 的实例化,通过 StandardServiceRegistryBuilder 完成。
对 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();
02.4-使用 MyBatis 进行数据访问
从严格意义上讲,MyBatis 不是一个 ORM 框架,只能算作是一个半自动的 SQL 映射框架。 它需要手写 SQL语句,以及自定义 ResultSet 与对象之间的映射关系(例如 Mapper.xml)。 不过,从提升开发效率方面,MyBatis 是一款开发利器。
与前面介绍 Hibernate 一样,我先来介绍 MyBatis 中的核心概念(可以参照、对比 Hibernate 一起理解)。
SqlSession & SqlSessionFactory & SqlSessionFactoryBuilder
上述三个类的作用范围和生命周期:
- SqlSessionFactoryBuilder,主要是用来创建 SqlSessionFactory,所以一旦它的目的完成了,这个类的对象就可以销毁了,没必要长期维护这个类的对象。 所以,这个类的最佳作用范围就是在某个方法内,例如作为方法的局部变量。
- SqlSessionFactory,主要用来创建 SqlSession,一旦被创建,它将在应用的整个声明周期内有效。它的最佳作用范围是整个应用,最佳模式是静态单例模式。 SqlSessionFactory 应该是线程间安全的。
- SqlSession,每个线程应该有它自己的对象。SqlSession 对象不应在多个线程间共享,而且它不是线程安全的。它的最佳作用范围是一次请求内或方法内。
- Mapper 实例,通过 SqlSession 获得,所以它的最佳作用范围应当与 SqlSession 保持一致。而且,最佳的作用范围是方法作用域内。
使用 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 目录。
02.5-MyBatis 与 Spring 集成
在 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 { }
02.6-Mybatis Mapper
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 等等。
03-通过 JPA 进行数据访问
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 支持两种类型的映射方案:
- 基于注解,实体注解为 @Entity
- 基于 xml 配置文件 针对 xml 配置文件,它可以是位于任何 jar 包 中的 /META-INF 目录下的 *.xml 文件;也可以是 /META-INF/persistence.xml 同目录下的 *.xml 文件。
03.1-Hibernate JPA
Hibernate 对 JPA 的实现中,启动方式(bootstrap)分为两类:
- container-based bootstrap,容器会根据 /META-INF/persistence.xml 中的内容,为每个 persistent-unit 创建一个 EntityManagerFactory。 并且会通过 @PersistenceUnit 注入到需要它的地方。
- java复制代码
- @PersistenceUnit(unitName = "xxxName") private EntityManagerFactory emf;
- 通过 @PersistenceContext 可以向应用中注入一个默认的 EntityManager。
- java复制代码
- @PersistenceContext private EntityManager em;
- application-based bootstrap,通过 Persistence.createEntityManagerFactory( "xxxName" ) 方式来创建 /META-INF/persistence.xml 中定义的 persistent-unit。
03.2-Spring JPA
Spring 提供了三种方式来设置 EntityManagerFactory。
- LocalEntityManagerFactoryBean,用在简单的部署环境中,例如独立应用进程、继承测试等。
- LocalContainerEntityManagerFactoryBean,提供全部的 JPA 功能。
- 通过 JNDI 获得,一般用在 J2EE 环境中。
04-基于 JDBC、Hibernate、JPA 方式实现 DAO 模式
Data Access Object(数据访问对象,简称 DAO)是一种设计模式,它将对数据库的访问等操作封装在 DAO 接口及其实现中,屏蔽了应用层对数据库操作,是一种“解耦”设计。 在 DAO 模式中,一般由三部分组成:
- 实体类,一般来说是 POJO 对象;
- DAO 接口及其实现;
- 应用层服务等其他需要访问数据库的部分。
下面,我给出一个简单的实例。
- 首先,需要定义一个实体类。
- java复制代码
- public class Event { private Long id; private Date time; private String title; /** 省略属性的 getter/setter 及无参构造器 */ }
- 其次,定义一个 DAO 接口,并提供一个简单实现。
- java复制代码
- public interface IDao<T> { T get(Long id); List<T> getAll(); void update(T t); void save(T t); void delete(T t); } java复制代码public class EventMemDao implements IDao<Event> { private List<Event> events = new ArrayList<>(32); @Override public Event get(Long id) { return events.stream().filter(e -> e.getId().equals(id)).findFirst().orElse(null); } @Override public List<Event> getAll() { return events; } /** 省略 IDao 中其他方法的实现 */ }
- 最后,在应用层使用 DAO 实现,来完成数据库访问。
- java复制代码
- private static void memDao(Event e1, Event e2) { IDao<Event> memDao = new EventMemDao(); memDao.save(e1); memDao.save(e2); final List<Event> all = memDao.getAll(); all.forEach(System.out::println); }
注意,在上面的第2步里,我并没有直接访问数据库,而是模拟了一个内存数据库,应用层通过 DAO 对象存储数据,它并不关心数据最终存在数据库、内存还是文件中。 通过上面的例子,你可以发现 DAO 带来的一个好处就是将应用层与底层的数据存储隔离、解耦,是一种好的设计模式。
04.1-通过 Spring JDBCTemplate 访问数据库
如果我们想替换上面的 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); } // 省略其他 }
04.2-通过 Hibernate 访问数据库
如果要将访问数据库的方式替换为通过 Hibernate API,在 DAO 模式下也很简单:
java复制代码public class EventHibernateDaoImpl implements IDao<Event> { private SessionFactory sessionFactory; public EventHibernateDaoImpl(SessionFactory sessionFactory) { this.sessionFactory = sessionFactory; } public SessionFactory getSessionFactory() { return sessionFactory; } @Override public Event get(Long id) { Event event = null; try (Session session = getSessionFactory().openSession()){ session.beginTransaction(); event = session.get(Event.class, id); session.getTransaction().commit(); } catch (HibernateException ignored) { } return event; } @Override public List<Event> getAll() { List<Event> events = new ArrayList<>(); try (Session session = getSessionFactory().openSession()){ session.beginTransaction(); Query<Event> query = session.createQuery("select * from event", Event.class); events.addAll(query.list()); session.getTransaction().commit(); } catch (HibernateException ignored) { } return events; } // 省略其他 }
04.3-通过 Spring Data JPA 访问数据库
如果使用 Spring Data JPA,则首先需要先定义一个 Repository 实现:
java复制代码public interface EventRepository extends JpaRepository<Event, Long> { }
然后,在我们的 DAO 实现中,将操作数据库的方式换成基于 JPA 的:
java复制代码public class EventJpaDaoImpl implements IDao<Event> { private EventRepository eventRepository; public EventJpaDaoImpl(EventRepository eventRepository) { this.eventRepository = eventRepository; } public EventRepository getEventRepository() { return eventRepository; } @Override public Event get(Long id) { return getEventRepository().getReferenceById(id); } @Override public List<Event> getAll() { return getEventRepository().findAll(); } // 省略其他 }
通过前面的例子,可以看到,我们只需要修改具体的 Dao 实现,应用层基本不需要改动,就替换掉了底层持久化实现。 这中 DAO 模式的设计,让数据持久化层与应用层解耦,两者可以独立升级、改造。
04.4-使用泛型优化 DAO 模式
前两节介绍的 DAO 模式虽然在设计上实现了业务层与持久化层的解耦,但由于应用中的实体类与 DAO 实现类的关系往往是一对一对应的,意味着我们在开发应用时需要实现、维护大量的 DAO 实现类。 而且这些 DAO 实现类大多代码都比较相似,重复代码较多。 随着业务发展和项目规模增长,这一层的代码将越来越多。 本节将介绍一种使用 Java 泛型来减少 DAO 实现类的方法。 我们继续复用前两节中使用的抽象 DAO 接口 IDao。 然后,我们使用泛型实现一个 GenericDao 接口。
java复制代码public class GenericDao<T extends Serializable> implements IDao<T> { private Class<T> clazz; private EntityManager entityManager; public void setEntityManager(EntityManager entityManager) { this.entityManager = entityManager; } public void setClazz(Class<T> clazz) { this.clazz = clazz; } @Override public T get(Long id) { return entityManager.find(clazz, id); } @Override public List<T> getAll() { return entityManager.createQuery("From " + clazz.getName()).getResultList(); } /** 省略其他的 getter/setter 以及其他 IDao 中定义的方法实现 */ }
然后,我们增加一个实体类 Message,来验证一下如何使用 GenericDao 来对两个不同类型的实体类进行持久化操作。
java复制代码@Entity @Table(name = "MESSAGES") public class Message implements Serializable { @Id @GeneratedValue private Long id; private String topic; private String content; /** 省略其他的 getter/setter 以及无参构造器 */ }
在使用时,需要按照不同的实体类型创建不同的 GenericDao 实例,例如:
java复制代码private static void genericDao() { GenericDao<Event> eventGenericDao = new GenericDao<>(); eventGenericDao.setEntityManager(entityManager); eventGenericDao.setClazz(Event.class); eventGenericDao.save(new Event(new Date(), "Our very first event!")); eventGenericDao.save(new Event(new Date(), "A follow up event!")); final List<Event> events = eventGenericDao.getAll(); events.forEach(System.out::println); final GenericDao<Message> messageGenericDao = new GenericDao<>(); messageGenericDao.setEntityManager(entityManager); messageGenericDao.setClazz(Message.class); messageGenericDao.save(new Message("greeting", "hello!")); messageGenericDao.save(new Message("greeting", "how are you?")); final List<Message> messages = messageGenericDao.getAll(); messages.forEach(System.out::println); }
到此为止,大功告成。 如果看到这里,相信你对 DAO 模式应该有了一个清晰的认识,希望能在日后的开发中帮到你。 另外,一个与 DAO 模式类似的是 Repository 模式,在我之前的一篇文章中有介绍过《Spring Boot「31」DAO 模式与 Repository 模式对比》。
05-总结
在今天的文章中,我们一起盘点了 Spring 开发应用时访问数据库的几种方式,并简单介绍了它们的原理。 我把常见的几种方式列举在这篇文章中,作为一个总结,也作为一个参考,以便后续使用。
作者:Samson_bu