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

本文涉及的产品
RDS AI 助手,专业版
RDS MySQL DuckDB 分析主实例,集群系列 4核8GB
简介: 当我们开发应用时,访问数据库是一种常见的需求。 基本上所有需要持久化的数据,一般都存储在数据库中,例如常用的开源数据库 MySQL。 在今天的文章中,我将盘点一下 Java 应用访问数据的几种方式。

当我们开发应用时,访问数据库是一种常见的需求。 基本上所有需要持久化的数据,一般都存储在数据库中,例如常用的开源数据库 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 位于应用与关系数据库之间,如下图所示:

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

  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。 这也是为什么官网会有如下的提示:

@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

相关实践学习
每个IT人都想学的“Web应用上云经典架构”实战
本实验从Web应用上云这个最基本的、最普遍的需求出发,帮助IT从业者们通过“阿里云Web应用上云解决方案”,了解一个企业级Web应用上云的常见架构,了解如何构建一个高可用、可扩展的企业级应用架构。
MySQL数据库入门学习
本课程通过最流行的开源数据库MySQL带你了解数据库的世界。 &nbsp; 相关的阿里云产品:云数据库RDS MySQL 版 阿里云关系型数据库RDS(Relational Database Service)是一种稳定可靠、可弹性伸缩的在线数据库服务,提供容灾、备份、恢复、迁移等方面的全套解决方案,彻底解决数据库运维的烦恼。 了解产品详情:&nbsp;https://www.aliyun.com/product/rds/mysql&nbsp;
相关文章
|
3月前
|
存储 人工智能 NoSQL
AI大模型应用实践 八:如何通过RAG数据库实现大模型的私有化定制与优化
RAG技术通过融合外部知识库与大模型,实现知识动态更新与私有化定制,解决大模型知识固化、幻觉及数据安全难题。本文详解RAG原理、数据库选型(向量库、图库、知识图谱、混合架构)及应用场景,助力企业高效构建安全、可解释的智能系统。
|
6月前
|
存储 关系型数据库 数据库
附部署代码|云数据库RDS 全托管 Supabase服务:小白轻松搞定开发AI应用
本文通过一个 Agentic RAG 应用的完整构建流程,展示了如何借助 RDS Supabase 快速搭建具备知识处理与智能决策能力的 AI 应用,展示从数据准备到应用部署的全流程,相较于传统开发模式效率大幅提升。
附部署代码|云数据库RDS 全托管 Supabase服务:小白轻松搞定开发AI应用
|
5月前
|
监控 Java API
Spring Boot 3.2 结合 Spring Cloud 微服务架构实操指南 现代分布式应用系统构建实战教程
Spring Boot 3.2 + Spring Cloud 2023.0 微服务架构实践摘要 本文基于Spring Boot 3.2.5和Spring Cloud 2023.0.1最新稳定版本,演示现代微服务架构的构建过程。主要内容包括: 技术栈选择:采用Spring Cloud Netflix Eureka 4.1.0作为服务注册中心,Resilience4j 2.1.0替代Hystrix实现熔断机制,配合OpenFeign和Gateway等组件。 核心实操步骤: 搭建Eureka注册中心服务 构建商品
1032 3
|
4月前
|
SQL Java 数据库连接
Spring Data JPA 技术深度解析与应用指南
本文档全面介绍 Spring Data JPA 的核心概念、技术原理和实际应用。作为 Spring 生态系统中数据访问层的关键组件,Spring Data JPA 极大简化了 Java 持久层开发。本文将深入探讨其架构设计、核心接口、查询派生机制、事务管理以及与 Spring 框架的集成方式,并通过实际示例展示如何高效地使用这一技术。本文档约1500字,适合有一定 Spring 和 JPA 基础的开发者阅读。
536 0
|
3月前
|
消息中间件 缓存 Java
Spring框架优化:提高Java应用的性能与适应性
以上方法均旨在综合考虑Java Spring 应该程序设计原则, 数据库交互, 编码实践和系统架构布局等多角度因素, 旨在达到高效稳定运转目标同时也易于未来扩展.
210 8
|
4月前
|
缓存 Java 应用服务中间件
Spring Boot配置优化:Tomcat+数据库+缓存+日志,全场景教程
本文详解Spring Boot十大核心配置优化技巧,涵盖Tomcat连接池、数据库连接池、Jackson时区、日志管理、缓存策略、异步线程池等关键配置,结合代码示例与通俗解释,助你轻松掌握高并发场景下的性能调优方法,适用于实际项目落地。
800 5
|
4月前
|
存储 弹性计算 Cloud Native
云原生数据库的演进与应用实践
随着企业业务扩展,传统数据库难以应对高并发与弹性需求。云原生数据库应运而生,具备计算存储分离、弹性伸缩、高可用等核心特性,广泛应用于电商、金融、物联网等场景。阿里云PolarDB、Lindorm等产品已形成完善生态,助力企业高效处理数据。未来,AI驱动、Serverless与多云兼容将推动其进一步发展。
253 8
|
5月前
|
Java 应用服务中间件 开发者
Spring Boot 技术详解与应用实践
本文档旨在全面介绍 Spring Boot 这一广泛应用于现代企业级应用开发的框架。内容将涵盖 Spring Boot 的核心概念、核心特性、项目自动生成与结构解析、基础功能实现(如 RESTful API、数据访问)、配置管理以及最终的构建与部署。通过本文档,读者将能够理解 Spring Boot 如何简化 Spring 应用的初始搭建和开发过程,并掌握其基本使用方法。
460 2
|
5月前
|
人工智能 监控 安全
如何快速上手【Spring AOP】?核心应用实战(上篇)
哈喽大家好吖~欢迎来到Spring AOP系列教程的上篇 - 应用篇。在本篇,我们将专注于Spring AOP的实际应用,通过具体的代码示例和场景分析,帮助大家掌握AOP的使用方法和技巧。而在后续的下篇中,我们将深入探讨Spring AOP的实现原理和底层机制。 AOP(Aspect-Oriented Programming,面向切面编程)是Spring框架中的核心特性之一,它能够帮助我们解决横切关注点(如日志记录、性能统计、安全控制、事务管理等)的问题,提高代码的模块化程度和复用性。