天天看点

Spring 应用如何访问数据库,看这一篇就够了!

当我们开发应用时,访问数据库是一种常见的需求。 基本上所有需要持久化的数据,一般都存储在数据库中,例如常用的开源数据库 MySQL。 在今天的文章中,我将盘点一下 Java 应用访问数据的几种方式。

在 Java 开发中,常见的访问数据库的方式有如下几种:

  1. 通过 JDBC 访问数据库。
  2. 通过 ORM 框架访问数据库,例如 Hibernate、Mybatis 等(严格意义上 MyBatis 并不能算是一个 ORM 框架,它底层通过 JDBC 完成数据库操作)。
  3. 通过 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 中提供了四种访问数据库的方式:

  1. org.springframework.jdbc.core.JdbcTemplate,是最普遍、常用的方法。
  2. java复制代码
  3. // 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))");
  4. org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate,通过代理方式,封装了 JdbcTemplate,并提供了额外的访问接口。 主要区别在于 JdbcTemplate 中的 '?' 表示参数,NamedParameterJdbcTemplate 中使用变量名表示参数 ':xxx'
  5. java复制代码
  6. 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);
  7. org.springframework.jdbc.core.simple.SimpleJdbcInsert 和 org.springframework.jdbc.core.simple.SimpleJdbcCall 是针对插入、存储过程调用进行的优化、简化。 实际的底层操作,仍然要靠 JdbcTemplate 来完成。 它们通过 JDBC 驱动提供的元数据信息 java.sql.DatabaseMetaData 来自动检测表中的列信息、存储过程中的入参和出参信息。
  8. 通过继承 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 位于应用与关系数据库之间,如下图所示:

Spring 应用如何访问数据库,看这一篇就够了!

Hibernate 提供了两组风格的 API,一组是对 JPA 的实现,一组是原生 API 的实现。 这两组 API 的关系如下:

Spring 应用如何访问数据库,看这一篇就够了!

其中,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 的几种方式:

  1. Hibernate 原生 API 搭配 hibernate.cfg.xml(用来配置 SessionFactory)、xx.hbm.xml(配置 Object Mapping,一般一个 Entity 对应一个 hbm.xml 文件,hibernate.cfg.xml 中通过 <mapping resource="xx" /> 指定)。
  2. java复制代码
  3. 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();
  4. Hibernate 原生 API 搭配 hibernate.cfg.xml、@Entity(hibernate.cfg.xml 中通过 <mapping class="xx" /> 指定) 等注解。
  5. 使用 JPA 中的 persistence.xml 搭配 @Entity 等注解。
  6. xml复制代码
  7. <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。 有两种方式:

  1. 直接创建 Service 对象,并关联到 ServiceRegistry
  2. 创建 ServiceInitiator 对象,并将其交给 ServiceRegistry,在需要时,会通过 ServiceInitiator 创建 Service(某种形式的 lazy instantiation)

ServiceRegistry 通过 createServiceBinding 方法注册关联关系,该方法接受 Service 实例或 ServiceInitiator 实例作为参数。

Hibernate 中有三种类型的 ServiceRegistry 实现,它们一起形成了 Hibernate 中的 ServiceRegistry 层次结构。

  1. 第一种,BootstrapServiceRegistry,它包含(且仅包含)三个 binding。
  2. ClassLoaderService,该服务定义了 Hibernate 与 ServiceLoader 交互的能力。 Hibernate 可能运行在不同的环境中,例如应用服务器、OSGi 容器、或者其他的模块化类加载系统,而导致类加载过程存在差异性。 ClassLoaderService 提供了一个抽象的接口,屏蔽了不同环境的实现差异而导致的复杂性。 它的主要功能包括:根据名称定位类、定位资源文件、集成 JDK 原生的 ServiceLoader 等。
  3. IntegratorService,负责管理所有的 Integrator。 Integrator 有三种来源:第一种是 IntegratorServiceImpl 中注册的,例如 BeanValidationIntegrator; 第二种是通过 BootstrapServiceRegistryBuilder#with 注册的; 第三种是通过 ClassLoaderService 在 /META-INF/services/org.hibernate.integrator.spi.Integrator 中,通过 SPI 发现机制发现的。
  4. StrategySelector,是 short naming(简称)服务。提供简称到全量名的转换,使配置时可以使用简称,减少心智负担。
  5. 在使用过程中,BootstrapServiceRegistry 没有父 ServiceRegistry,而且它包含的 binding 不允许修改(增加、删除)。
  6. StandardServiceRegistry 是 Hibernate 中主要的 ServiceRegistry。在使用过程中,它通常作为 BootstrapServiceRegistry 的“子容器”。 它持有了 Hibernate 使用大多数 Service。常见的 Service 列表可以在 org.hibernate.service.StandardServiceInitiators 中查看。 与 BootstrapServiceRegistry 不同的是,它维护的 Service 是可以修改的(增加或替换)。
  7. 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。 这也是为什么官网会有如下的提示:

Spring 应用如何访问数据库,看这一篇就够了!

@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 接口及其实现;
  • 应用层服务等其他需要访问数据库的部分。

下面,我给出一个简单的实例。

  1. 首先,需要定义一个实体类。
  2. java复制代码
  3. public class Event { private Long id; private Date time; private String title; /** 省略属性的 getter/setter 及无参构造器 */ }
  4. 其次,定义一个 DAO 接口,并提供一个简单实现。
  5. java复制代码
  6. 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 中其他方法的实现 */ }
  7. 最后,在应用层使用 DAO 实现,来完成数据库访问。
  8. java复制代码
  9. 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

链接:https://juejin.cn/post/7242312269889093689