spring data:spring-data-jdbc spring-data-relational 源码解析 (2)

最近这几年在做数据中台相关的项目,有个技术点就是要支持多款数据库,尤其是一些国产数据库, sql 语法多样,如何做统一就是一个我们面临的一个难题,在扒 JPA 相关的代码时,发现了一个可以参考的点,就是 spring-data-relational.

简介

  • spring-data-jdbc 一般通过 starter 引入:
    并随 hikariCP 连接池,一起引入到项目中。
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-jdbc</artifactId>
        </dependency>

在这里插入图片描述

Spring Data JDBC 的目的有别于 JPA 的就在于概念简单化:

If you load an entity, SQL statements get run. Once this is done, you have a completely loaded entity. No lazy loading or caching is done.
If you save an entity, it gets saved. If you do not, it does not. There is no dirty tracking and no session.
There is a simple model of how to map entities to tables. It probably only works for rather simple cases. If you do not like that, you should code your own strategy. Spring Data JDBC offers only very limited support for customizing the strategy with annotations.
如果你加载一个实体,SQL语句就会运行。完成此操作后,您将拥有一个完全加载的实体。不进行延迟加载或缓存。
如果你保存一个实体,它就会被保存。如果你不这样做,它就不会。没有脏跟踪,也没有会话。
有一个如何将实体映射到表的简单模型。它可能只适用于相当简单的情况。如果你不喜欢这样,你应该编写自己的策略。Spring Data JDBC对使用注释自定义策略的支持非常有限。

简单来说就是抛弃了 JPA 后面 hibernate 所隐藏的复杂部分,让我们所用即所得。没有复杂的会话、跟踪等等。

  • Spring-data-relational 是 spring-data-jdbc 的依赖部分,主要面向关系型数据库的 DML.
    在这里插入图片描述
    对于 spring-data 的路线,多是以 DML 为主,配合 liquid、flyway 等数据库迁移工具来保证,对于 JPA 以 hibernate 为根基的 auto-ddl 并不在这里适用。

项目特点

  1. 高度抽象:Spring Data Relational(通过Spring Data JPA等模块)提供了对关系数据库操作的高度抽象,使得开发者可以通过简单的接口和方法名约定来执行复杂的数据库操作。
  2. 一致性API:无论底层使用哪种关系数据库,Spring Data都提供了一致的编程模型,降低了数据库迁移的复杂性。
  3. 强大的查询能力:支持基于方法名的查询推导、Criteria API、JPQL(Java Persistence Query Language)等多种查询方式,满足复杂的查询需求。
  4. 集成Spring框架:无缝集成Spring框架,利用Spring的依赖注入(DI)和面向切面编程(AOP)等特性,简化开发。
  5. 丰富的生态系统:Spring Data是Spring生态系统的一部分,与Spring Boot、Spring Cloud等项目紧密集成,支持快速开发和部署。

解决的主要问题

  • 简化数据库访问层开发:通过提供一套标准的接口和抽象,减少了数据库访问层(DAO层)的模板代码,使开发者能够更专注于业务逻辑的实现。
  • 提高开发效率:通过自动化的查询生成和强大的查询能力,减少了手动编写SQL语句的需求,提高了开发效率。
  • 增强系统的可维护性和可扩展性:通过一致的编程模型和灵活的扩展机制,使得系统在面对数据库变更或扩展时能够更容易地适应。

关联的项目

  • Spring Data JPA:Spring Data的一个子项目,专门用于简化JPA(Java Persistence API)的使用。
  • Spring Boot:提供了Spring Data的自动配置功能,使得在Spring Boot项目中集成Spring Data变得更加简单。
  • Hibernate:作为JPA的一个实现,经常与Spring Data JPA一起使用,提供ORM(对象关系映射)功能。
  • Spring Framework:Spring Data是Spring框架的一部分,依赖于Spring的核心功能,如依赖注入和AOP。

如何引入到项目工程中

  1. 添加依赖:在项目的pom.xml( 中添加Spring Data JDBC starter的依赖。
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-jdbc</artifactId>
        </dependency>
  1. 配置数据源:在application.propertiesapplication.yml中配置数据库连接信息。

  2. 定义实体类:根据数据库表结构定义相应的实体类,并不需要像 JPA 注解进行标注,定义简单 pojo 即可。
    在这里插入图片描述

  3. 创建Repository接口:继承Spring Data的JpaRepositoryCrudRepository接口,定义数据访问方法。

  4. 使用Repository:在业务层或服务层中注入Repository接口,通过调用其方法来执行数据库操作。

源码分析框架

下面我们以一个简单的自定义接口实现来看源码层是如何对一个查询处理并生成为 SQL 的。

  1. 接口层
    对于 Repository 接口来说,本身不含任何实现,Spring 体系通过动态代理进行方法调用,采用反射的形式。
interface CategoryRepository extends CrudRepository<Category, Long>, WithInsert<Category> {
    List<Category> findByName(String name);
}

CategoryRepository 在自定义查询方法时,代理给了 SimpleJdbcRepository ,该 Repository 提供了一般 JDBC 查询所需要的方法。如果不需要自定义查询方法,使用 Repository 已定义好的就采用该类代理实现。
在这里插入图片描述
在这里要另外需要注意的就是

List chain = this.advised.getInterceptorsAndDynamicInterceptionAdvice(method, targetClass);

这个 chain 提供了非常灵活的扩展能力:
在这里插入图片描述

  1. 实现层:深入JpaRepository的实现类(通常是Spring Data 内部实现,非直接暴露给开发者),了解查询方法的解析和执行过程。
    调用方法有很深的调用栈:
    在这里插入图片描述
    在这里插入图片描述
    PartTree 就是进行方法名解析的类,并将方法名转成 Criteria 的元数据。
    在这里插入图片描述
    最终,就到了这里,
   protected ParametrizedQuery complete(@Nullable Criteria criteria, Sort sort) {
        RelationalPersistentEntity<?> entity = this.entityMetadata.getTableEntity();
        Table table = Table.create(this.entityMetadata.getTableName());
        MapSqlParameterSource parameterSource = new MapSqlParameterSource();
        SelectBuilder.SelectLimitOffset limitOffsetBuilder = this.createSelectClause(entity, table);
        SelectBuilder.SelectWhere whereBuilder = this.applyLimitAndOffset(limitOffsetBuilder);
        SelectBuilder.SelectOrdered selectOrderBuilder = this.applyCriteria(criteria, entity, table, parameterSource, whereBuilder);
        selectOrderBuilder = this.applyOrderBy(sort, entity, table, selectOrderBuilder);
        SelectBuilder.BuildSelect completedBuildSelect = selectOrderBuilder;
        if (this.lockMode.isPresent()) {
            completedBuildSelect = selectOrderBuilder.lock(((Lock)this.lockMode.get()).value());
        }

        Select select = ((SelectBuilder.BuildSelect)completedBuildSelect).build();
        // 生成 sql
        String sql = SqlRenderer.create(this.renderContextFactory.createRenderContext()).render(select);
        return new ParametrizedQuery(sql, parameterSource);
    }

render 方法在渲染 sql 的时候调用 spring-data-relational 的 sql 部分,使用visitor 模式(前面几个系列 spotbugs,p3c-pmd 都有涉猎此设计模式,在进行操作和数据分离方面有非常广泛的应用)。

  1. 扩展点:研究如何通过自定义查询、使用@Query注解等方式来扩展Spring Data 的功能。
    @Query("SELECT * FROM Category WHERE description = :description")
    List<Category> findByDD(String description);

对于这种 @Query 注解进行处理的方法与上面的 PartTree 转成 criteria 的元数据不同,由用户自己定义 sql ,就避免的进行 SQL 创建的过程。所以很简单地从注解中取出:

	@Nullable
    private <T> T getMergedAnnotationAttribute(String attribute) {
        Query queryAnnotation = (Query)AnnotatedElementUtils.findMergedAnnotation(this.method, Query.class);
        return AnnotationUtils.getValue(queryAnnotation, attribute);
    }
------------------
  public Object execute(Object[] objects) {
        RelationalParameterAccessor accessor = new RelationalParametersParameterAccessor(this.getQueryMethod(), objects);
        ResultProcessor processor = this.getQueryMethod().getResultProcessor().withDynamicProjection(accessor);
        JdbcQueryExecution.ResultProcessingConverter converter = new JdbcQueryExecution.ResultProcessingConverter(processor, this.converter.getMappingContext(), this.converter.getEntityInstantiators());
        RowMapper<Object> rowMapper = this.determineRowMapper(this.rowMapperFactory.create(this.resolveTypeToRead(processor)), converter, accessor.findDynamicProjection() != null);
        JdbcQueryExecution<?> queryExecution = this.getQueryExecution(this.queryMethod, this.determineResultSetExtractor(rowMapper), rowMapper);
        MapSqlParameterSource parameterMap = this.bindParameters(accessor);
        String query = this.determineQuery(); // 这里
        if (ObjectUtils.isEmpty(query)) {
            throw new IllegalStateException(String.format("No query specified on %s", this.queryMethod.getName()));
        } else {
            return queryExecution.execute(this.processSpelExpressions(objects, parameterMap, query), parameterMap);
        }
    }

反射的方法执行也从 PartTreeJdbcQuery 切换到 StringBasedJdbcQuery
在这里插入图片描述
在这里插入图片描述
而这个 queries 的初始化要在更早的 IOC 过程:
org.springframework.data.repository.core.support.RepositoryFactoryBeanSupport#afterPropertiesSet
org.springframework.data.repository.core.support.RepositoryFactorySupport#getRepository(java.lang.Class, org.springframework.data.repository.core.support.RepositoryComposition.RepositoryFragments)

public QueryExecutorMethodInterceptor(RepositoryInformation repositoryInformation, ProjectionFactory projectionFactory, Optional<QueryLookupStrategy> queryLookupStrategy, NamedQueries namedQueries, List<QueryCreationListener<?>> queryPostProcessors, List<RepositoryMethodInvocationListener> methodInvocationListeners) {
        this.repositoryInformation = repositoryInformation;
        this.namedQueries = namedQueries;
        this.queryPostProcessors = queryPostProcessors;
        this.invocationMulticaster = (RepositoryInvocationMulticaster)(methodInvocationListeners.isEmpty() ? NoOpRepositoryInvocationMulticaster.INSTANCE : new RepositoryInvocationMulticaster.DefaultRepositoryInvocationMulticaster(methodInvocationListeners));
        this.resultHandler = new QueryExecutionResultHandler(RepositoryFactorySupport.CONVERSION_SERVICE);
        if (!queryLookupStrategy.isPresent() && repositoryInformation.hasQueryMethods()) {
            throw new IllegalStateException("You have defined query methods in the repository but do not have any query lookup strategy defined. The infrastructure apparently does not support query methods");
        } else {
            this.queries = (Map)queryLookupStrategy.map((it) -> {
                return this.mapMethodsToQuery(repositoryInformation, it, projectionFactory);
            }).orElse(Collections.emptyMap());
        }
    }
  1. relational 渲染 sql
    扒这段代码地原因在文章最开始也讲了,如何规避多数据库适配地场景。手上的项目要支持9款数据库: oracle,ms,mysql,mariadib,pg,highgo,oscar,kingbase,dameng 。各个数据库地语法也不尽相同。在系统库支持上,JPA是一个很好地解决方式,目前项目也是采用此技术栈。但是还有一点问题就是数据中台项目相关的功能,如面向数仓的数据查询连接器,connector 不仅仅是连接池连接的提供,还得有语法屏障的破除的能力,不然就是 sql 满天飞。基于此目的,我们查询了该包的源码实现:

relational 的代码结构如下:
在这里插入图片描述
有下面的几个包:

  • conversion
  • dialect :包括几个常见数据库的 dialect,如 oracle,mysql,mssql,pg 等,有别于 hibernate 的 dialect。
  • mapping:
  • query:
  • sql:渲染成sql语句

以最简单的分页查询为例:

 @Test
    public void test_sqlrender_field2() {
        List<Dialect> list = Arrays.asList(MySqlDialect.INSTANCE, PostgresDialect.INSTANCE, OracleDialect.INSTANCE,
                H2Dialect.INSTANCE, Db2Dialect.INSTANCE, SqlServerDialect.INSTANCE);
        for (Dialect dialect : list) {
            RenderContextFactory factory = new RenderContextFactory(dialect);
            RenderContext renderContext = factory.createRenderContext();
            BaseSelectBuilder baseSelectBuilder = new BaseSelectBuilder();
            baseSelectBuilder.from("tableTest");// 表名
            baseSelectBuilder.select(Column.create("id", tableTest.getTable()),
            Column.create("name", tableTest.getTable()),
            Column.create("age", tableTest.getTable()),
            Column.create("address", tableTest.getTable())); // 字段名
            baseSelectBuilder.limitOffset(10, 0); // 分页
            String s = SqlRenderer.create(renderContext).render(baseSelectBuilder.build());
            System.out.println(dialect.getClass().getName()+" "+s);
        }

    }

在这里插入图片描述
思路就打开了,我们可以利用 relational 提供的 api 开发数据库无关的应用功能。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值