跳转至

第2章:快速上手与对比实践

引言:对比式学习,理解ORM选型之道

在上一章中,我们回顾了Java持久化技术的演进历程,深入了解了从JDBC、Hibernate到现代ORM框架的发展脉络,以及Jimmer框架的创新特性。本章将通过实际代码和案例,帮助你快速掌握Jimmer的核心用法,并通过与其他主流框架的对比,加深对Jimmer优势的理解。

本章目标与学习方式

想象一下,你是一位技术负责人,正在为公司的新项目选择持久化方案。团队中有人推荐使用Hibernate,有人建议采用MyBatis,还有人提出尝试新兴的Jimmer框架。面对这些选择,你需要充分了解各个框架的优缺点,才能做出明智的决策。

本章的主要目标是:

  1. 实践掌握Jimmer的基础用法:通过具体代码示例,使你能够快速上手Jimmer框架
  2. 对比理解不同框架的特点:将Jimmer与Hibernate/JPA、MyBatis、原生JDBC进行并列比较
  3. 建立框架选型的思维模型:学会从多个维度评估ORM框架,形成自己的技术决策能力
  4. 掌握Jimmer解决实际问题的方法:通过典型案例,理解Jimmer如何解决传统框架的痛点

我们将采用"实例驱动"的学习方式,每个技术点都会通过具体的业务场景展示。例如,当讲解动态查询时,我们会展示一个电商平台的商品搜索功能,分别用四种框架实现,并分析比较它们的异同。

这种方法不仅帮助你理解抽象概念,更能使你体会到不同框架在实际应用中的优劣势。就像学开车一样,光看说明书不如实际坐在驾驶座上操作来得直观。

案例设计说明

为什么选择电商平台作为我们的案例项目?因为它包含了企业应用中最常见且具有代表性的数据模型和业务场景:

  1. 多样的实体关系
  2. 商品与分类:一对多关系
  3. 订单与订单项:一对多关系,带有丰富的业务规则
  4. 用户与角色:多对多关系,涉及权限管理
  5. 商品分类:自引用的树形结构

  6. 典型的业务场景

  7. 商品管理:基础的CRUD操作
  8. 商品搜索:复杂的动态查询条件
  9. 订单处理:涉及事务和状态管理
  10. 数据统计:聚合查询和报表生成

  11. 常见的技术挑战

  12. N+1查询问题:处理关联数据时的性能瓶颈
  13. 动态对象图:不同场景需要不同深度的数据
  14. 复杂查询:多条件、多表关联的查询优化
  15. 缓存管理:数据一致性与性能平衡

电商平台的这些特点,恰恰是各个ORM框架设计时需要重点考虑的问题,也是开发者在实际工作中经常遇到的挑战。通过这个案例,你可以直观地看到Jimmer如何优雅地解决这些问题,以及它与传统方案的差异。

就像建筑师通过模型展示设计理念一样,电商平台这个"模型"将帮助我们呈现Jimmer的核心设计思想和价值主张。

多框架对比的意义

"知己知彼,百战不殆"。只有了解多种技术方案,才能做出明智的选择。我们对比Jimmer、Hibernate/JPA、MyBatis和原生JDBC的目的在于:

  1. 建立参照系

当你初次接触一个新框架时,如果没有参照物,很难判断它的优劣。就像评价一辆新车,如果没有对比其他车型,你很难说它"加速快"还是"操控好"。通过与已知框架对比,你能更客观地评估Jimmer的价值。

  1. 理解设计取舍

每个框架都有其设计哲学和重点关注的问题。Hibernate追求对象关系映射的完整性,MyBatis注重SQL的掌控能力,而Jimmer则强调动态数据结构和类型安全。通过对比,你能理解这些设计取舍背后的思考。

  1. 明确适用场景

没有绝对完美的框架,只有更适合特定场景的选择。通过多框架对比,你将了解到: - 什么情况下Jimmer是最佳选择 - 什么场景可能更适合使用其他框架 - 如何在项目中合理组合多种技术方案

  1. 平滑技术迁移

如果你已经熟悉其他ORM框架,对比学习可以帮助你建立知识连接,快速掌握Jimmer。这就像学习一门新的编程语言,如果你能将它与已知语言对比,学习效率会大大提高。

通过这种对比式学习,你不仅能掌握Jimmer的使用方法,更能理解它为什么被设计成这样,以及它相比其他框架的独特价值。这些洞察将帮助你在实际项目中做出更明智的技术决策。

阅读指南

本章内容循序渐进,从环境搭建开始,逐步深入到实体建模、数据操作、关联处理、动态查询等方面。每个部分都遵循"问题-解决方案-对比分析"的结构,帮助你理解各个框架的异同。

针对不同背景的读者,我们提供以下阅读建议:

  1. ORM新手
  2. 建议按顺序阅读全章内容
  3. 重点关注案例中的问题描述和解决思路
  4. 尝试运行代码示例,加深理解

  5. Hibernate/JPA经验者

  6. 可以重点关注2.2.2节Jimmer的接口式实体和2.5.2节动态查询实现
  7. 注意比较Jimmer与Hibernate在关联处理和延迟加载方面的差异
  8. 思考Jimmer如何解决你在使用Hibernate时遇到的痛点

  9. MyBatis经验者

  10. 重点关注2.2.2节和2.6.2节,了解Jimmer的类型安全和动态DTO机制
  11. 对比Jimmer与MyBatis在SQL控制和对象映射方面的差异
  12. 思考如何将MyBatis的优势与Jimmer结合使用

  13. 技术决策者

  14. 关注每节末尾的对比分析和2.7节的综合案例
  15. 重点理解各框架的性能特性和适用场景
  16. 思考如何在现有项目中引入或迁移到Jimmer

本章大量使用代码示例,建议你在阅读时在IDE中尝试运行这些代码,亲身体验不同框架的使用感受。记住,理解概念很重要,但通过实践获得的体验更加深刻。

现在,让我们开始这段对比式学习之旅,探索Jimmer如何重新定义Java持久化的开发体验。

2.1 项目环境搭建

在开始深入探索Jimmer和其他ORM框架之前,我们需要先搭建一个完整的开发环境。想象一下,你刚刚受命负责一个新电商平台的研发工作,团队成员正在等待你确定技术栈和项目架构。在这样的场景下,合理的环境搭建和技术选型将直接影响项目的开发效率和未来的可维护性。

对于一个现代Java应用来说,我们不仅需要考虑开发语言和框架,还需要关注构建工具、数据库、容器化技术等多方面的选择。特别是当我们计划对比多种ORM框架时,如何设计一个既能体现各框架特点,又便于横向比较的项目结构就显得尤为重要。

本节将带你完成这个电商平台的基础环境搭建,包括技术栈选型、开发环境准备以及各ORM框架的基础配置。通过这一过程,你不仅能够为后续的学习做好准备,还能了解不同框架在项目初始阶段的配置差异,从而更好地理解它们的设计理念。

记住,好的开始是成功的一半。一个精心设计的项目结构和开发环境,会让后续的编码工作事半功倍。让我们开始动手搭建吧。

2.1.1 技术栈选择与规划

"我们应该使用什么技术栈来构建新的电商平台?"这可能是你作为技术负责人面临的第一个问题。在回答这个问题前,让我们先分析一下电商平台的核心需求和技术挑战。

电商平台需求与架构设计

电商平台本质上是一个处理商品、订单、用户等核心业务实体的系统。它需要应对的技术挑战包括:

  • 高并发访问:尤其在促销活动期间,系统需要处理大量的并发请求
  • 复杂的数据关系:商品、订单、用户之间存在丰富的关联关系
  • 动态的数据结构:不同场景下需要展示不同深度的数据
  • 高性能查询:特别是商品搜索和订单查询等核心功能
  • 数据一致性:在分布式环境下保证交易数据的一致性

基于这些需求,我们设计了以下电商系统的核心数据模型:

@startuml
!theme plain
skinparam defaultFontName Source Han Sans CN
skinparam defaultFontSize 14
skinparam classFontStyle bold
skinparam classFontSize 16
skinparam classFontColor #333333
skinparam classBackgroundColor #F8F8F8
skinparam classBorderColor #CCCCCC

package "电商核心模型" {
  class Product {
    + id: Long
    + name: String
    + price: BigDecimal
    + stock: Integer
    + attributes: JSONB
    + images: String[]
    + isPublished: Boolean
  }

  class Category {
    + id: Long
    + name: String
    + parentId: Long?
    + path: String
  }

  class Order {
    + id: Long
    + orderNo: String
    + totalAmount: BigDecimal
    + status: String
    + createdTime: LocalDateTime
  }

  class OrderItem {
    + id: Long
    + orderId: Long
    + productId: Long
    + quantity: Integer
    + price: BigDecimal
  }

  class User {
    + id: Long
    + username: String
    + email: String
    + phone: String?
    + isActive: Boolean
  }

  class Role {
    + id: Long
    + name: String
    + description: String?
  }

  class Address {
    + id: Long
    + userId: Long
    + recipient: String
    + phone: String
    + province: String
    + city: String
    + detail: String
    + isDefault: Boolean
  }
}

Category "1" *-- "0..*" Category : 父分类
Category "1" *-- "0..*" Product : 包含
Product "1" o-- "0..*" OrderItem : 引用
Order "1" *-- "1..*" OrderItem : 包含
User "1" *-- "0..*" Order : 创建
User "1" *-- "0..*" Address : 拥有
User "many" -- "many" Role : 拥有 >
Order "1" -- "1" Address : 使用 >

@enduml

这个数据模型清晰地展示了电商系统中的实体关系:商品分类形成树形结构;商品归属于分类;订单由多个订单项组成;用户可以创建订单、管理地址、拥有多个角色。模型设计涵盖了一对多关系(用户与订单)、多对多关系(用户与角色)和自引用关系(分类的树形结构),很好地代表了企业应用中常见的数据建模场景。

示例代码库简介

为了帮助你更好地理解和实践本书中的概念,我们提供了完整的配套示例代码库:jimmer-in-action-samples。这个代码库包含了书中所有章节的示例代码,你可以通过以下方式获取:

git clone https://github.com/lijma/jimmer-in-action-samples.git

示例代码库的目的不仅是展示Jimmer框架的功能,还包括与其他主流ORM框架的对比实现。通过这些示例,你可以:

  1. 在本地环境中运行和测试所有示例
  2. 直接对比不同框架实现同一功能的差异
  3. 在实际项目中参考或复用这些最佳实践
  4. 跟随书中的进度逐步深入学习

我强烈建议你在阅读本书的同时,克隆并探索这个代码库。实际的动手操作将帮助你更深入地理解概念,培养实践能力。

多项目代码组织方式

为了方便对比不同ORM框架的实现差异,我们采用"按章节和框架分离"的项目组织方式:

jimmer-in-action-samples/
├── chapter2/                # 第二章相关代码
│   ├── jimmer/              # Jimmer实现
│   ├── hibernate/           # Hibernate/JPA实现
│   ├── mybatis/             # MyBatis实现
│   ├── jdbc/                # 原生JDBC实现
│   └── common/              # 共享代码和资源
├── chapter3/                # 第三章相关代码(基于第二章演进)
│   └── ...

这种组织方式有几个显著优势:

  1. 便于横向比较:同一功能点的四种实现并列展示,直观对比
  2. 简化学习曲线:读者可以选择先深入学习一个框架,再横向对比其他框架
  3. 保持演进连续性:每章代码基于前一章演进,体现渐进式开发过程
  4. 独立可运行:每个框架项目都能独立运行,方便实践和测试

在共享代码和资源方面,我们将数据库脚本、共用工具类和测试数据等放在common目录下,确保不同框架实现基于相同的业务模型和测试数据,保证对比的公平性。

基础技术栈选择与理由

在开始构建项目前,我们需要确定基础技术栈。这些选择不仅关系到当前的开发体验,也会影响未来的维护和扩展。作为读者,无论你是已经熟悉这些技术,还是初次接触,理解这些选择的原因都能帮助你在实际工作中做出更明智的决策。

  1. JDK 21

作为Java开发者,你可能会疑惑:"最新的JDK 21是否稳定可靠?我们是否应该选择更保守的JDK 17?"

实际上,JDK 21不仅是最新的长期支持版本,它还引入了许多能显著提升开发效率的新特性:虚拟线程让我们能用同步的代码风格处理异步任务;增强的模式匹配简化了对复杂对象的处理;字符串模板让文本处理更加直观。这些特性在本书的示例代码中都会直接受益,让你体验到更现代的Java编程方式。

  1. Gradle 8.5+

如果你曾在Maven和Gradle之间犹豫,本书选择Gradle的原因很简单 - 它在管理多模块项目时的优势正好满足我们按框架分离示例代码的需求。尤其是当你需要为不同的框架(如Jimmer和Hibernate)配置不同的编译流程时,Gradle的灵活性会让你的工作轻松许多。

  1. PostgreSQL 15

虽然任何关系型数据库都能支持基本的SQL操作,但当你需要处理JSON数据和数组时,PostgreSQL的优势就非常明显了。在本书的电商示例中,这些高级数据类型能让你用更贴近业务模型的方式存储和查询数据,而不必为了适应数据库结构扭曲你的领域模型。

  1. Docker & Docker Compose

想象一下,能否让所有读者在几分钟内启动一个完全相同的开发环境?Docker正是这一挑战的答案。通过本书提供的Docker配置,你可以一键启动包含数据库、缓存等所有必要组件的环境,无论你使用的是Windows、macOS还是Linux系统。这样你就可以专注于学习ORM框架本身,而不是纠结于环境配置问题。

  1. Spring Boot 3.2+

作为Java生态中最流行的应用框架,Spring Boot让不同的ORM框架能够在统一的应用结构中运行,这对于我们比较不同ORM框架的实现差异至关重要。同时,它的自动配置特性也大大简化了每个框架的初始设置,让你能更快进入到核心概念的学习中。

这些技术选择反映了现代Java应用开发的最佳实践,它们共同构成了一个高效、灵活且易于理解的开发环境。在接下来的章节中,我们将基于这些基础技术,逐步构建并比较不同ORM框架的实现方式。即使你在实际工作中使用的技术栈与此不同,这里的原则和模式也同样适用,能够帮助你做出更合理的技术决策。

各框架技术栈对比

在共同的基础技术栈之上,四种ORM框架各有其特定的依赖和版本:

框架 核心依赖 版本 其他关键依赖
Jimmer org.babyfish.jimmer:jimmer-spring-boot-starter 0.8.69+ org.babyfish.jimmer:jimmer-sql-kotlin(可选Kotlin支持)
Hibernate/JPA org.springframework.boot:spring-boot-starter-data-jpa 3.2.0+ org.hibernate.orm:hibernate-core:6.4.0.Final
MyBatis org.mybatis.spring.boot:mybatis-spring-boot-starter 3.0.3+ com.github.pagehelper:pagehelper-spring-boot-starter(分页插件)
JDBC org.springframework.boot:spring-boot-starter-jdbc 3.2.0+ com.zaxxer:HikariCP(连接池)

在框架与技术栈的适配性方面:

  • Jimmer与JDK 21:Jimmer的核心设计(编译时代码生成、接口声明式实体)与JDK 21的记录模式和模式匹配相得益彰,能够发挥Java最新特性的优势。

  • Hibernate与PostgreSQL:Hibernate 6.x对PostgreSQL特有功能(如JSONB、数组类型)的支持已经相当成熟,可以充分利用数据库特性。

  • MyBatis与Spring Boot:MyBatis的Spring Boot Starter提供了良好的自动配置,简化了传统MyBatis繁琐的XML配置。

  • 所有框架与Gradle:所有框架都能良好地集成到Gradle构建系统中,特别是Jimmer的代码生成插件与Gradle的任务系统集成得很好。

通过以上技术栈的选择和规划,我们为电商平台项目奠定了坚实的技术基础。接下来,我们将详细介绍如何搭建这套开发环境。

2.1.2 开发环境准备

作为新项目的技术负责人,你需要为团队搭建一个标准的开发环境。理想情况下,无论是经验丰富的高级开发者,还是刚加入团队的新人,都应该能在短时间内完成环境配置并开始编码。这不仅提高了团队的整体效率,也减少了因环境差异导致的"在我电脑上能跑"问题。

基础环境搭建

我们的开发环境基于以下核心组件:

  1. JDK安装与配置

本项目使用JDK 21 LTS版本。可使用多种方式安装JDK:

# 使用SDKMAN(跨平台)
sdk install java 21.0.7-amzn

# macOS (使用Homebrew)
brew install --cask temurin

# Ubuntu/Debian Linux
sudo apt-get install temurin-21-jdk

# Windows
# 可从Oracle、Adoptium或Microsoft等官方渠道下载

安装完成后,设置JAVA_HOME环境变量并验证安装:

# 验证Java版本
java -version
# 输出示例: openjdk version "21.0.7" 2025-04-15 LTS
  1. Gradle安装与配置

本项目使用Gradle 8.5+版本:

# macOS (使用Homebrew)
brew install gradle

# 使用SDKMAN(跨平台)
sdk install gradle 8.5

# 手动安装(所有平台)
# 1. 下载Gradle二进制包
# 2. 解压到指定目录
# 3. 添加到PATH环境变量

验证Gradle安装:

gradle --version
# 应显示Gradle 8.5或更高版本
  1. Docker环境配置

Docker和Docker Compose是我们环境的核心部分,它们帮助我们统一数据库等基础设施:

# macOS/Windows
# 下载并安装Docker Desktop

# Ubuntu/Debian Linux
sudo apt-get update
sudo apt-get install docker-ce docker-compose
sudo systemctl enable docker
sudo systemctl start docker
sudo usermod -aG docker $USER

验证Docker安装:

docker --version
docker-compose --version
  1. IDE配置选项

虽然任何Java IDE或文本编辑器都可以用于开发,IntelliJ IDEA为Java和Kotlin开发提供了全面的支持。对于本书的示例,以下插件可能有助于提高开发效率:

  • Jimmer IDEA Plugin:为Jimmer实体提供代码提示和导航支持
  • Database Navigator:管理和查询PostgreSQL数据库
  • Gradle Plugin:集成Gradle构建功能
  • PlantUML Integration:查看和编辑UML图

Docker化数据库环境

为了确保所有开发者使用完全相同的数据库环境,我们使用Docker容器运行PostgreSQL和相关工具。

  1. PostgreSQL容器配置

jimmer-in-action-samples/chapter2/common/docker目录下,创建了专用的docker-compose.yml文件:

version: '3.8'

services:
  postgres:
    image: postgres:15
    container_name: postgres-ch2
    environment:
      POSTGRES_DB: jimmer_demo
      POSTGRES_USER: postgres
      POSTGRES_PASSWORD: postgres
    ports:
      - "5432:5432"
    volumes:
      - ./../sql:/docker-entrypoint-initdb.d
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U postgres"]
      interval: 5s
      timeout: 5s
      retries: 5

  pgadmin:
    image: dpage/pgadmin4
    container_name: pgadmin-ch2
    environment:
      PGADMIN_DEFAULT_EMAIL: admin@example.com
      PGADMIN_DEFAULT_PASSWORD: admin
    ports:
      - "5050:80"
    depends_on:
      - postgres

这个配置定义了两个服务:PostgreSQL数据库和pgAdmin管理工具。特别要注意的是,我们设置了健康检查,确保数据库完全就绪后才标记为可用。

  1. 数据库初始化脚本

common/sql目录中,提供了两个关键的SQL脚本:

  • 01-schema.sql:创建数据库表结构,包括Product、Category、Order等实体
  • 02-test-data.sql:插入测试数据,以便开发和测试

这些脚本通过Docker volume挂载到容器的初始化目录,实现了数据库的自动初始化。而且每个章节都有独立的容器和配置,避免了章节之间的相互干扰。

  1. 数据库管理

通过配置的pgAdmin工具,你可以在浏览器中访问http://localhost:5050来管理数据库,执行SQL查询,或者查看表结构。这对于理解ORM框架的底层行为非常有帮助,特别是在学习和调试阶段。

框架特定开发工具

为了高效使用各种ORM框架,以下是一些可用的专用工具:

  1. Jimmer开发工具

Jimmer框架有配套的IDEA插件和Gradle插件:

  • IDEA插件提供了接口式实体与生成类之间的导航、智能补全等功能
  • Gradle插件自动执行代码生成任务,集成到构建流程中

此外,Jimmer官方提供了Web控制台用于探索和调试,可用于查看生成的SQL和缓存状态。

  1. Hibernate/JPA工具

JPA开发可用的工具包括:

  • JPA Buddy:IDEA插件,提供实体创建和管理功能
  • Hibernate Query Analyzer:用于分析和优化HQL查询
  • JPA Model Gen:从数据库生成实体类

  • MyBatis工具

MyBatis开发相关工具:

  • MyBatis Generator:自动生成实体类、Mapper接口和XML
  • MyBatisX:IDEA插件,提供Mapper和XML之间的导航
  • PageHelper:简化分页查询

  • SQL开发工具

对于所有框架,理解底层SQL是重要的:

  • DBeaver:跨平台数据库工具,支持PostgreSQL和其他数据库
  • Database Navigator:IDEA内置的数据库工具
  • p6spy:SQL日志库,可记录和格式化应用程序执行的SQL语句

测试环境准备

良好的测试实践是项目成功的关键。以下是本项目使用的测试工具:

  1. 单元测试框架

所有项目使用JUnit 5和AssertJ进行测试:

// build.gradle
testImplementation 'org.junit.jupiter:junit-jupiter:5.9.2'
testImplementation 'org.assertj:assertj-core:3.24.2'
  1. API测试工具

REST API测试可使用的工具:

  • REST Assured:Java库,用于验证REST服务
  • Testcontainers:提供临时数据库实例用于集成测试
  • Spring Boot Test:集成测试支持

  • 数据库测试策略

数据库测试的常用做法包括:

  • 测试前重置数据库状态
  • 使用事务回滚保持测试独立性
  • 使用特定框架的测试辅助工具

  • 性能测试工具

评估不同框架性能的工具包括:

  • JMH (Java Microbenchmark Harness):微基准测试工具
  • Gatling:负载测试框架
  • Metrics API:收集性能指标

环境验证与问题排查

为了帮助你确认环境配置正确,我们提供了一个全面的检查脚本check-environment.sh

#!/bin/bash

# 设置颜色输出
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[0;33m'
NC='\033[0m' # No Color

# 省略部分脚本内容...

# 主函数
main() {
    echo "==================================================="
    echo "     Jimmer实战 - 第2章 环境检查脚本                "
    echo "==================================================="

    # 检查JDK、Docker、Gradle等基础工具
    check_basic_tools

    # 检查Docker容器状态
    check_docker_containers

    # 检查项目配置文件
    check_project_configuration

    # 显示最终结果
    if [[ $all_checks_passed ]]; then
        print_success "环境检查完成,所有检查均通过!"
    else
        print_error "环境检查发现问题,请解决上述问题后再继续。"
    fi
}

这个脚本会检查基础工具、Docker容器状态和项目配置,帮助你快速发现和解决可能的环境问题。

开发环境启动流程

现在,让我们总结一下基础开发环境的启动流程:

  1. 克隆代码库

    git clone https://github.com/lijma/jimmer-in-action-samples.git
    cd jimmer-in-action-samples/chapter2
    

  2. 启动Docker环境

    cd common/docker
    docker-compose up -d
    

  3. 验证环境状态

    cd ..
    ./check-environment.sh
    

通过以上步骤,你就完成了基础开发环境的搭建,包括必要的工具安装和数据库环境准备。接下来,我们将在2.1.3节中详细介绍如何为每个ORM框架配置具体的项目结构和依赖。

完成环境准备后,你已经拥有了一个标准化、可复制的基础环境,这为我们后续比较不同ORM框架提供了公平的基础。

2.1.3 基础项目配置

"选择正确的持久化方案就像选择一把合适的剑——它不仅要锋利,还要适合你的手。"一位拥有20年Java开发经验的架构师曾这样告诉我。在企业级应用开发中,持久化框架的选择往往决定了项目的长期成功。不同的框架虽然都能完成数据库操作这一基本任务,但它们的设计理念、编程模型和性能特性却各不相同。

想象一下,当你站在技术选型的十字路口,面前是传统的JDBC、成熟的Hibernate、标准化的JPA、灵活的MyBatis和新锐的Jimmer,你会如何做出选择?仅靠框架官网的介绍显然是不够的。真正的理解来自于实践和对比——当这些框架处理相同问题时的不同表现,才能真正揭示它们的本质。

这一节,我们将带你进行一次"框架探索之旅"。通过一个电子商城的分类管理功能,我们将展示五种不同框架的配置和实现方式。这个示例虽然简单,但蕴含了企业应用中常见的复杂关系——树形自引用结构。正是这种结构,最能测试框架处理对象关系的能力。

在我们的示例项目jimmer-in-action-samples/chapter2中,你将看到五个独立但结构相似的子项目:

jimmer-in-action-samples/chapter2/
├── common/              # 共享配置和工具
   ├── docker/         # Docker配置
   └── sql/           # SQL脚本
├── jdbc/               # JDBC示例项目
├── hibernate/          # Hibernate示例项目
├── jpa/                # JPA示例项目
├── mybatis/            # MyBatis示例项目
└── jimmer/             # Jimmer示例项目

为了确保比较的公平性,所有项目都连接到同一个PostgreSQL数据库,使用相同的数据模型和初始数据。这就像是在相同的赛道上进行一场技术马拉松——相同的起点和终点,但每个参赛者都有自己独特的跑步姿势。

统一的数据世界

在探索框架差异之前,让我们先理解它们所共享的数据世界。我们的电子商城数据库设计得简单而实用,核心是商品分类(Category)和商品(Product)这两个实体。特别值得注意的是分类表的设计——它采用了自引用的树形结构:

-- 商品分类表
CREATE TABLE category (
    id BIGSERIAL PRIMARY KEY,
    name VARCHAR(100) NOT NULL,
    parent_id BIGINT,
    path VARCHAR(200),
    created_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
    modified_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
    CONSTRAINT fk_category_parent FOREIGN KEY (parent_id) REFERENCES category(id)
);

这种设计在现实世界中非常常见。想想亚马逊的商品分类:电子产品包含手机、电脑等子类别;服装包含男装、女装等子类别。这种层次结构不仅直观,还允许无限层级的分类扩展。

然而,从技术角度看,这种自引用结构也带来了挑战: 1. 如何优雅地表示父子关系 2. 如何处理循环引用问题 3. 如何高效查询特定层级的分类 4. 如何避免N+1查询性能问题

正是这些挑战,让不同框架的差异变得清晰可见。

初始数据:构建分类树

为了让示例更加生动,我们预置了一些分类和商品数据:

-- 插入分类数据
INSERT INTO category (id, name, parent_id, path) VALUES 
(1, '电子产品', NULL, '/1'),
(2, '手机', 1, '/1/2'),
(3, '电脑', 1, '/1/3'),
(4, '服装', NULL, '/4'),
(5, '男装', 4, '/4/5'),
(6, '女装', 4, '/4/6');

这些数据构成了一个两层的分类树,根分类是"电子产品"和"服装",各自有自己的子分类。特别注意path字段——它存储了从根到当前节点的完整路径,是一种称为"物化路径"(Materialized Path)的树形结构优化技术。这种技术允许我们通过简单的模式匹配快速找到所有子孙节点,而不需要复杂的递归查询。

现在,我们的舞台已经搭建完成,数据世界已经构建好。接下来,就让我们看看各个框架如何在这个相同的世界中施展各自的魔法。从最基础的JDBC开始,我们将一步步了解每种框架的独特风格和优势。

1. JDBC项目配置

让我们从最基础的JDBC项目开始探索。作为Java程序员几乎都要接触的技术,JDBC提供了与数据库交互的基本API。与其他ORM框架相比,JDBC是最"接近底层"的选择,它让开发者获得对SQL和数据库操作的完全控制。

JDBC项目结构如下:

jimmer-in-action-samples/chapter2/jdbc/
├── build.gradle              # 项目构建脚本
├── src/
│   ├── main/
│   │   ├── java/
│   │   │   └── org/lijma/jdbc/samples/
│   │   │       ├── entity/           # 实体类
│   │   │       │   └── Category.java
│   │   │       ├── repository/       # 数据访问层
│   │   │       │   ├── CategoryRepository.java
│   │   │       │   └── CategoryRepositoryImpl.java
│   │   │       └── JdbcApplication.java  # 应用入口
│   │   └── resources/
│   │       └── application.yml       # 配置文件
│   └── test/
│       └── ...                       # 测试代码

构建配置

JDBC项目的build.gradle配置简洁明了,仅包含必要的依赖:

plugins {
    id 'java'
    id 'org.springframework.boot' version '3.2.0'
    id 'io.spring.dependency-management' version '1.1.4'
}

group = 'org.lijma.jdbc.samples'
version = '0.0.1-SNAPSHOT'

java {
    sourceCompatibility = '17'
}

repositories {
    mavenCentral()
}

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-jdbc'
    implementation 'org.springframework.boot:spring-boot-starter-web'

    // 数据库驱动
    runtimeOnly 'org.postgresql:postgresql'

    // 开发辅助工具
    compileOnly 'org.projectlombok:lombok'
    annotationProcessor 'org.projectlombok:lombok'

    // 测试相关
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
    testImplementation 'org.junit.jupiter:junit-jupiter-api'
    testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine'
}

test {
    useJUnitPlatform()
}

这里使用了Spring Boot的JDBC启动器,它包含了必要的JDBC依赖和连接池。PostgreSQL驱动负责与数据库的实际通信。

应用配置

application.yml文件包含基本的数据源配置:

spring:
  application:
    name: jdbc-sample

  datasource:
    url: jdbc:postgresql://localhost:5432/jimmer_demo
    username: postgres
    password: postgres
    driver-class-name: org.postgresql.Driver
    hikari:
      connection-timeout: 30000
      maximum-pool-size: 10
      minimum-idle: 5

server:
  port: 8084

logging:
  level:
    org.springframework.jdbc: DEBUG

配置很直观:指定数据库连接URL、用户名和密码,以及Hikari连接池的相关参数。为了方便调试,我们设置了较高的JDBC日志级别。

实体定义

使用JDBC时,实体类只是简单的Java对象(POJO),没有任何ORM注解。以Category类为例:

package org.lijma.jdbc.samples.entity;

import lombok.Data;
import java.time.LocalDateTime;

@Data
public class Category {
    private Long id;
    private String name;
    private Long parentId;  // 父分类ID
    private Category parent; // 父分类对象
    private String path;
    private LocalDateTime createdTime;
    private LocalDateTime modifiedTime;
}

这里使用了Lombok的@Data注解来自动生成getter/setter、toString等方法,使代码更简洁。注意parentIdparent两个属性:它们反映了同一个关联关系的两个方面——ID引用和对象引用。在纯JDBC中,我们需要手动维护这种关联。

数据访问层

JDBC项目的数据访问层分为接口和实现:

// 接口定义
public interface CategoryRepository {
    List<Category> findAll();
}

// 实现类
@Repository
public class CategoryRepositoryImpl implements CategoryRepository {

    private final JdbcTemplate jdbcTemplate;

    public CategoryRepositoryImpl(DataSource dataSource) {
        this.jdbcTemplate = new JdbcTemplate(dataSource);
    }

    @Override
    public List<Category> findAll() {
        String sql = "SELECT id, name, parent_id, path, created_time, modified_time FROM category";
        return jdbcTemplate.query(sql, new CategoryRowMapper());
    }

    private static class CategoryRowMapper implements RowMapper<Category> {
        @Override
        public Category mapRow(ResultSet rs, int rowNum) throws SQLException {
            Category category = new Category();
            category.setId(rs.getLong("id"));
            category.setName(rs.getString("name"));
            category.setParentId(rs.getLong("parent_id"));
            if (rs.wasNull()) {
                category.setParentId(null);
            }
            category.setPath(rs.getString("path"));
            category.setCreatedTime(rs.getTimestamp("created_time").toLocalDateTime());
            category.setModifiedTime(rs.getTimestamp("modified_time").toLocalDateTime());
            return category;
        }
    }
}

在这里,我们使用了Spring的JdbcTemplate来简化JDBC操作,但仍需要手动编写SQL查询和对象映射代码。特别是CategoryRowMapper内部类,它负责将数据库结果集的一行转换为一个Category对象,这是JDBC方式最明显的特征之一。

JDBC方式的特点

JDBC方式的优势在于: 1. 完全控制SQL:开发者可以精确控制执行的每一条SQL语句 2. 最小依赖:只需要JDBC驱动,没有额外的框架依赖 3. 学习曲线平缓:基于Java标准API,容易理解

但同时也有明显的劣势: 1. 大量样板代码:需要手动处理连接获取、SQL执行、结果映射等 2. 缺乏ORM特性:没有自动的对象-关系映射 3. 手动管理关联:需要额外代码处理对象间的关联关系

2. Hibernate项目配置

从JDBC的原始方法,我们现在转向第一个成功的Java ORM框架——Hibernate。作为JPA规范的前身和主要实现,Hibernate允许开发者以面向对象的方式思考数据访问问题,而非关系型数据库的表与行。

Hibernate项目结构如下:

jimmer-in-action-samples/chapter2/hibernate/
├── build.gradle              # 项目构建脚本
├── src/
│   ├── main/
│   │   ├── java/
│   │   │   └── org/lijma/hibernate/samples/
│   │   │       ├── config/            # 配置类
│   │   │       │   └── HibernateConfig.java
│   │   │       ├── entity/            # 实体类
│   │   │       │   └── Category.java
│   │   │       ├── repository/        # 数据访问层
│   │   │       │   ├── CategoryRepository.java
│   │   │       │   └── CategoryRepositoryImpl.java
│   │   │       └── HibernateApplication.java
│   │   └── resources/
│   │       └── application.yml        # 配置文件
│   └── test/
│       └── ...                        # 测试代码

构建配置

Hibernate项目的build.gradle明显比JDBC复杂一些:

plugins {
    id 'java'
    id 'org.springframework.boot' version '3.2.0'
    id 'io.spring.dependency-management' version '1.1.4'
}

group = 'org.lijma.hibernate.samples'
version = '0.0.1-SNAPSHOT'

java {
    sourceCompatibility = '17'
}

repositories {
    mavenCentral()
}

dependencies {
    // 使用原生Hibernate而非Spring Data JPA
    implementation 'org.hibernate.orm:hibernate-core:6.4.0.Final'
    implementation 'org.springframework.boot:spring-boot-starter-jdbc'
    implementation 'org.springframework.boot:spring-boot-starter-web'

    // 数据库驱动
    runtimeOnly 'org.postgresql:postgresql'

    // 开发辅助工具
    compileOnly 'org.projectlombok:lombok'
    annotationProcessor 'org.projectlombok:lombok'

    // 测试相关
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
    testImplementation 'org.junit.jupiter:junit-jupiter-api'
    testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine'
}

test {
    useJUnitPlatform()
}

这里我们使用了Hibernate核心库,而不是通过Spring Data JPA,以便更清晰地展示Hibernate本身的特性。

应用配置

application.yml文件包含Hibernate特有的配置参数:

spring:
  application:
    name: hibernate-sample

  datasource:
    url: jdbc:postgresql://localhost:5432/jimmer_demo
    username: postgres
    password: postgres
    driver-class-name: org.postgresql.Driver
    hikari:
      connection-timeout: 30000
      maximum-pool-size: 10
      minimum-idle: 5

# Hibernate原生配置,不使用JPA
hibernate:
  connection:
    url: jdbc:postgresql://localhost:5432/jimmer_demo
    username: postgres
    password: postgres
    driver_class: org.postgresql.Driver
  dialect: org.hibernate.dialect.PostgreSQLDialect
  show_sql: true
  format_sql: true
  hbm2ddl:
    auto: validate

server:
  port: 8083

logging:
  level:
    org.hibernate.SQL: DEBUG
    org.hibernate.type.descriptor.sql.BasicBinder: TRACE

除了基本的数据源配置外,这里还有Hibernate专有的设置: - dialect指定方言,让Hibernate能生成针对特定数据库的SQL - show_sqlformat_sql用于调试 - hbm2ddl.auto设为validate,表示Hibernate会验证数据库结构与实体映射是否一致

Hibernate配置类

与JDBC不同,Hibernate需要一个专门的配置类来设置会话工厂:

package org.lijma.hibernate.samples.config;

import org.hibernate.SessionFactory;
import org.hibernate.boot.registry.StandardServiceRegistryBuilder;
import org.hibernate.cfg.Configuration;
import org.hibernate.cfg.Environment;
import org.lijma.hibernate.samples.entity.Category;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;

import java.util.Properties;

@org.springframework.context.annotation.Configuration
public class HibernateConfig {

    @Value("${hibernate.connection.url}")
    private String url;

    @Value("${hibernate.connection.username}")
    private String username;

    @Value("${hibernate.connection.password}")
    private String password;

    @Value("${hibernate.connection.driver_class}")
    private String driverClass;

    @Value("${hibernate.dialect}")
    private String dialect;

    @Value("${hibernate.show_sql}")
    private String showSql;

    @Value("${hibernate.format_sql}")
    private String formatSql;

    @Value("${hibernate.hbm2ddl.auto}")
    private String hbm2ddlAuto;

    @Bean
    public SessionFactory sessionFactory() {
        Properties settings = new Properties();
        settings.put(Environment.DRIVER, driverClass);
        settings.put(Environment.URL, url);
        settings.put(Environment.USER, username);
        settings.put(Environment.PASS, password);
        settings.put(Environment.DIALECT, dialect);
        settings.put(Environment.SHOW_SQL, showSql);
        settings.put(Environment.FORMAT_SQL, formatSql);
        settings.put(Environment.HBM2DDL_AUTO, hbm2ddlAuto);
        settings.put(Environment.CURRENT_SESSION_CONTEXT_CLASS, "thread");

        Configuration configuration = new Configuration();
        configuration.setProperties(settings);

        // 添加实体类
        configuration.addAnnotatedClass(Category.class);

        return configuration.buildSessionFactory(
            new StandardServiceRegistryBuilder()
                .applySettings(configuration.getProperties())
                .build()
        );
    }
}

这个配置类负责设置Hibernate的SessionFactory,它是Hibernate应用的核心。通过代码配置,我们指定了连接参数、方言和实体类。

实体定义

Hibernate的实体类通过注解映射到数据库表:

package org.lijma.hibernate.samples.entity;

import lombok.Data;
import jakarta.persistence.*;
import java.time.LocalDateTime;

@Entity
@Table(name = "category")
@Data
public class Category {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(nullable = false)
    private String name;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "parent_id")
    private Category parent;

    @Column(name = "path")
    private String path;

    @Column(name = "created_time", nullable = false)
    private LocalDateTime createdTime;

    @Column(name = "modified_time", nullable = false)
    private LocalDateTime modifiedTime;
}

与JDBC的实体类相比,Hibernate实体包含了大量ORM映射注解: - @Entity@Table表明这是一个实体类,对应数据库中的category表 - @Id@GeneratedValue标识主键及其生成策略 - @Column指定列属性,如是否可为空、长度等 - @ManyToOne@JoinColumn定义了与父分类的多对一关系

特别注意parent属性:与JDBC中需要同时保存ID和对象不同,Hibernate中我们只需定义对象引用,框架会自动处理背后的外键关系。

数据访问层

Hibernate的数据访问层实现如下:

package org.lijma.hibernate.samples.repository;

import org.lijma.hibernate.samples.entity.Category;
import java.util.List;

public interface CategoryRepository {
    List<Category> findAll();
}
package org.lijma.hibernate.samples.repository;

import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.hibernate.query.Query;
import org.lijma.hibernate.samples.entity.Category;
import org.springframework.stereotype.Repository;

import java.util.List;

@Repository
public class CategoryRepositoryImpl implements CategoryRepository {

    private final SessionFactory sessionFactory;

    public CategoryRepositoryImpl(SessionFactory sessionFactory) {
        this.sessionFactory = sessionFactory;
    }

    @Override
    public List<Category> findAll() {
        try (Session session = sessionFactory.openSession()) {
            Query<Category> query = session.createQuery("FROM Category", Category.class);
            return query.list();
        }
    }
}

与JDBC相比,Hibernate的数据访问代码简洁很多: - 不需要编写SQL语句(使用HQL - Hibernate Query Language) - 不需要手动映射结果集 - 通过Session管理对象的持久化状态 - 可以直接使用面向对象的查询语言

Hibernate方式的特点

Hibernate方式的优势在于: 1. 减少样板代码:自动化的对象-关系映射减少了大量手动代码 2. 对象化思维:允许开发者以面向对象方式思考数据访问 3. 自动化关联管理:自动处理实体间的关联关系,包括懒加载

但同时也有一些挑战: 1. 配置复杂:相比JDBC,需要更多配置 2. 学习曲线陡峭:需要理解很多概念,如会话、持久化上下文等 3. 性能调优复杂:需要注意N+1查询等性能问题

3. JPA项目配置

JPA(Java Persistence API)是Java EE规范的一部分,它标准化了对象关系映射的API。虽然Hibernate是JPA的主要实现之一,但JPA提供了更标准化的接口,使应用程序可以在不同JPA实现之间切换。

JPA项目结构如下:

jimmer-in-action-samples/chapter2/jpa/
├── build.gradle              # 项目构建脚本
├── src/
│   ├── main/
│   │   ├── java/
│   │   │   └── org/lijma/jpa/samples/
│   │   │       ├── entity/            # 实体类
│   │   │       │   └── Category.java
│   │   │       ├── repository/        # 数据访问层
│   │   │       │   └── CategoryRepository.java
│   │   │       └── JpaApplication.java
│   │   └── resources/
│   │       └── application.yml        # 配置文件
│   └── test/
│       └── ...                        # 测试代码

构建配置

JPA项目的build.gradle使用Spring Data JPA:

plugins {
    id 'java'
    id 'org.springframework.boot' version '3.2.0'
    id 'io.spring.dependency-management' version '1.1.4'
}

group = 'org.lijma.jpa.samples'
version = '0.0.1-SNAPSHOT'

java {
    sourceCompatibility = '17'
}

repositories {
    mavenCentral()
}

dependencies {
    // Spring Data JPA
    implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
    implementation 'org.springframework.boot:spring-boot-starter-web'

    // 数据库驱动
    runtimeOnly 'org.postgresql:postgresql'

    // 开发辅助工具
    compileOnly 'org.projectlombok:lombok'
    annotationProcessor 'org.projectlombok:lombok'

    // 测试相关
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
    testImplementation 'org.junit.jupiter:junit-jupiter-api'
    testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine'
}

test {
    useJUnitPlatform()
}

Spring Data JPA在JPA基础上提供了更高层次的抽象,进一步简化了数据访问层的开发。

应用配置

application.yml文件包含JPA特有的配置:

spring:
  application:
    name: jpa-sample

  datasource:
    url: jdbc:postgresql://localhost:5432/jimmer_demo
    username: postgres
    password: postgres
    driver-class-name: org.postgresql.Driver
    hikari:
      connection-timeout: 30000
      maximum-pool-size: 10
      minimum-idle: 5

  jpa:
    database-platform: org.hibernate.dialect.PostgreSQLDialect
    hibernate:
      ddl-auto: validate
    properties:
      hibernate:
        format_sql: true
    show-sql: true
    open-in-view: false

server:
  port: 8085

logging:
  level:
    org.hibernate.SQL: DEBUG
    org.hibernate.type.descriptor.sql.BasicBinder: TRACE

这里的配置与Hibernate类似,但遵循Spring Boot对JPA的命名约定。特别注意open-in-view: false设置,这是一个避免常见性能问题的最佳实践。

实体定义

JPA实体类使用JPA标准注解:

package org.lijma.jpa.samples.entity;

import lombok.Data;
import jakarta.persistence.*;
import java.time.LocalDateTime;

@Entity
@Table(name = "category")
@Data
public class Category {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(nullable = false)
    private String name;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "parent_id")
    private Category parent;

    @Column(name = "path")
    private String path;

    @Column(name = "created_time", nullable = false)
    private LocalDateTime createdTime;

    @Column(name = "modified_time", nullable = false)
    private LocalDateTime modifiedTime;
}

JPA实体的注解与Hibernate几乎相同,这不足为奇,因为Hibernate是JPA规范的核心实现者之一。关键的区别在于,这些注解来自jakarta.persistence包,而不是Hibernate特有的包。

数据访问层

JPA项目的数据访问层利用Spring Data JPA的强大功能:

package org.lijma.jpa.samples.repository;

import org.lijma.jpa.samples.entity.Category;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

@Repository
public interface CategoryRepository extends JpaRepository<Category, Long> {
}

与之前的例子相比,这里最显著的变化是接口直接继承自JpaRepository,无需编写实现类。Spring Data JPA会自动为接口生成实现,包括标准的CRUD操作。只需要声明接口,即可获得完整的数据访问功能,包括: - findAll() - findById() - save() - delete() - 分页查询 - 自定义查询方法

JPA方式的特点

JPA方式的优势在于: 1. 标准API:使用Java EE标准API,避免厂商锁定 2. 简化的DAO层:通过Spring Data JPA,可以几乎不写实现代码 3. 丰富的查询能力:支持方法名派生查询、JPQL和原生SQL

和Hibernate类似,JPA也面临一些挑战: 1. 性能调优:需要注意懒加载和N+1查询问题 2. 配置复杂度:虽然有所简化,但仍需要不少配置 3. 灵活性限制:标准化带来了一些灵活性的损失

4. MyBatis项目配置

与Hibernate和JPA这样的全功能ORM框架不同,MyBatis采取了一种"SQL映射"的方法,让开发者保持对SQL的控制,同时提供简便的结果映射机制。这种方法尤其适合那些对SQL控制要求较高的项目。

MyBatis项目结构如下:

jimmer-in-action-samples/chapter2/mybatis/
├── build.gradle                # 项目构建脚本
├── src/
│   ├── main/
│   │   ├── java/
│   │   │   └── org/lijma/mybatis/samples/
│   │   │       ├── entity/            # 实体类
│   │   │       │   └── Category.java
│   │   │       ├── mapper/            # SQL映射层
│   │   │       │   └── CategoryMapper.java
│   │   │       └── MyBatisApplication.java
│   │   └── resources/
│   │       ├── application.yml        # 配置文件
│   │       └── mapper/                # XML映射文件
│   │           └── CategoryMapper.xml
│   └── test/
│       └── ...                        # 测试代码

构建配置

MyBatis项目的build.gradle配置如下:

plugins {
    id 'java'
    id 'org.springframework.boot' version '3.2.0'
    id 'io.spring.dependency-management' version '1.1.4'
}

group = 'org.lijma.mybatis.samples'
version = '0.0.1-SNAPSHOT'

java {
    sourceCompatibility = '17'
}

repositories {
    mavenCentral()
}

dependencies {
    // MyBatis 相关
    implementation 'org.mybatis.spring.boot:mybatis-spring-boot-starter:3.0.3'
    implementation 'org.springframework.boot:spring-boot-starter-web'

    // 数据库驱动
    runtimeOnly 'org.postgresql:postgresql'

    // 开发辅助工具
    compileOnly 'org.projectlombok:lombok'
    annotationProcessor 'org.projectlombok:lombok'

    // 测试相关
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
    testImplementation 'org.junit.jupiter:junit-jupiter-api'
    testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine'
}

test {
    useJUnitPlatform()
}

MyBatis Spring Boot Starter提供了良好的自动配置,简化了传统MyBatis项目中繁琐的XML配置。

应用配置

application.yml文件包含MyBatis特有的配置:

spring:
  application:
    name: mybatis-sample

  datasource:
    url: jdbc:postgresql://localhost:5432/jimmer_demo
    username: postgres
    password: postgres
    driver-class-name: org.postgresql.Driver
    hikari:
      connection-timeout: 30000
      maximum-pool-size: 10
      minimum-idle: 5

mybatis:
  mapper-locations: classpath:mapper/*.xml
  type-aliases-package: org.lijma.mybatis.samples.entity
  configuration:
    map-underscore-to-camel-case: true
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl

server:
  port: 8086

logging:
  level:
    org.lijma.mybatis.samples.mapper: DEBUG

MyBatis的配置主要关注两点: 1. mapper-locations: 指定XML映射文件的位置 2. type-aliases-package: 指定实体类所在的包,简化XML中的类型引用 3. map-underscore-to-camel-case: 启用下划线命名到驼峰命名的自动转换

实体定义

MyBatis的实体类与JDBC类似,是一个简单的POJO,没有任何特殊注解:

package org.lijma.mybatis.samples.entity;

import lombok.Data;
import java.time.LocalDateTime;

@Data
public class Category {
    private Long id;
    private String name;
    private Long parentId;
    private Category parent;
    private String path;
    private LocalDateTime createdTime;
    private LocalDateTime modifiedTime;
}

这种"纯净"的实体类是MyBatis的一个优点,实体类无需感知持久化框架的存在。

数据访问层

MyBatis项目的数据访问层分为Java接口和XML映射文件:

package org.lijma.mybatis.samples.mapper;

import org.apache.ibatis.annotations.Mapper;
import org.lijma.mybatis.samples.entity.Category;

import java.util.List;

@Mapper
public interface CategoryMapper {
    List<Category> findAll();
}
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="org.lijma.mybatis.samples.mapper.CategoryMapper">

    <resultMap id="categoryResultMap" type="org.lijma.mybatis.samples.entity.Category">
        <id property="id" column="id"/>
        <result property="name" column="name"/>
        <result property="parentId" column="parent_id"/>
        <result property="path" column="path"/>
        <result property="createdTime" column="created_time"/>
        <result property="modifiedTime" column="modified_time"/>
    </resultMap>

    <select id="findAll" resultMap="categoryResultMap">
        SELECT id, name, parent_id, path, created_time, modified_time
        FROM category
    </select>

</mapper>

MyBatis的数据访问层有两个关键部分: 1. 接口定义:使用@Mapper注解标记的接口,方法声明对应数据库操作 2. XML映射:包含SQL语句和对象映射规则的XML文件,与接口方法一一对应

这种分离的设计让SQL可以单独维护,同时享受接口的类型安全。

MyBatis方式的特点

MyBatis方式的优势在于: 1. SQL完全可控:开发者可以精确控制执行的每一条SQL语句 2. 性能可预期:性能表现与手写JDBC接近,但代码量显著减少 3. 灵活的映射:可以处理复杂的查询结果映射,包括一对多、多对多关系 4. 上手简单:概念少,学习曲线平缓

但同时也有一些挑战: 1. SQL维护成本:SQL语句分散在XML文件中,维护和重构有一定难度 2. 动态SQL复杂:复杂的条件查询需要编写较多的动态SQL标签 3. 代码生成依赖:对于大型项目,往往需要代码生成工具来减少重复劳动

5. Jimmer项目配置

最后,我们来到本书的主角——Jimmer。作为一个现代的ORM框架,Jimmer融合了不可变对象、编译时代码生成和类型安全查询等创新特性,为Java开发者提供了全新的数据访问体验。

Jimmer项目结构如下:

jimmer-in-action-samples/chapter2/jimmer/
├── build.gradle              # 项目构建脚本
├── src/
│   ├── main/
│   │   ├── java/
│   │   │   └── org/lijma/jimmer/samples/
│   │   │       ├── entity/            # 实体接口
│   │   │       │   └── Category.java
│   │   │       ├── repository/        # 数据访问层
│   │   │       │   ├── CategoryRepository.java
│   │   │       │   └── CategoryRepositoryImpl.java
│   │   │       └── JimmerApplication.java
│   │   └── resources/
│   │       └── application.yml        # 配置文件
│   └── test/
│       └── ...                        # 测试代码

构建配置

Jimmer项目的build.gradle配置如下:

plugins {
    id 'java'
    id 'org.springframework.boot' version '3.2.0'
    id 'io.spring.dependency-management' version '1.1.4'
}

group = 'org.lijma.jimmer.samples'
version = '0.0.1-SNAPSHOT'

java {
    sourceCompatibility = '17'
}

ext {
    jimmerVersion = '0.8.69'
}

configurations {
    compileOnly {
        extendsFrom annotationProcessor
    }
}

repositories {
    mavenCentral()
}

dependencies {
    // Jimmer依赖
    implementation "org.babyfish.jimmer:jimmer-spring-boot-starter:${jimmerVersion}"

    // Jimmer APT注解处理器
    annotationProcessor "org.babyfish.jimmer:jimmer-apt:${jimmerVersion}"

    // Spring Boot依赖
    implementation 'org.springframework.boot:spring-boot-starter-web'

    // 数据库依赖
    implementation 'org.postgresql:postgresql'

    // 测试依赖
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
    testAnnotationProcessor "org.babyfish.jimmer:jimmer-apt:${jimmerVersion}" // 为测试添加APT处理器
}

tasks.named('test') {
    useJUnitPlatform()
}

Jimmer的构建脚本比其他框架略复杂,主要是因为需要配置注解处理器(APT),这是Jimmer实现编译时代码生成的关键。注意jimmer-apt依赖被声明为注解处理器,它会在编译时生成额外的类。

应用配置

application.yml文件包含Jimmer特有的配置:

server:
  port: 8081

spring:
  application:
    name: jimmer-sample
  datasource:
    url: jdbc:postgresql://localhost:5432/jimmer_demo
    username: postgres
    password: postgres
    driver-class-name: org.postgresql.Driver

# Jimmer配置
jimmer:
  dialect: org.babyfish.jimmer.sql.dialect.PostgresDialect
  show-sql: true
  pretty-sql: true
  database-validation-mode: WARNING

logging:
  level:
    org.babyfish.jimmer: DEBUG

Jimmer的配置相对简单,主要是指定数据库方言和调试选项。

实体定义

Jimmer的实体定义与传统ORM框架有显著不同,它使用接口而非类:

package org.lijma.jimmer.samples.entity;

import org.babyfish.jimmer.sql.*;
import org.jetbrains.annotations.Nullable;
import java.time.LocalDateTime;

@Entity
public interface Category {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    long id();

    String name();

    @Nullable
    @ManyToOne
    @JoinColumn(name = "parent_id")
    Category parent();

    @Nullable
    String path();

    @Column(name = "created_time")
    LocalDateTime createdTime();

    @Column(name = "modified_time")
    LocalDateTime modifiedTime();
}

这种接口式定义是Jimmer的一个核心特性,与传统的ORM框架有本质区别: 1. 实体是接口而非类,方法名采用命名式(无get前缀) 2. 没有setter方法,体现了不可变对象的设计理念 3. 使用@Nullable明确标注可空属性,提高类型安全性

虽然注解看起来与JPA类似,但Jimmer实体接口没有setter方法,这反映了Jimmer的不可变对象设计哲学。在编译时,Jimmer会根据这些接口生成真正的实现类。

数据访问层

Jimmer项目的数据访问层实现如下:

package org.lijma.jimmer.samples.repository;

import org.lijma.jimmer.samples.entity.Category;
import java.util.List;

public interface CategoryRepository {
    List<Category> findAll();
}
package org.lijma.jimmer.samples.repository;

import org.babyfish.jimmer.sql.JSqlClient;
import org.lijma.jimmer.samples.entity.Category;
import org.lijma.jimmer.samples.entity.CategoryTable;
import org.springframework.stereotype.Repository;

import java.util.List;

@Repository
public class CategoryRepositoryImpl implements CategoryRepository {

    private final JSqlClient sqlClient;

    public CategoryRepositoryImpl(JSqlClient sqlClient) {
        this.sqlClient = sqlClient;
    }

    @Override
    public List<Category> findAll() {
        CategoryTable table = CategoryTable.$;
        return sqlClient
            .createQuery(table)
            .select(table)
            .execute();
    }
}

Jimmer的数据访问层使用了完全不同的API风格: 1. JSqlClient是核心API,用于创建查询和执行数据操作 2. CategoryTable.$是自动生成的表元数据,提供类型安全的查询条件 3. 查询API采用流式风格,既直观又类型安全

这种强类型的查询方式让开发者能在编译期就发现潜在问题,而不是运行时才出错。

Jimmer方式的特点

Jimmer方式的优势在于: 1. 不可变对象模型:实体对象一旦创建就不可修改,避免了状态管理的复杂性 2. 类型安全:编译时代码生成和强类型API,减少运行时错误 3. 高效的关联处理:自动解决N+1查询问题,智能优化查询 4. 动态对象图:可以按需获取不同深度和形状的对象图

但也有一些考虑点: 1. 学习曲线:新概念和API需要时间学习 2. 构建复杂度:需要配置注解处理器 3. 社区成熟度:相比其他成熟框架,社区和生态还在成长中

小结

通过这五种不同框架对同一个业务场景的实现,我们可以清晰地看到它们在设计理念和使用方式上的差异:

  1. JDBC:直接、底层、完全控制,但需要大量样板代码
  2. Hibernate:完全ORM、对象化思维、自动关联管理,但配置复杂
  3. JPA:标准API、简化DAO层、平衡控制与便利,但有一定性能挑战
  4. MyBatis:SQL可控、灵活映射、半自动ORM,但需要维护大量SQL
  5. Jimmer:不可变实体、类型安全、智能查询优化,但有学习成本

每种框架都有其适用场景,选择哪一种取决于你的项目需求和团队偏好。在接下来的章节中,我们将更深入地探索Jimmer的核心特性,这些特性让它在现代应用开发中脱颖而出。

完成这个基础项目配置后,你已经具备了使用不同ORM框架处理相同业务场景的能力。这种对比学习方法不仅帮助你理解各个框架的特性,还能让你在项目中做出更明智的技术选择。

2.2 实体建模对比

数据建模是使用任何ORM框架的第一步也是最核心的步骤,它直接决定了应用如何看待数据以及如何组织业务逻辑。在本节中,我们将深入对比五种不同持久化框架的实体建模方式,包括Jimmer、JPA、Hibernate、MyBatis和JDBC。为了便于对比,所有示例都基于同一个电商系统的Category(商品分类)实体。

2.2.1 数据库表结构设计

首先,我们来看一下底层数据库中商品分类表的结构设计:

-- 商品分类表
CREATE TABLE category (
    id BIGSERIAL PRIMARY KEY,
    name VARCHAR(100) NOT NULL,
    parent_id BIGINT,
    path VARCHAR(200),
    created_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
    modified_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
    CONSTRAINT fk_category_parent FOREIGN KEY (parent_id) REFERENCES category(id)
);

这个表包含以下字段: - id: 主键标识 - name: 分类名称 - parent_id: 父分类ID(自引用关系) - path: 分类路径(用于快速查找层级关系) - created_time: 创建时间 - modified_time: 最后修改时间

特别值得注意的是parent_id字段,它构成了一个自引用关系,使得Category实体可以形成树状结构,这是电商系统中常见的分类设计模式。

2.2.2 Jimmer的接口式实体与不可变设计

Jimmer采用了与传统ORM框架完全不同的实体定义方式——接口而非类。这种设计是Jimmer不可变对象模型的基础,也是它区别于其他框架的关键特征。

package org.lijma.jimmer.samples.entity;

import org.babyfish.jimmer.sql.*;
import org.jetbrains.annotations.Nullable;
import java.time.LocalDateTime;

@Entity
public interface Category {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    long id();

    String name();

    @Nullable
    @ManyToOne
    @JoinColumn(name = "parent_id")
    Category parent();

    @Nullable
    String path();

    @Column(name = "created_time")
    LocalDateTime createdTime();

    @Column(name = "modified_time")
    LocalDateTime modifiedTime();
}

Jimmer实体的底层设计原理:

  1. 接口-实现分离模式:Jimmer采用了接口定义、编译时生成实现的架构,这种设计模式与Java的SPI(Service Provider Interface)机制类似,实现了关注点分离。开发者只需关注"做什么"(What),而框架负责"怎么做"(How)。

例子:这就像是我们在餐厅点菜一样,你只需要告诉服务员"我要一份宫保鸡丁"(接口定义),而不需要关心厨师如何切菜、炒菜的具体步骤(实现细节)。Jimmer的接口定义就是这样的"菜单",而编译器则是"厨师",自动为你准备好符合要求的实现类。

  1. 不可变对象的实现机制
  2. Jimmer在编译时生成的实现类包含所有属性的final字段
  3. 使用内部Builder进行对象构造
  4. 每次修改都会创建新对象,不会改变原对象
  5. 所有实现类都覆盖了equals()和hashCode(),基于所有属性值计算

例子:假设你要修改一个分类名称从"电子产品"到"数码产品":

// 原始对象
Category electronics = findCategoryById(1L);  // 电子产品

// 在传统ORM中的修改方式
electronics.setName("数码产品");  // 直接修改原对象

// 在Jimmer中的修改方式
Category digital = electronics.withName("数码产品"); 
// electronics对象保持不变,返回新的Category对象
System.out.println(electronics.name()); // 仍然输出 "电子产品"
System.out.println(digital.name());     // 输出 "数码产品"
这就像你在Word文档上保存副本一样,原文档保持不变,修改只会影响新创建的副本。

  1. 类型安全的自引用处理
  2. 传统ORM中的递归引用容易导致JSON序列化问题和无限循环
  3. Jimmer通过动态视图(动态DTO)和智能遍历机制,优雅地处理了这个问题
  4. 编译期类型检查确保所有引用都是类型安全的

  5. 运行时元模型

  6. Jimmer为每个实体生成静态元模型类(如Categories
  7. 元模型类提供了类型安全的属性访问器,用于构建查询
  8. 这些元模型对象包含实体所有属性的信息,实现真正的"代码即文档"

  9. 内存效率优化

  10. 使用自定义的内部位图(bitmap)跟踪已修改字段
  11. 智能差异检测,只序列化修改的字段
  12. 支持稀疏对象,无需所有属性都有值

代码生成与使用模式:

当编译上述代码时,Jimmer会生成多个重要的类:

  1. CategoryImpl: 实际的不可变实现类,包含:
  2. 所有属性的final字段
  3. 递归的equals()和hashCode()实现
  4. 高效的toString()方法
  5. 链式的修改器方法(返回新对象)

  6. CategoryDraft: 可变的"草稿"类,用于复杂对象创建场景

  7. 提供setter方法用于构建对象
  8. 可转换为不可变的CategoryImpl

  9. Categories: 静态元模型类,用于类型安全的查询

// Jimmer的使用模式
// 创建新对象(不可变)
Category category = Objects.createCategory(draft -> {
    draft.setName("电子产品");
    draft.setPath("/electronics");
});

// 修改对象(返回新实例)
Category updatedCategory = Objects.createCategory(draft -> {
    draft.setId(category.id());
    draft.setName("电子产品更新");
    draft.setPath(category.path());
    // 其他属性会自动复制
});

// 类型安全的查询
Categories categories = Categories.CATEGORY;
List<Category> electronics = sqlClient
    .createQuery(categories)
    .where(categories.name().eq("电子产品"))
    .select(categories)
    .execute();

通过这种设计,Jimmer在编译期就能检测大量潜在错误,如:

  • 属性名拼写错误
  • 类型不匹配
  • 访问不存在的属性
  • 尝试修改不可变对象

2.2.3 JPA/Hibernate的注解式实体与代理增强

JPA和Hibernate采用了注解驱动的类定义方式,这是Java生态中最广泛使用的ORM实体定义模式。其核心是运行时增强和代理对象。

package org.lijma.jpa.samples.entity;

import jakarta.persistence.*;
import lombok.Data;
import java.time.LocalDateTime;

@Entity
@Table(name = "category")
@Data
public class Category {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(nullable = false)
    private String name;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "parent_id")
    private Category parent;

    @Column(name = "path")
    private String path;

    @Column(name = "created_time", nullable = false)
    private LocalDateTime createdTime;

    @Column(name = "modified_time", nullable = false)
    private LocalDateTime modifiedTime;
}

JPA/Hibernate实体的底层设计原理:

  1. 代理对象与延迟加载
  2. Hibernate使用字节码增强(ByteBuddy或Javassist)创建实体类的子类作为代理
  3. 代理对象拦截属性访问,实现懒加载
  4. 对于@ManyToOne(fetch = FetchType.LAZY),Hibernate不会立即加载parent对象,而是创建代理
  5. 只有首次访问parent的属性时才会触发SQL查询

例子:这就像是你在看电视节目,节目信息中显示"更多细节请按遥控器上的INFO键"。你只有在真正需要这些额外信息时才会按下INFO键(触发加载)。同样,当你获取一个Category对象时,它的parent关系初始是一个看起来像Category但实际是代理的对象:

Category category = entityManager.find(Category.class, 1L);
System.out.println(category.getClass().getName());
// 输出: org.lijma.jpa.samples.entity.Category$HibernateProxy$a7bc23f1

// 直到这一刻才会触发SQL查询加载parent的实际数据
String parentName = category.getParent().getName();

  1. 脏检查机制
  2. JPA/Hibernate实体在加载后会被持久化上下文(PersistenceContext)跟踪
  3. 任何属性修改都会被记录
  4. 事务提交时,通过比较当前状态和快照来检测变更
  5. 这种机制无需显式调用save/update方法

  6. 缓存与关联管理

  7. 一级缓存(Session级别)自动管理,确保同一个会话中实体唯一性
  8. 二级缓存(可选)实现跨会话的缓存
  9. 级联操作通过cascade属性配置,可实现关联对象的自动保存、更新或删除

  10. 反射与内省机制

  11. Hibernate大量使用Java反射机制
  12. 在启动时扫描所有实体类的注解
  13. 构建元数据模型用于运行时映射
  14. 这种动态特性带来了灵活性,但也增加了运行时开销

  15. JDBC与SQL生成

  16. 实体状态变更自动转换为SQL语句
  17. SQL方言(Dialect)确保跨数据库兼容性
  18. 批量操作优化减少数据库往返

使用模式与问题:

// JPA/Hibernate的使用模式
// 创建与保存
Category category = new Category();
category.setName("电子产品");
category.setPath("/electronics");
category.setCreatedTime(LocalDateTime.now());
category.setModifiedTime(LocalDateTime.now());
entityManager.persist(category);

// 延迟加载与N+1问题
Category electronics = entityManager.find(Category.class, 1L);
// 如果未使用join fetch,下面的代码会触发新的SQL查询(N+1问题)
Category parent = electronics.getParent(); 

// 脏检查自动更新
electronics.setName("电子设备");  // 无需显式调用update方法
// 事务提交时自动保存更改

JPA/Hibernate的这种设计存在几个固有问题:

  1. N+1查询问题:不当使用懒加载容易导致大量额外查询
  2. 代理对象限制:实体类需要有无参构造器、非final类和属性
  3. 序列化问题:懒加载属性在脱离持久化上下文后可能无法访问
  4. 双向关联维护负担:需手动维护双向关联的两端

以N+1查询问题为例: 假设我们要获取所有分类及其父分类名称:

List<Category> categories = entityManager
    .createQuery("FROM Category", Category.class)
    .getResultList();

// 看似简单的代码,但每次循环都可能触发新的SQL查询
for (Category category : categories) {
    if (category.getParent() != null) {
        System.out.println(
            category.getName() + " 的父分类是: " + 
            category.getParent().getName()  // 这里可能触发额外查询!
        );
    }
}

如果有100个分类,且大部分有父分类,这段代码可能会执行1次查询获取所有分类,然后再执行近100次查询获取各自的父分类,这就是典型的N+1查询问题。解决方法是使用JOIN FETCH:

List<Category> categories = entityManager
    .createQuery("FROM Category c LEFT JOIN FETCH c.parent", Category.class)
    .getResultList();

2.2.4 MyBatis的轻量级实体与SQL映射

与完全的ORM框架不同,MyBatis采用了更为轻量的"对象-SQL映射"方式,实体类通常更加简单,映射信息集中在XML文件中。

package org.lijma.mybatis.samples.entity;

import lombok.Data;
import java.time.LocalDateTime;

@Data
public class Category {
    private Long id;
    private String name;
    private Long parentId;
    private Category parent;
    private String path;
    private LocalDateTime createdTime;
    private LocalDateTime modifiedTime;
}

MyBatis实体与映射的底层设计原理:

  1. SQL中心设计
  2. 实体类只是数据容器,不包含ORM元数据
  3. 所有映射逻辑和SQL位于XML映射文件中
  4. 这种分离使SQL优化与重构更容易

例子:这类似于建筑设计中的"结构与装饰分离"原则。实体类就像建筑的骨架结构,而XML映射则是可以灵活变化的装饰风格。比如同一个Category实体可以有多种查询方式:

<!-- 基本查询 -->
<select id="findByName" resultMap="categoryMap">
  SELECT * FROM category WHERE name = #{name}
</select>

<!-- 性能优化查询 -->
<select id="findByNameOptimized" resultMap="categoryMap">
  SELECT id, name FROM category WHERE name = #{name}
</select>

<!-- 关联查询 -->
<select id="findWithProducts" resultMap="categoryWithProductsMap">
  SELECT c.*, p.* 
  FROM category c
  LEFT JOIN product p ON p.category_id = c.id
  WHERE c.name = #{name}
</select>
而实体类保持不变。

  1. 半自动映射机制
  2. 结果映射(ResultMap)定义了SQL结果集与对象的映射关系
  3. 参数映射定义了Java对象如何映射到SQL参数
  4. 开发者完全控制SQL和映射过程

  5. 动态SQL能力

  6. XML中的条件标签(如<if><choose>)实现动态SQL构建
  7. 这种模板化方法既保留了SQL的可读性,又提供了动态生成能力
  8. 不依赖编程语言的类型系统,更接近原生SQL

  9. 缓存设计

  10. 简单的两级缓存系统
  11. 一级缓存基于SqlSession(默认启用)
  12. 二级缓存基于命名空间(需手动配置)
  13. 缓存粒度较粗,可能导致缓存失效问题

  14. 对象加载策略

  15. 懒加载通过额外的查询实现,而非代理对象
  16. 关联查询依赖嵌套查询(<association>)或联表查询

典型的MyBatis映射文件(XML):

<mapper namespace="org.lijma.mybatis.samples.repository.CategoryMapper">
  <resultMap id="categoryMap" type="org.lijma.mybatis.samples.entity.Category">
    <id property="id" column="id"/>
    <result property="name" column="name"/>
    <result property="parentId" column="parent_id"/>
    <result property="path" column="path"/>
    <result property="createdTime" column="created_time"/>
    <result property="modifiedTime" column="modified_time"/>
    <association property="parent" column="parent_id" 
                 select="selectCategoryById"/>
  </resultMap>

  <select id="selectCategoryById" resultMap="categoryMap">
    SELECT id, name, parent_id, path, created_time, modified_time
    FROM category
    WHERE id = #{id}
  </select>

  <select id="selectCategoryByName" resultMap="categoryMap">
    SELECT id, name, parent_id, path, created_time, modified_time
    FROM category
    WHERE name LIKE CONCAT('%', #{name}, '%')
  </select>

  <insert id="insertCategory" useGeneratedKeys="true" keyProperty="id">
    INSERT INTO category (name, parent_id, path, created_time, modified_time)
    VALUES (#{name}, #{parentId}, #{path}, #{createdTime}, #{modifiedTime})
  </insert>
</mapper>

使用模式:

// MyBatis的使用模式
// Mapper接口
public interface CategoryMapper {
    Category selectCategoryById(Long id);
    List<Category> selectCategoryByName(String name);
    int insertCategory(Category category);
}

// 使用Mapper
Category category = new Category();
category.setName("电子产品");
category.setPath("/electronics");
category.setCreatedTime(LocalDateTime.now());
category.setModifiedTime(LocalDateTime.now());
categoryMapper.insertCategory(category);

// 查询
Category electronics = categoryMapper.selectCategoryById(1L);

MyBatis实体与SQL映射的设计带来了几个明显的优缺点:

优势: 1. SQL完全可控,便于性能优化 2. 缓存粒度可细化到语句级别 3. 更容易与遗留系统集成 4. 学习曲线平缓,接近原生JDBC

劣势: 1. 大量SQL需要手动管理 2. XML与Java的割裂降低了重构和类型安全 3. 映射关系不直观,需要同时理解Java和XML 4. 缺乏高级ORM特性如脏检查和自动关联管理

2.2.5 JDBC的手动映射与底层控制

在最基础的JDBC应用中,实体类通常是最简单的数据容器,所有映射逻辑完全由开发者手动控制。

package org.lijma.jdbc.samples.entity;

import lombok.Data;
import java.time.LocalDateTime;

@Data
public class Category {
    private Long id;
    private String name;
    private Long parentId;
    private Category parent;
    private String path;
    private LocalDateTime createdTime;
    private LocalDateTime modifiedTime;
}

JDBC的底层设计与映射原理:

  1. 全手动映射
  2. JDBC不提供任何自动映射
  3. 开发者需要手动从ResultSet获取数据并设置到对象
  4. 所有转换逻辑都需要显式编码

例子:这就像是手工组装一台电脑,每个组件都需要你亲自选择、连接和配置。以下是从ResultSet到Category对象的手动映射:

Category category = new Category();
category.setId(rs.getLong("id"));
category.setName(rs.getString("name"));

// 类型转换也需要手动处理
Timestamp createdTs = rs.getTimestamp("created_time");
if (createdTs != null) {
    category.setCreatedTime(createdTs.toLocalDateTime());
}

// 处理NULL值也要格外小心
Long parentId = rs.getLong("parent_id");
if (!rs.wasNull()) {  // 必须检查是否为NULL
    category.setParentId(parentId);
}

这种"一砖一瓦"的手工建造方式既繁琐又容易出错,但给予了开发者最大的控制权。

  1. 底层连接管理
  2. 显式的连接获取、释放和事务控制
  3. 需要精心管理资源以避免泄漏
  4. 手动设置和重置所有数据库参数

  5. 批处理与性能

  6. 手动创建和管理PreparedStatement
  7. 显式批量操作优化
  8. 手动调优每个SQL语句

  9. 纯SQL控制

  10. 没有任何中间层抽象
  11. SQL错误在运行时而非编译时检测
  12. 数据库方言差异需要手动处理

典型的JDBC代码示例:

// JDBC实体加载示例
public Category findById(Long id) {
    String sql = "SELECT id, name, parent_id, path, created_time, modified_time FROM category WHERE id = ?";
    try (Connection conn = dataSource.getConnection();
         PreparedStatement ps = conn.prepareStatement(sql)) {
        ps.setLong(1, id);
        try (ResultSet rs = ps.executeQuery()) {
            if (rs.next()) {
                Category category = new Category();
                category.setId(rs.getLong("id"));
                category.setName(rs.getString("name"));
                category.setParentId(rs.getLong("parent_id"));
                category.setPath(rs.getString("path"));
                category.setCreatedTime(rs.getTimestamp("created_time").toLocalDateTime());
                category.setModifiedTime(rs.getTimestamp("modified_time").toLocalDateTime());

                // 如果需要加载父类别,必须手动执行另一次查询
                if (category.getParentId() != null) {
                    category.setParent(findById(category.getParentId()));
                }

                return category;
            }
        }
    } catch (SQLException e) {
        throw new RuntimeException("Failed to find category by id: " + id, e);
    }
    return null;
}

// JDBC保存示例
public void save(Category category) {
    String sql = "INSERT INTO category (name, parent_id, path, created_time, modified_time) VALUES (?, ?, ?, ?, ?)";
    try (Connection conn = dataSource.getConnection();
         PreparedStatement ps = conn.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS)) {
        ps.setString(1, category.getName());
        if (category.getParentId() != null) {
            ps.setLong(2, category.getParentId());
        } else {
            ps.setNull(2, Types.BIGINT);
        }
        ps.setString(3, category.getPath());
        ps.setTimestamp(4, Timestamp.valueOf(category.getCreatedTime()));
        ps.setTimestamp(5, Timestamp.valueOf(category.getModifiedTime()));

        int rows = ps.executeUpdate();
        if (rows > 0) {
            try (ResultSet rs = ps.getGeneratedKeys()) {
                if (rs.next()) {
                    category.setId(rs.getLong(1));
                }
            }
        }
    } catch (SQLException e) {
        throw new RuntimeException("Failed to save category: " + category, e);
    }
}

JDBC的完全手动控制既是其优势也是劣势:

优势: 1. 最高性能潜力,无框架额外开销 2. 完全控制SQL和数据库交互 3. 无黑魔法,所有行为都是明确的 4. 无外部依赖,稳定性最高

劣势: 1. 极高的代码量和重复劳动 2. 错误处理复杂,异常传播管理困难 3. 事务和资源管理容易出错 4. 无法享受ORM带来的高级特性

2.2.6 设计哲学与实现机制的横向对比

现在我们已经深入了解了五种不同框架的实体定义和底层设计,让我们从几个关键维度进行横向对比:

1. 设计哲学对比

框架 设计哲学 核心理念 技术范式
Jimmer 声明式+不可变 接口定义实体,实现由框架生成 函数式编程+响应式
JPA/Hibernate 对象关系映射 对象世界与关系世界的桥梁 面向对象+声明式
MyBatis SQL中心化 SQL是核心,对象是辅助 半自动映射+SQL模板
JDBC 底层控制 完全控制数据库交互 过程式编程+命令式

设计哲学对比中的实际应用例子:

// 假设我们要实现"查找某个名称的分类,并获取其所有父分类"的功能

// Jimmer的函数式风格 - 类型安全且声明式
Categories c = Categories.CATEGORY;
List<Category> results = sqlClient
    .createQuery(c)
    .where(c.name().eq("手机"))
    .select(c.fetch(
        FetchPaths.builder()
            .add(c.parent())
            .add(c.parent().parent())
            .build())
    )
    .execute();

// JPA/Hibernate的面向对象风格 - 依赖代理和路径表达式
TypedQuery<Category> query = entityManager.createQuery(
    "SELECT c FROM Category c " +
    "LEFT JOIN FETCH c.parent p1 " +
    "LEFT JOIN FETCH p1.parent p2 " +
    "WHERE c.name = :name", Category.class);
query.setParameter("name", "手机");
List<Category> results = query.getResultList();

// MyBatis的SQL模板风格 - 依赖XML映射
// 在XML中:
// <select id="findWithParents" resultMap="categoryWithParentsMap">
//   SELECT c1.*, c2.*, c3.*
//   FROM category c1
//   LEFT JOIN category c2 ON c1.parent_id = c2.id
//   LEFT JOIN category c3 ON c2.parent_id = c3.id
//   WHERE c1.name = #{name}
// </select>
List<Category> results = categoryMapper.findWithParents("手机");

// JDBC的过程式风格 - 完全控制但冗长
List<Category> results = new ArrayList<>();
try (Connection conn = dataSource.getConnection();
     PreparedStatement ps = conn.prepareStatement(
         "SELECT c1.*, c2.*, c3.* " +
         "FROM category c1 " +
         "LEFT JOIN category c2 ON c1.parent_id = c2.id " +
         "LEFT JOIN category c3 ON c2.parent_id = c3.id " +
         "WHERE c1.name = ?")) {
    ps.setString(1, "手机");
    try (ResultSet rs = ps.executeQuery()) {
        while(rs.next()) {
            // 数十行的手动映射代码...
        }
    }
}

这个例子清晰地展示了不同框架在解决同一问题时的设计思路和代码风格差异。

2. 实现机制对比

机制 Jimmer JPA/Hibernate MyBatis JDBC
对象创建 编译时生成实现 反射+代理 反射 直接构造
状态追踪 不可变+比较 脏检查
懒加载 动态视图 代理对象 嵌套查询 手动实现
关联导航 编译时检查 运行时解析 XML配置 手动编码
缓存策略 多级缓存 一级+二级缓存 语句+命名空间缓存 无内置缓存

实际例子:递归树形结构的加载与处理

假设我们需要完整加载一个类别及其所有子类别的树状结构:

// Jimmer - 通过动态视图控制加载深度
FetcherOwner<Category> categoryBuilder = 
    sqlClient.createFetcher(Category.class);

categoryBuilder.addRecursion("children", (args) -> {
    args.path("id", "name", "path");  // 指定需要的属性
    args.depth(5);  // 控制递归深度
});

Category root = sqlClient.findById(categoryBuilder.build(), 1L);
System.out.println("子类别数量: " + root.childCategories().size());

// JPA/Hibernate - 通过EntityGraph或JPQL控制加载
EntityGraph<Category> graph = entityManager.createEntityGraph(Category.class);
graph.addSubgraph("childCategories")
     .addSubgraph("childCategories");  // 只能硬编码递归层级

Map<String, Object> hints = new HashMap<>();
hints.put("jakarta.persistence.loadgraph", graph);
Category root = entityManager.find(Category.class, 1L, hints);

// MyBatis - 通过XML配置嵌套查询
// 在XML中定义递归查询:
// <resultMap id="categoryTreeMap" type="Category">
//   <collection property="childCategories" select="findChildCategories" column="id" />
// </resultMap>
// <select id="findChildCategories" resultMap="categoryTreeMap">
//   SELECT * FROM category WHERE parent_id = #{id}
// </select>
Category root = categoryMapper.findCategoryTreeById(1L);

// JDBC - 手动实现递归加载
Category root = new Category();
// ... 加载根分类基本信息 ...
root.setChildCategories(findChildCategories(root.getId(), 5)); // 递归函数

private List<Category> findChildCategories(Long parentId, int depth) {
    if (depth <= 0) return Collections.emptyList();
    // ... JDBC代码查询子分类 ...
    // 递归处理每个子分类
    for (Category child : children) {
        child.setChildCategories(findChildCategories(child.getId(), depth - 1));
    }
    return children;
}

3. 性能特性对比

性能特性 Jimmer JPA/Hibernate MyBatis JDBC
启动性能 编译期负担 扫描+反射开销大 XML解析中等 最小开销
内存占用 不可变对象开销 代理+缓存开销大 中等 最小
查询性能 自动优化高 中高(HQL优化) 手动优化 完全手动
批量操作 自动优化 需配置 需手动控制 完全手动
首次加载 类转换开销 中等 最快

批量插入性能对比例子:

假设我们需要批量导入1000个新商品类别:

// 准备1000个类别对象
List<Category> categories = new ArrayList<>(1000);
for (int i = 0; i < 1000; i++) {
    // ... 构造对象 ...
}

// Jimmer - 自动批处理
sqlClient.getEntities().batchSave(categories);

// JPA/Hibernate - 需要配置批处理
// 配置:hibernate.jdbc.batch_size=50
int batchSize = 50;
for (int i = 0; i < categories.size(); i++) {
    entityManager.persist(categories.get(i));
    if (i % batchSize == 0) {
        entityManager.flush();
        entityManager.clear();
    }
}

// MyBatis - 有两种方式:逐个插入或自定义批处理
// 方式1:逐个插入(性能较差)
for (Category category : categories) {
    categoryMapper.insert(category);
}

// 方式2:自定义批处理SQL
// XML中:<insert id="batchInsert">
//   INSERT INTO category (name, parent_id, path, created_time, modified_time)
//   VALUES 
//   <foreach collection="list" item="item" separator=",">
//     (#{item.name}, #{item.parentId}, #{item.path}, #{item.createdTime}, #{item.modifiedTime})
//   </foreach>
// </insert>
categoryMapper.batchInsert(categories);

// JDBC - 手动批处理控制
try (Connection conn = dataSource.getConnection()) {
    conn.setAutoCommit(false);
    try (PreparedStatement ps = conn.prepareStatement(
            "INSERT INTO category (name, parent_id, path, created_time, modified_time) " +
            "VALUES (?, ?, ?, ?, ?)")) {
        for (Category category : categories) {
            ps.setString(1, category.getName());
            // ... 设置其他参数 ...
            ps.addBatch();
        }
        ps.executeBatch();
    }
    conn.commit();
}

这个例子显示了各框架在处理大量数据时的性能优化方式。Jimmer提供了最简洁的API同时实现了高性能,Hibernate需要正确配置和使用,MyBatis需要手动编写批处理SQL,而JDBC提供了最大控制权但需要最多代码。

4. 开发体验对比

开发特性 Jimmer JPA/Hibernate MyBatis JDBC
类型安全 编译期检查 部分运行时检查 字符串SQL低 无类型检查
调试难度 低(可预测) 中(状态跟踪) 中(XML解析) 高(完全手动)
学习曲线 陡峭 中等 平缓 最简单但冗长
重构支持 最佳(编译检查) 中等(注解) 弱(字符串) 最弱
IDE支持 强(类型系统) 强(成熟生态) 中(插件) 基础

重构场景对比例子:

假设我们需要将Category实体的path属性重命名为treePath

// Jimmer - 编译时检查确保所有引用都被更新
// 修改实体定义
@Entity
public interface Category {
    // ... 其他属性 ...

    // 原来: String path();
    @Nullable
    String treePath();  // 修改后
}

// IDE会自动检测所有使用到path()的地方并报错,编译器也会报错
// 所有查询如:
sqlClient.createQuery(Category.class)
    .where(Categories.PATH.eq("/electronics"))  // 这里会编译错误
    .select(...)
    .execute();
// 必须改为:
sqlClient.createQuery(Category.class)
    .where(Categories.TREE_PATH.eq("/electronics"))  // 正确引用
    .select(...)
    .execute();

// JPA/Hibernate - 注解可以指定列名,但JPQL中的属性名必须手动修改
@Entity
@Table(name = "category")
public class Category {
    // 原来: 
    // @Column(name = "path")
    // private String path;

    @Column(name = "path")  // 列名可以不变
    private String treePath;  // 属性名改变
}

// 所有JPQL查询必须手动更新:
// 原来: "SELECT c FROM Category c WHERE c.path = :path"
// 必须改为:
"SELECT c FROM Category c WHERE c.treePath = :path"
// 如果忘记修改,将在运行时抛出异常

// MyBatis - XML映射需要手动更新,没有自动检查
// 实体类中修改:
@Data
public class Category {
    // private String path;
    private String treePath;
}

// XML文件中必须手动更新所有SQL:
// <select id="findByPath">
//   SELECT * FROM category WHERE path = #{path}  <!-- 错误!属性已重命名 -->
// </select>
// 必须改为:
// <select id="findByPath">
//   SELECT * FROM category WHERE path = #{treePath}  <!-- 正确引用 -->
// </select>
// 如果忘记修改,将在运行时出现值为null的问题,难以调试

// JDBC - 完全手动映射,需要修改所有结果集处理代码
// 原来:
// category.setPath(rs.getString("path"));
// 必须改为:
category.setTreePath(rs.getString("path"));
// 没有任何自动检查,容易遗漏

这个例子说明了Jimmer在重构支持方面的优势,通过编译时检查确保所有引用都被正确更新,减少了运行时错误的风险。

5. 关键设计决策与权衡

框架 关键设计决策 获得的优势 付出的代价
Jimmer 接口+不可变对象 类型安全+可预测性 学习曲线+概念复杂度
JPA/Hibernate 注解+运行时增强 开发效率+对象范式 性能开销+黑盒行为
MyBatis SQL中心+XML映射 SQL控制+性能 类型安全低+冗余工作
JDBC 完全手动控制 最高控制度+透明性 开发效率低+代码量大

实际项目中的权衡决策例子:

// 场景:电商系统中查询热门商品,并返回其分类信息
// 需求:高性能、类型安全、可维护性

// Jimmer解决方案:类型安全的查询 + 自定义DTO
// 定义一个定制返回类型
@Dto
public interface ProductWithCategoryDto {
    String productName();
    BigDecimal price();
    @ManyToOne
    Category category();
}

// 查询代码
List<ProductWithCategoryDto> hotProducts = sqlClient
    .createQuery(Products.class)
    .where(Products.IS_PUBLISHED.eq(true))
    .orderBy(Products.SALES.desc())
    .limit(10)
    .select(ProductWithCategoryDto.class)
    .execute();

// JPA/Hibernate解决方案:JPQL + 结果转换
List<Object[]> results = entityManager.createQuery(
    "SELECT p.name, p.price, c FROM Product p " +
    "JOIN p.category c " +
    "WHERE p.isPublished = true " +
    "ORDER BY p.sales DESC")
    .setMaxResults(10)
    .getResultList();

List<ProductWithCategoryDto> hotProducts = results.stream()
    .map(row -> new ProductWithCategoryDtoImpl(
        (String)row[0], 
        (BigDecimal)row[1], 
        (Category)row[2]))
    .collect(Collectors.toList());

// MyBatis解决方案:自定义SQL + 映射
// XML中:
// <select id="findHotProducts" resultMap="productWithCategoryMap">
//   SELECT p.name, p.price, c.* 
//   FROM product p
//   JOIN category c ON p.category_id = c.id
//   WHERE p.is_published = true
//   ORDER BY p.sales DESC
//   LIMIT 10
// </select>
List<ProductWithCategoryDto> hotProducts = productMapper.findHotProducts();

// JDBC解决方案:直接SQL + 手动映射
List<ProductWithCategoryDto> hotProducts = new ArrayList<>();
try (Connection conn = dataSource.getConnection();
     PreparedStatement ps = conn.prepareStatement(
         "SELECT p.name, p.price, c.* " +
         "FROM product p " +
         "JOIN category c ON p.category_id = c.id " +
         "WHERE p.is_published = true " +
         "ORDER BY p.sales DESC " +
         "LIMIT 10")) {
    try (ResultSet rs = ps.executeQuery()) {
        while (rs.next()) {
            // 手动映射结果到DTO
            ProductWithCategoryDto dto = new ProductWithCategoryDtoImpl();
            dto.setProductName(rs.getString("name"));
            dto.setPrice(rs.getBigDecimal("price"));

            Category category = new Category();
            // ... 设置分类属性 ...
            dto.setCategory(category);

            hotProducts.add(dto);
        }
    }
}

这个例子展示了各框架如何处理实际业务需求,以及如何在性能、类型安全和开发效率之间进行权衡。Jimmer通过类型安全的查询和自动映射提供了最优雅的解决方案,而其他框架则各有优缺点。

实体建模选择总结:

通过对Jimmer、JPA/Hibernate、MyBatis和JDBC四种框架实体建模方式的深入对比,我们可以得出以下关于实体设计的关键结论:

  1. 不可变 vs 可变实体
  2. Jimmer的不可变实体设计提供了数据一致性保证和并发安全性,但有一定的学习曲线
  3. 传统ORM的可变实体更符合直觉,但在复杂场景下可能导致难以排查的数据异常
  4. 实体的可变性应该基于业务领域特性和并发需求来选择

  5. 编译时安全 vs 运行时验证

  6. Jimmer和其他编译时检查工具可以在开发阶段发现大量潜在问题
  7. 运行时验证虽然灵活,但可能导致线上异常
  8. 随着应用复杂度增加,编译时安全的价值越发显著

  9. 关系表达方式

  10. 声明式关系(Jimmer/JPA)更接近领域模型思维,简化了复杂关系的表达
  11. 显式关系(MyBatis/JDBC)提供了更精细的控制,但增加了维护负担
  12. 自动关联加载与手动加载的选择应基于性能要求和业务复杂度

  13. 实体映射策略

  14. 元数据驱动映射(注解/接口)有利于统一处理和代码生成
  15. 手动映射提供灵活性,但代码量大且易出错
  16. 映射策略应考虑团队经验、项目规模和长期维护成本

在实际项目中,实体建模的选择不仅关系到代码的简洁性和可维护性,更直接影响到应用的性能特性和扩展能力。最佳实践是根据具体业务场景选择合适的实体设计方式,有时甚至在同一个系统中针对不同模块采用不同的实体建模策略。

接下来,我们将在第2.3节中探讨如何基于这些不同的实体模型实现基础的数据操作功能。实体建模只是起点,真正体现各框架特性的是对这些实体的增删改查操作以及复杂查询的支持能力。我们将通过具体示例展示如何使用这些框架实现相同的数据访问需求,并对比它们在API设计、代码简洁性和性能特性方面的差异。

2.3 基础数据操作

在上一节中,我们详细对比了不同框架的实体建模方式。本节将深入探讨如何使用这些不同的框架实现基础的数据操作(CRUD),并通过实际代码示例和性能对比,帮助你理解各框架在实际应用中的差异和优势。

"代码即设计,一套优雅的数据访问层API不仅能减少开发工作量,更能显著提升系统的可维护性和可扩展性。" —— 资深架构师,电商平台技术负责人

2.3.1 商品模块的CRUD实现

我们选择电商系统中的核心模块——商品管理作为实际案例,来对比五种框架的CRUD实现。首先回顾一下商品实体的基本结构:

  • 商品ID:唯一标识
  • 名称、价格、描述:基本商品信息
  • 分类:商品所属分类
  • 库存数量、上架状态:商品状态属性
  • 创建时间、修改时间:审计信息

1. JDBC实现

JDBC作为最基础的数据库访问API,需要开发者手动编写SQL和处理结果集映射。以下是ProductRepositoryImpl的核心实现:

@Repository
public class ProductRepositoryImpl implements ProductRepository {

    private final JdbcTemplate jdbcTemplate;
    private final CategoryRepository categoryRepository;

    // 构造函数注入依赖...

    @Override
    public List<Product> findAll() {
        String sql = "SELECT id, name, price, description, category_id, stock, published, created_time, modified_time FROM product";
        return jdbcTemplate.query(sql, new ProductRowMapper());
    }

    @Override
    public Product save(Product product) {
        if (product.getId() == null) {
            return insertProduct(product);
        } else {
            return updateProduct(product);
        }
    }

    private Product insertProduct(Product product) {
        String sql = "INSERT INTO product (name, price, description, category_id, stock, published, created_time, modified_time) " +
                     "VALUES (?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)";

        KeyHolder keyHolder = new GeneratedKeyHolder();

        jdbcTemplate.update(connection -> {
            PreparedStatement ps = connection.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS);
            ps.setString(1, product.getName());
            ps.setBigDecimal(2, product.getPrice());
            ps.setString(3, product.getDescription());
            if (product.getCategoryId() != null) {
                ps.setLong(4, product.getCategoryId());
            } else {
                ps.setNull(4, java.sql.Types.BIGINT);
            }
            ps.setInt(5, product.getStock());
            ps.setBoolean(6, product.getPublished() != null ? product.getPublished() : true);
            return ps;
        }, keyHolder);

        // 获取生成的ID
        Number key = (Number) keyHolder.getKeys().get("id");
        product.setId(key.longValue());
        return product;
    }

    private class ProductRowMapper implements RowMapper<Product> {
        @Override
        public Product mapRow(ResultSet rs, int rowNum) throws SQLException {
            Product product = new Product();
            product.setId(rs.getLong("id"));
            product.setName(rs.getString("name"));
            product.setPrice(rs.getBigDecimal("price"));
            product.setDescription(rs.getString("description"));

            // 处理外键关联
            Long categoryId = rs.getLong("category_id");
            if (rs.wasNull()) {
                product.setCategoryId(null);
            } else {
                product.setCategoryId(categoryId);
                // 延迟加载分类信息
                categoryRepository.findById(categoryId).ifPresent(product::setCategory);
            }

            // 设置其他字段...
            return product;
        }
    }
}

JDBC的实现特点是:完全控制SQL和结果集处理,但需要大量模板代码和手动映射,特别是在处理关联关系时更为复杂。

2. Hibernate实现

Hibernate通过SessionAPI和对象状态管理简化了数据操作:

@Repository
public class ProductRepositoryImpl implements ProductRepository {

    private final SessionFactory sessionFactory;

    // 构造函数注入依赖...

    @Override
    public List<Product> findAll() {
        try (Session session = openSession()) {
            Transaction tx = session.beginTransaction();
            try {
                List<Product> result = session.createQuery("FROM Product", Product.class).getResultList();
                tx.commit();
                return result;
            } catch (Exception e) {
                tx.rollback();
                throw e;
            }
        }
    }

    @Override
    public Product save(Product product) {
        try (Session session = openSession()) {
            Transaction tx = session.beginTransaction();
            try {
                session.saveOrUpdate(product);
                tx.commit();
                return product;
            } catch (Exception e) {
                tx.rollback();
                throw e;
            }
        }
    }

    @Override
    public List<Product> findByPriceBetween(BigDecimal min, BigDecimal max) {
        try (Session session = openSession()) {
            Transaction tx = session.beginTransaction();
            try {
                CriteriaBuilder builder = session.getCriteriaBuilder();
                CriteriaQuery<Product> criteria = builder.createQuery(Product.class);
                Root<Product> root = criteria.from(Product.class);

                criteria.select(root);

                Predicate pricePredicate = builder.between(root.get("price"), min, max);
                criteria.where(pricePredicate);

                List<Product> result = session.createQuery(criteria).getResultList();
                tx.commit();
                return result;
            } catch (Exception e) {
                tx.rollback();
                throw e;
            }
        }
    }
}

Hibernate的优势在于对象-关系映射的自动处理,支持HQL和Criteria API,事务管理也相对简化。但仍需要手动处理Session和Transaction,且复杂的对象状态管理需要深入理解。

3. JPA实现

JPA通过Spring Data进一步简化了持久层代码:

@Repository
public interface ProductRepository extends JpaRepository<Product, Long> {

    // 通过分类ID查找产品
    List<Product> findByCategoryId(Long categoryId);

    // 通过名称模糊查询产品
    List<Product> findByNameContaining(String name);

    // 查询价格区间内的产品
    List<Product> findByPriceBetween(BigDecimal min, BigDecimal max);

    // 更新发布状态
    @Modifying
    @Transactional
    @Query("UPDATE Product p SET p.published = :published WHERE p.id = :id")
    int updatePublishStatus(@Param("id") Long id, @Param("published") boolean published);
}

JPA/Spring Data最显著的特点是通过接口声明实现数据访问,完全消除了实现类的编写。通过方法命名约定或注解配置查询,极大减少了样板代码。

4. MyBatis实现

MyBatis结合了SQL控制和对象映射的优势:

@Mapper
public interface ProductMapper {

    List<Product> findAll();

    Product findById(Long id);

    List<Product> findByNameLike(String name);

    List<Product> findByPriceBetween(
        @Param("minPrice") BigDecimal minPrice, 
        @Param("maxPrice") BigDecimal maxPrice
    );

    int insert(Product product);

    int update(Product product);

    int deleteById(Long id);
}
<!-- ProductMapper.xml -->
<mapper namespace="org.lijma.mybatis.samples.mapper.ProductMapper">
    <resultMap id="productResultMap" type="org.lijma.mybatis.samples.entity.Product">
        <id property="id" column="id"/>
        <result property="name" column="name"/>
        <!-- 其他字段映射... -->
    </resultMap>

    <select id="findAll" resultMap="productResultMap">
        SELECT id, name, price, description, category_id, stock, published, created_time, modified_time
        FROM product
    </select>

    <insert id="insert" useGeneratedKeys="true" keyProperty="id">
        INSERT INTO product (name, price, description, category_id, stock, published, created_time, modified_time)
        VALUES (#{name}, #{price}, #{description}, #{categoryId}, #{stock}, #{published}, #{createdTime}, #{modifiedTime})
    </insert>

    <!-- 其他SQL映射... -->
</mapper>

MyBatis的特点是分离了SQL和Java代码,提供了强大的SQL构建和映射能力,但需要管理XML配置文件,且手动处理关联关系可能较为复杂。

5. Jimmer实现

Jimmer通过类型安全的DSL和动态Fetcher实现数据访问:

@Repository
public class ProductRepositoryImpl implements ProductRepository {

    private final JSqlClient sqlClient;

    // 定义默认的fetcher,包含所有标量字段和category关联
    private static final Fetcher<Product> DEFAULT_FETCHER = 
        ProductFetcher.$.allScalarFields().category();

    // 构造函数注入依赖...

    @Override
    public List<Product> findAll() {
        ProductTable table = ProductTable.$;
        return sqlClient
            .createQuery(table)
            .select(table.fetch(DEFAULT_FETCHER))
            .execute();
    }

    @Override
    @Transactional
    public void save(Product product) {
        sqlClient.save(product);
    }

    @Override
    public List<Product> findByNameLike(String name) {
        ProductTable table = ProductTable.$;
        return sqlClient
            .createQuery(table)
            .where(table.name().like("%" + name + "%"))
            .select(table.fetch(DEFAULT_FETCHER))
            .execute();
    }

    @Override
    @Transactional
    public Product updatePublishStatus(long id, boolean published) {
        ProductTable table = ProductTable.$;

        // 使用SQL直接更新
        sqlClient
            .createUpdate(table)
            .set(table.published(), published)
            .set(table.modifiedTime(), LocalDateTime.now())
            .where(table.id().eq(id))
            .execute();

        // 重新查询并返回更新后的商品
        return sqlClient
            .createQuery(table)
            .where(table.id().eq(id))
            .select(table.fetch(DEFAULT_FETCHER))
            .execute()
            .stream()
            .findFirst()
            .orElseThrow(() -> new IllegalArgumentException("Product not found after update: " + id));
    }
}

Jimmer的特点是结合了类型安全查询、不可变对象和动态Fetcher,为开发者提供了简洁但功能强大的API,特别是在处理关联数据和构建动态查询时更具优势。

2.3.2 数据访问模式对比

通过上述代码示例,我们可以归纳不同框架的数据访问模式特点:

框架 数据访问模式 主要特点 典型应用场景
JDBC 直接访问 完全控制、手动映射、透明度高 性能要求极高、特殊数据库功能
Hibernate 对象-关系映射 会话管理、对象状态、自动映射 领域驱动设计、复杂对象模型
JPA 声明式仓库 接口化、方法命名约定、无实现类 标准CRUD、快速开发
MyBatis SQL映射 SQL分离、XML配置、灵活映射 复杂查询、SQL优化
Jimmer 数据结构操作 不可变对象、类型安全、动态获取器 任意形状数据结构的读写、GraphQL API、复杂业务系统

Jimmer相较于传统ORM框架的核心差异在于,它不仅仅关注单一对象的持久化,而是专注于处理任意形状的完整数据结构。这种设计理念使Jimmer特别适合以下场景:

  1. 需要处理复杂数据图的应用:比如嵌套多级的组织结构、产品目录等,Jimmer可以一次操作完整的数据树
  2. GraphQL API实现:Jimmer原生支持按需加载数据字段和关联,与GraphQL的设计理念高度契合
  3. 高度动态的查询需求:通过动态Fetcher精确控制数据加载范围,消除N+1问题
  4. 严格类型安全要求的企业应用:编译时检查避免运行时错误,提高系统稳定性
  5. 需要高性能且易维护的复杂业务系统:兼顾了代码质量和系统性能

2.3.3 服务层设计与实现

在ORM框架之上,服务层是实现业务逻辑的关键组件。不同框架的数据访问模式会显著影响服务层的设计风格和实现方式。

Jimmer的服务层设计优势

Jimmer的不可变对象模型和灵活的数据结构处理能力,为服务层设计带来了诸多独特优势:

@Service
public class ProductService {

    private final JSqlClient sqlClient;
    private final ProductRepository productRepository;

    // 构造函数注入...

    @Transactional
    public Product createProduct(Product draft) {
        // 验证分类是否存在
        if (draft.isAssociated(Product$.CATEGORY)) {
            Category category = sqlClient
                .findById(Category.class, draft.getCategory().id())
                .orElseThrow(() -> new IllegalArgumentException("Category not found"));
        }

        // 设置默认值并创建不可变对象
        Product newProduct = Products.newProduct(draft::appendTo)
            .setCreatedTime(LocalDateTime.now())
            .setModifiedTime(LocalDateTime.now());

        // 一行代码完成保存,自动处理所有关联
        return sqlClient.save(newProduct);
    }

    @Transactional
    public Product updateProduct(long id, Product draft) {
        // 使用TableEx API执行按需更新,只修改提供的字段
        ProductTable table = ProductTable.$;

        // 检查商品是否存在
        sqlClient.findById(Product.class, id)
            .orElseThrow(() -> new IllegalArgumentException("Product not found"));

        // 使用SQL方言的合并功能实现高效更新
        return sqlClient
            .createMerger(Product.class)
            .setIgnoreNullProperties(true) // 忽略未提供的属性
            .merge(draft.withId(id));
    }

    public Product findProductWithDetails(long id, String detailsSpec) {
        // 使用动态Fetcher按需加载产品详情
        ProductFetcher fetcher = ProductFetcher.$.allScalarFields();

        // 根据detailsSpec动态构建获取器
        if (detailsSpec.contains("category")) {
            fetcher = fetcher.category(CategoryFetcher.$.allScalarFields());
        }
        if (detailsSpec.contains("reviews")) {
            fetcher = fetcher.reviews(ReviewFetcher.$.allScalarFields().user());
        }

        // 一次查询加载完整数据结构,避免N+1问题
        return sqlClient
            .findById(fetcher, id)
            .orElseThrow(() -> new IllegalArgumentException("Product not found"));
    }
}

这种基于不可变对象和动态数据结构的服务层设计带来了以下优势:

  1. 业务逻辑更加纯粹:不可变对象使函数式风格的业务逻辑更自然,消除了副作用,提高了代码可测试性
  2. 更精确的数据加载控制:通过动态Fetcher,服务层可以精确控制查询返回的数据结构,避免过度获取或数据不足
  3. 简化的事务处理:Jimmer的保存指令能够自动处理对象图中的所有操作,简化了复杂事务的编排
  4. 更强的并发安全性:不可变对象天然支持并发访问,无需担心对象状态被意外修改
  5. 动态视图支持:服务层可以根据不同的业务场景动态调整返回的数据结构,而无需定义多个不同的方法

相比之下,使用JPA/Hibernate的服务层通常依赖于可变对象和懒加载,这可能导致以下问题:

@Service
public class ProductService {

    private final ProductRepository productRepository;

    @Transactional
    public Product save(Product product) {
        if (product.getId() == null) {
            product.setCreatedTime(LocalDateTime.now());
        }
        product.setModifiedTime(LocalDateTime.now());

        // 需要关注产品对象的状态,处理关联
        if (product.getCategory() != null && product.getCategory().getId() == null) {
            // 处理新创建的关联对象...
        }

        // 可能导致级联保存,难以控制
        return productRepository.save(product);
    }

    @Transactional(readOnly = true)
    public Product findWithDetails(Long id) {
        Product product = productRepository.findById(id)
            .orElseThrow(() -> new IllegalArgumentException("Product not found"));

        // 强制初始化懒加载关联,避免懒加载异常
        Hibernate.initialize(product.getCategory());
        Hibernate.initialize(product.getReviews());

        // 返回的是可变对象,可能在服务层外被修改
        return product;
    }
}

JDBC的服务层则通常需要处理更多的底层细节:

@Service
public class ProductService {

    private final ProductRepository productRepository;
    private final PlatformTransactionManager transactionManager;

    public Product save(Product product) {
        TransactionTemplate transactionTemplate = new TransactionTemplate(transactionManager);
        return transactionTemplate.execute(status -> {
            try {
                // 手动维护审计字段
                if (product.getId() == null) {
                    product.setCreatedTime(LocalDateTime.now());
                }
                product.setModifiedTime(LocalDateTime.now());

                // 手动管理关联对象
                if (product.getCategory() != null) {
                    // 验证分类是否存在...
                }

                // 需要手动处理异常
                return productRepository.save(product);
            } catch (Exception e) {
                status.setRollbackOnly();
                throw e;
            }
        });
    }
}

Jimmer的服务层设计反映了现代应用开发的最佳实践:

  1. 面向数据结构:将完整数据结构作为一等公民,与GraphQL等现代API设计理念一致
  2. 不可变性优先:降低了系统复杂度,提高了可预测性和可测试性
  3. 编译时安全:类型安全的DSL避免了运行时错误,提高了系统稳定性
  4. 声明式查询:使开发者能够以声明式方式表达数据需求,而不是命令式地描述如何获取数据

这种设计理念使得Jimmer特别适合处理复杂业务场景,尤其是在微服务架构、响应式系统和需要严格数据控制的企业应用中。

2.3.4 基础操作性能对比

性能是选择持久化框架的重要考量因素。Jimmer官方提供了完整的基准测试报告,所有测试均使用H2内存数据库,尽量降低数据库本身的影响,更真实地反映各框架在对象映射层面的性能差异。

下面是基于Jimmer官方benchmark测试的性能对比数据:

每秒操作次数(数值越高越好)

数据量 JDBC(ColIndex) JDBC(ColName) Jimmer JPA(Hibernate) Exposed jOOQ MyBatis
10行 66,780 47,840 43,880 24,890 16,990 36,210 36,210
100行 9,240 6,080 7,220 2,900 2,300 3,920 4,170
1000行 950 640 840 290 240 400 430

每次操作耗时,单位:微秒(数值越低越好)

数据量 JDBC(ColIndex) JDBC(ColName) Jimmer JPA(Hibernate) Exposed jOOQ MyBatis
10行 15μs 21μs 23μs 40μs 59μs 28μs 28μs
100行 108μs 164μs 138μs 345μs 435μs 255μs 240μs
1000行 1,053μs 1,563μs 1,190μs 3,448μs 4,167μs 2,500μs 2,326μs

测试环境: - 硬件:基于标准硬件配置 - 软件:使用H2内存数据库 - 测试方法:基于JMH(Java Microbenchmark Harness)的基准测试

对比分析:

  1. 查询性能比较:在小数据量场景下,Jimmer的性能接近JDBC(ColName),随着数据量增加到1000行时,Jimmer的性能优势更加明显,明显超过了JDBC(ColName)。

  2. 与其他ORM比较:从测试数据可以看出,Jimmer在所有数据量场景下,性能都显著优于JPA(Hibernate)、Exposed等传统ORM框架。100行数据时,Jimmer的性能是Hibernate的2.5倍;1000行数据时,这个差距进一步拉大到2.9倍。

  3. 与SQL构建工具比较:相比jOOQ和MyBatis等SQL构建工具,Jimmer也保持了性能优势,特别是在处理更大数据量时。

Jimmer官方基准测试揭示了一个有趣的现象:当数据量增加时,Jimmer的性能优势更加明显。特别是与JDBC(ColName)相比,Jimmer在处理大量数据时表现更佳,这得益于两个关键优化:

  1. 底层采用JDBC(ColIndex)方式访问ResultSet,这比按列名访问性能更高
  2. 不使用Java反射动态设置属性值,而是通过编译时生成的高效代码处理对象属性赋值

总体而言,Jimmer在保持开发友好性的同时,提供了接近甚至超越低级API的性能,特别是在处理大量数据的场景中表现出色。这打破了"ORM必然导致性能损失"的传统观念,证明了现代ORM框架可以在不牺牲性能的前提下,提供卓越的开发体验。

小结

本节我们深入探讨了五种主流持久化框架在基础数据操作方面的实现方式和性能表现。通过对比分析,我们可以得出以下结论:

  1. 各框架各有所长:JDBC提供了最大的控制力,JPA简化了开发,MyBatis平衡了SQL控制和对象映射,而Jimmer则通过不可变对象和类型安全查询带来了新的开发范式。

  2. Jimmer的创新点:作为新一代ORM框架,Jimmer通过将任意形状的数据结构作为核心概念,解决了传统ORM的诸多痛点,特别是在处理复杂数据结构、动态查询和高性能场景时表现突出。

  3. 服务层设计的演进:Jimmer的不可变对象模型促使开发者采用更加函数式和声明式的服务层设计,提高了代码的可预测性和可测试性。

  4. 性能神话的打破:Jimmer的性能测试结果证明,高级别抽象和优异性能并非不可兼得,通过精心的设计和优化,现代ORM框架可以提供接近甚至超越低级API的性能。

数据操作的基础能力是一个框架的根本,但真正能够体现框架优势的往往是在处理复杂关系时。在下一节中,我们将进一步探讨这些框架在处理一对多、多对多以及递归结构等复杂关联数据场景中的表现,这也是ORM框架的核心价值所在。我们将看到Jimmer如何通过动态Fetcher、关联视图等创新概念,为复杂关联数据处理提供前所未有的灵活性和性能。

2.4 关联数据处理

2.4.1 一对多/多对一关系处理:ORM框架的基石

关联关系处理是ORM框架的核心价值主张。在各类关系中,一对多/多对一是最基础也最常见的。以电商系统为例,商品(Product)和分类(Category)之间的关系是典型的多对一关系——一个分类可以包含多个商品,而一个商品只能属于一个分类。这种看似简单的关系,实际上涉及到了关系型数据库和面向对象编程范式之间的根本性差异。

一对多关系的本质与挑战

为什么一对多关系会成为ORM技术的焦点?这涉及到所谓的"阻抗不匹配"(Impedance Mismatch)问题。

关系型数据库视角:在关系数据库中,一对多关系通过外键实现。例如:

CREATE TABLE category (
    id BIGINT PRIMARY KEY,
    name VARCHAR(100) NOT NULL
);

CREATE TABLE product (
    id BIGINT PRIMARY KEY,
    name VARCHAR(100) NOT NULL,
    category_id BIGINT,
    FOREIGN KEY (category_id) REFERENCES category(id)
);

这里,product表中的category_id列是指向category表的外键,通过这个外键可以建立产品和分类之间的关联。

面向对象视角:在面向对象的世界里,同样的关系是通过对象引用来表达的:

// 对象关系表达
class Category {
    private Long id;
    private String name;
    private List<Product> products; // 一对多的"一"方包含对"多"方的引用集合
}

class Product {
    private Long id;
    private String name;
    private Category category; // 多对一的"多"方包含对"一"方的引用
}

这两种表达方式之间存在根本性的差异: 1. 表示形式差异:数据库使用外键,对象使用引用 2. 方向性差异:数据库关系是单向的(通过外键从"多"查"一"),而对象关系可以是双向的 3. 加载策略差异:数据库通过JOIN即时加载关联数据,而对象关系可能需要延迟加载或预加载 4. 修改传播差异:对象关系中的修改需要同步到数据库的多个表中

这些差异正是ORM框架需要解决的核心问题。下面,我们深入分析不同框架如何处理这些挑战。

Jimmer的一对多关系处理机制

Jimmer在处理一对多关系时,采用了全新的思路,核心在于其不可变对象模型类型安全的Fetcher API

  1. 声明式关系定义
// 在Category实体中 - 一方
@Entity
public interface Category {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    long id();

    String name();

    // 反向关联,通过mappedBy指定
    @OneToMany(mappedBy = "category")
    List<Product> products(); 
}

// 在Product实体中 - 多方
@Entity
public interface Product {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    long id();

    String name();

    // 正向关联,通过外键列关联
    @ManyToOne
    @JoinColumn(name = "category_id")
    Category category();
}

Jimmer使用接口而非类来定义实体,这是其与传统ORM的根本区别之一。在运行时,Jimmer会生成这些接口的实现类,内部采用高度优化的不可变对象模型。

  1. 数据加载与N+1问题解决方案

Jimmer通过其独特的Fetcher API解决了关联数据加载中的N+1问题:

// 定义数据加载形状
Fetcher<Product> productFetcher = ProductFetcher.$
    .allScalarFields()  // 选择所有基本字段
    .category(         // 加载关联的分类
        CategoryFetcher.$.allScalarFields()
    );

// 使用预定义的形状加载数据
List<Product> products = sqlClient
    .createQuery(ProductTable.$)
    .select(ProductTable.$.fetch(productFetcher))
    .execute();

Fetcher的核心优势在于: - 显式声明:数据加载形状由开发者显式定义,而非隐式推断 - 编译时安全:类型安全的API,编译时即可发现错误 - 单次加载:通过JOIN一次性加载完整数据图,避免N+1问题 - 精确控制:只加载需要的字段和关联,避免过度获取

在底层实现中,Jimmer会根据Fetcher自动生成优化的SQL语句:

-- Jimmer根据Fetcher自动生成的SQL
SELECT
    p.id, p.name, p.price, /* 其他product字段 */
    c.id as "c_id", c.name as "c_name", /* 其他category字段 */
FROM product p
LEFT JOIN category c ON p.category_id = c.id

这种机制相比传统ORM有几个关键优势: 1. 显式性:加载哪些数据是明确的,没有"惊喜" 2. 性能可预测:开发者可以清楚预见SQL执行情况 3. 按需加载:根据不同业务场景定制不同的Fetcher 4. 复用性:Fetcher可以模块化组合,提高代码复用

  1. 实际案例解析

以电商系统中的"查看商品详情"功能为例:

// 定义商品详情页的数据形状
Fetcher<Product> detailFetcher = ProductFetcher.$
    .allScalarFields()
    .category(CategoryFetcher.$
        .allScalarFields()
        .parent(CategoryFetcher.$.name())  // 只需要父分类的名称
    )
    .reviews(ReviewFetcher.$    // 同时加载评论
        .allScalarFields()
        .user(UserFetcher.$.name().avatar())  // 评论用户的部分信息
    );

// 查询逻辑简单清晰
Product productDetail = sqlClient
    .createQuery(ProductTable.$)
    .where(ProductTable.$.id().eq(productId))
    .select(ProductTable.$.fetch(detailFetcher))
    .fetchOne();

这段代码一次性加载了商品、分类(及其父分类)、评论以及评论用户的数据,并且精确控制了每个实体的字段加载范围。Jimmer会将这个复杂的数据图转换为高效的SQL查询,通常是一个带有多表JOIN的单一查询。

JPA/Hibernate的一对多关系处理对比

相比之下,JPA/Hibernate采用了不同的机制:

  1. 实体定义
@Entity
@Table(name = "category")
public class Category {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String name;

    @OneToMany(mappedBy = "category", fetch = FetchType.LAZY)
    private List<Product> products;

    // getters and setters
}

@Entity
@Table(name = "product")
public class Product {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String name;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "category_id")
    private Category category;

    // getters and setters
}

JPA的核心特点: - POJO模型:使用普通Java类 - 隐式懒加载:默认配置为延迟加载关联 - 代理访问:通过代理拦截关联属性的访问 - 双向关系:同时维护双向关系

  1. N+1问题与解决方案

在JPA中,处理N+1问题的方法有:

方法1: FETCH JOIN

List<Product> products = entityManager
    .createQuery(
        "SELECT p FROM Product p " +
        "LEFT JOIN FETCH p.category",
        Product.class
    )
    .getResultList();

方法2: EntityGraph

@EntityGraph(attributePaths = {"category"})
List<Product> findAll();

方法3: Batch Fetching

@Entity
@BatchSize(size = 25)
public class Category { ... }

与Jimmer的对比: 1. 显式性不足:数据加载逻辑不够明确,容易产生意外查询 2. 类型安全欠缺:JPQL/HQL为字符串,容易出现运行时错误 3. 灵活性受限:对复杂数据图加载支持有限 4. 代码复用性较低:查询逻辑难以模块化和复用

  1. 实际案例对比

以同样的"商品详情"功能为例:

// JPA/Hibernate方式
@Transactional(readOnly = true)
public ProductDTO getProductDetail(Long id) {
    Product product = productRepository.findById(id)
        .orElseThrow(() -> new ProductNotFoundException(id));

    // 触发懒加载
    Category category = product.getCategory();
    if (category != null) {
        category.getParent();  // 加载父分类
    }

    // 加载评论和用户
    List<Review> reviews = reviewRepository.findByProductId(id);

    // 手动组装DTO
    ProductDTO dto = new ProductDTO();
    // 复制属性...

    return dto;
}

这种方法的缺点: 1. 隐式查询:不清楚会执行多少次数据库查询 2. N+1风险:如果评论很多,每条评论加载用户会造成额外查询 3. 手动组装:需要手动将实体转换为DTO 4. 事务控制:需要在事务内访问懒加载属性

MyBatis与JDBC的一对多关系处理:控制与灵活性

MyBatis和JDBC采用更接近SQL的方式处理关联:

  1. MyBatis的关联处理
<resultMap id="productWithCategoryMap" type="org.example.Product">
    <id property="id" column="id"/>
    <result property="name" column="name"/>
    <association property="category" javaType="org.example.Category">
        <id property="id" column="category_id"/>
        <result property="name" column="category_name"/>
    </association>
</resultMap>

<select id="findProductsWithCategory" resultMap="productWithCategoryMap">
    SELECT p.*, c.id as category_id, c.name as category_name
    FROM product p
    LEFT JOIN category c ON p.category_id = c.id
</select>

MyBatis的特点: - 显式SQL:手动编写JOIN语句 - 自定义映射:通过XML配置映射关系 - 灵活性高:可以精确控制SQL执行 - 开发成本高:手动维护SQL和映射

  1. JDBC的关联处理
// 使用JDBC手动处理关联
public List<Product> findProductsWithCategory() throws SQLException {
    String sql = "SELECT p.*, c.id as category_id, c.name as category_name " +
                 "FROM product p " +
                 "LEFT JOIN category c ON p.category_id = c.id";

    List<Product> products = new ArrayList<>();

    try (Connection conn = dataSource.getConnection();
         PreparedStatement stmt = conn.prepareStatement(sql);
         ResultSet rs = stmt.executeQuery()) {

        while (rs.next()) {
            Product product = new Product();
            product.setId(rs.getLong("id"));
            product.setName(rs.getString("name"));

            if (rs.getObject("category_id") != null) {
                Category category = new Category();
                category.setId(rs.getLong("category_id"));
                category.setName(rs.getString("category_name"));
                product.setCategory(category);
            }

            products.add(product);
        }
    }

    return products;
}

JDBC的特点: - 完全控制:对SQL和映射逻辑完全掌控 - 性能优化:可以进行最底层的优化 - 代码冗长:需要大量样板代码 - 维护成本高:修改实体结构需要同步修改多处代码

一对多关系处理的最佳实践

综合比较各框架,我们可以提炼出以下最佳实践:

  1. 明确加载边界
  2. Jimmer的Fetcher API提供了最佳方案
  3. JPA应使用EntityGraph或FETCH JOIN显式加载
  4. MyBatis应设计合理的ResultMap
  5. JDBC应考虑封装常见的关联加载逻辑

  6. 避免循环依赖

  7. 双向关系需谨慎处理,避免序列化问题
  8. Jimmer的不可变对象模型天然避免了循环依赖问题
  9. JPA使用@JsonIgnore或DTO转换处理

  10. 按需加载

  11. 针对不同业务场景定制加载策略
  12. 避免过度获取和不足获取
  13. 合理使用即时加载和延迟加载

  14. 性能监控

  15. 定期审查SQL执行情况
  16. 警惕隐藏的N+1问题
  17. 使用批量加载代替单条加载

2.4.2 递归结构处理:树形数据的优雅实现

递归结构是关联关系的特殊形式,在企业应用中极为常见。典型的例子包括:组织架构、产品分类、文件目录等。这类结构的特点是单个实体既是父节点又是子节点,形成层次结构。

递归关系的数据模型与挑战

以商品分类为例,其数据库模型通常设计为:

CREATE TABLE category (
    id BIGINT PRIMARY KEY,
    name VARCHAR(100) NOT NULL,
    parent_id BIGINT,
    path VARCHAR(255),
    FOREIGN KEY (parent_id) REFERENCES category(id)
);

这里,parent_id是指向同表中记录的外键,形成自引用关系。path字段常用于存储完整路径,如1/5/23,表示当前分类的所有上级节点。

递归结构在ORM中面临的挑战:

  1. 无限递归风险:加载父节点过程可能无限深入
  2. 性能问题:逐级加载会导致多次数据库查询
  3. 双向关系:父子节点间的双向链接需要特殊处理
  4. 路径维护:分类路径的自动维护较为复杂

Jimmer的递归结构处理机制

Jimmer提供了几种处理递归结构的高级方案:

  1. 递归Fetcher:精确控制加载深度
// 递归Fetcher定义
Fetcher<Category> categoryFetcher = CategoryFetcher.$
    .allScalarFields()
    .parent(CategoryFetcher.$
        .allScalarFields()
        .parent(CategoryFetcher.$
            .allScalarFields()
        )
    )
    .childCategories(CategoryFetcher.$
        .allScalarFields()
    );

// 使用递归Fetcher加载分类树
Category category = sqlClient
    .createQuery(CategoryTable.$)
    .where(CategoryTable.$.id().eq(categoryId))
    .select(CategoryTable.$.fetch(categoryFetcher))
    .fetchOne();

这种方式的优势在于: - 明确的加载深度:Fetcher明确定义了加载的层级,避免无限递归 - 一次性查询:Jimmer会自动优化SQL,尽量通过一次查询加载完整树 - 灵活定制:可以为树的不同层级设置不同的加载字段

  1. 递归SQL查询:借助数据库CTE功能

Jimmer支持使用SQL的WITH RECURSIVE语法一次性查询整个树:

// 使用WITH RECURSIVE查询整个分类树
List<Category> categories = sqlClient
    .createNativeQuery(
        "WITH RECURSIVE cat_tree AS (" +
        "   SELECT * FROM category WHERE parent_id IS NULL " +
        "   UNION ALL " +
        "   SELECT c.* FROM category c " +
        "   JOIN cat_tree ct ON c.parent_id = ct.id" +
        ") SELECT * FROM cat_tree",
        Category.class
    )
    .execute();

这种方法充分利用了数据库的递归查询能力,具有极高的性能。

  1. 路径枚举模式:实用的树结构加速方案

Jimmer还支持路径枚举模式,通过维护路径字段来高效查询:

// 实体定义
@Entity
public interface Category {
    // 基本属性...

    @ManyToOne
    @JoinColumn(name = "parent_id")
    @OnDissociate(DissociateAction.SET_NULL)  // 父节点删除时设为null
    Category parent();

    @Nullable
    String path();  // 存储如"1/5/23"的路径

    @OneToMany(mappedBy = "parent")
    @OrderBy("name")
    List<Category> childCategories();
}

// 所有子分类的高效查询(不需要递归)
List<Category> allDescendants = sqlClient
    .createQuery(CategoryTable.$)
    .where(CategoryTable.$.path().like(parentPath + "/%"))
    .select(CategoryTable.$)
    .execute();

路径枚举模式的优势: - 查询高效:使用LIKE查询可以一次获取所有后代 - 层级信息内置:路径本身包含了层级信息 - 实现简单:不需要复杂的递归查询

  1. 案例:完整的分类树管理
@Transactional
public Category createCategory(Category draft) {
    // 验证父分类存在
    Category parent = null;
    if (draft.isAssociated(Category$.PARENT)) {
        parent = sqlClient
            .findById(Category.class, draft.getParent().id())
            .orElseThrow(() -> new EntityNotFoundException("父分类不存在"));
    }

    // 创建新分类
    CategoryDraft newCategory = CategoryDraft.$.produce(draft::appendTo);

    // 自动构建路径
    if (parent != null) {
        String parentPath = parent.path();
        newCategory.setPath(parentPath != null ? 
            parentPath + "/" + parent.id() : 
            String.valueOf(parent.id()));
    }

    // 保存并返回
    return sqlClient.save(newCategory);
}

这个例子展示了Jimmer如何处理分类树的创建,包括路径的自动维护。

JPA/Hibernate的递归结构处理

JPA/Hibernate处理递归结构的方式与Jimmer有显著差异:

  1. 基本递归关联
@Entity
@Table(name = "category")
public class Category {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String name;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "parent_id")
    private Category parent;

    @OneToMany(mappedBy = "parent", fetch = FetchType.LAZY)
    private List<Category> children;

    private String path;

    // getters and setters
}
  1. 递归加载策略与挑战
// 使用JPQL加载两级分类树
List<Category> topCategories = entityManager
    .createQuery(
        "SELECT DISTINCT c FROM Category c " +
        "LEFT JOIN FETCH c.children " +
        "WHERE c.parent IS NULL",
        Category.class
    )
    .getResultList();

// 但要加载更深层级,通常需要多次查询
for (Category topCategory : topCategories) {
    Hibernate.initialize(topCategory.getChildren());
    for (Category secondLevel : topCategory.getChildren()) {
        Hibernate.initialize(secondLevel.getChildren());
        // 可能继续递归...
    }
}

JPA处理递归结构的限制: - 懒加载困境:Either太浅(数据不完整)or太深(性能问题) - N+1问题:逐级加载会导致多次数据库查询 - 代码复杂:深度控制需要编写复杂的递归逻辑 - 路径维护:需要手动维护path字段

  1. 递归CTE的有限支持
// JPA原生SQL支持递归CTE
List<Object[]> results = entityManager
    .createNativeQuery(
        "WITH RECURSIVE cat_tree AS (" +
        "   SELECT * FROM category WHERE parent_id IS NULL " +
        "   UNION ALL " +
        "   SELECT c.* FROM category c " +
        "   JOIN cat_tree ct ON c.parent_id = ct.id" +
        ") SELECT * FROM cat_tree"
    )
    .getResultList();

// 但需要手动映射结果
List<Category> categories = new ArrayList<>();
for (Object[] row : results) {
    Category category = new Category();
    category.setId(((Number)row[0]).longValue());
    category.setName((String)row[1]);
    // 其他字段映射...
    categories.add(category);
}

与Jimmer相比的不足: - 映射负担:原生SQL需要手动映射结果 - 实体管理:结果不会自动被EntityManager管理 - 关系重建:需要手动重建实体间的关系

递归结构处理的最佳实践

综合各框架的经验,处理递归结构的最佳实践包括:

  1. 预先计算路径
  2. 维护path字段,使用分隔符(如"/")连接所有祖先ID
  3. 插入或移动节点时自动更新所有后代的路径
  4. 使用路径匹配进行高效的树查询

  5. 分层加载策略

  6. 根据UI展示需求设计加载深度
  7. 采用"按需加载"模式,先加载主要层级,再根据用户交互加载更深层级
  8. 考虑使用缓存存储常用树结构

  9. 性能优化技巧

  10. 利用数据库的递归CTE功能
  11. 避免在循环中逐个加载子节点
  12. 考虑使用存储过程处理复杂的树操作

  13. 选择适当的工具

  14. Jimmer的Fetcher提供了最优雅的递归加载控制
  15. JPA适合简单的层次结构
  16. 对性能要求极高的场景,考虑专用树形数据库或图数据库

递归结构虽然概念简单,但高效实现却是ORM框架的重要衡量标准。Jimmer在这方面的设计特别出色,通过显式加载控制、编译时安全的API以及高效的SQL生成,提供了处理复杂树形结构的优雅解决方案。

2.4.3 关联数据处理的综合对比与思考

通过前面对一对多/多对一关系和递归结构的详细讨论,我们可以对各ORM框架在关联数据处理方面进行一个全面的对比评估:

框架 关联定义方式 数据加载策略 N+1问题解决 递归结构支持 开发体验 性能特性
Jimmer 接口式定义,声明式关联 Fetcher显式控制 优秀(显式加载形状) 优秀(多种机制) 类型安全,代码简洁 优秀(可预测)
JPA/Hibernate 注解式定义,POJO 懒加载/预加载 良好(需手动处理) 中等(递归加载复杂) 自然的POJO风格 良好(需优化)
MyBatis XML映射文件 手动SQL控制 中等(需手写JOIN) 中等(需复杂配置) 灵活但繁琐 良好(可精细控制)
JDBC 手动映射代码 完全手动 弱(需全面自控) 弱(需大量代码) 底层控制,复杂 因开发者能力而异

性能对比分析

在处理大量关联数据场景下,不同框架的性能表现各有特点:

  1. 查询性能
  2. Jimmer通过智能SQL生成和缓存优化提供了优秀的查询性能,特别是在加载复杂数据结构时效率极高
  3. JPA/Hibernate在处理大量实体时存在性能瓶颈,特别是因懒加载引起的N+1问题
  4. MyBatis通过手写优化SQL可以获得不错性能,但工作量大
  5. JDBC理论上可以实现最优性能,但需要开发者精通SQL优化

  6. 内存消耗

  7. Jimmer的不可变对象虽会创建多个实例,但通过结构共享优化了内存使用
  8. JPA/Hibernate的持久化上下文在大数据量时内存消耗较大
  9. MyBatis和JDBC的内存使用较为直接,由开发者控制

  10. 批量操作

  11. 在批量导入/更新场景下,所有框架都需要特别优化
  12. Jimmer和Hibernate支持自动批处理,但需配置
  13. MyBatis和JDBC需要手动编写批处理逻辑

开发体验与生产力

  1. 代码可维护性
  2. Jimmer接口式实体和类型安全API使代码更加健壮,但有学习曲线
  3. JPA/Hibernate的POJO风格直观易懂,但隐式行为可能带来隐患
  4. MyBatis的XML映射文件与Java代码分离,不利于重构
  5. JDBC代码冗长,重复性高,维护成本高

  6. 调试复杂度

  7. Jimmer的显式数据加载使调试更简单直观
  8. JPA/Hibernate懒加载可能导致意外查询,调试复杂
  9. MyBatis和JDBC的SQL执行直接可见,便于调试

特定场景的最佳实践

基于上述分析,不同业务场景下的框架选择建议:

  1. 数据复杂度高、关联关系多的企业应用:
  2. 优先选择Jimmer,其Fetcher API和不可变对象模型特别适合处理复杂数据图
  3. 次选JPA/Hibernate,但需注意性能优化

  4. 以数据分析和报表为主的应用:

  5. MyBatis或JDBC更适合,因为可以精确控制SQL
  6. Jimmer的动态查询能力也很强,但可能过于复杂

  7. 大数据量、高并发的场景:

  8. 需重点关注N+1问题和批处理能力
  9. Jimmer和优化良好的MyBatis是好选择

  10. 复杂树形结构的应用(如组织架构、产品分类):

  11. Jimmer的递归Fetcher和路径枚举支持是最佳选择
  12. 复杂度和性能要求较低时,JPA也可接受

总结

在本节中,我们详细探讨了关联数据处理的各种策略和技术,解决了静态数据结构的加载和维护问题。然而,现代应用开发面临的一个重要挑战是动态性——如何处理不确定条件下的数据查询?

在下一节(2.5 动态查询实现),我们将继续探索ORM框架的另一个核心能力:动态查询。这个需求在电商系统的商品搜索功能中尤为突出——用户可能根据分类、价格区间、品牌、评分等多种条件组合进行筛选,并根据不同维度排序。这就要求我们的数据访问层具备高度灵活性,同时保持代码的可维护性和查询的高性能。

Jimmer的类型安全动态查询、JPA的Criteria API、MyBatis的动态SQL以及JDBC的手动查询构建都提供了解决方案,但在表达力、安全性和易用性上存在显著差异。在2.5章节中,我们将深入对比这些技术,探索动态数据访问的最佳实践。

2.5 动态查询实现

在实际业务系统中,用户通常需要根据多种条件组合来查询数据。以电商系统为例,用户可能需要按照价格区间、产品分类、名称关键词等多个条件筛选商品,并按照不同字段排序。这类需求要求我们的数据访问层能够处理动态的查询条件,而这正是ORM框架的一个关键挑战点。

2.5.1 动态查询的本质与挑战

动态查询本质上是一种在运行时确定查询条件、排序方式和返回结果结构的查询方式。与预先定义好的固定查询不同,动态查询需要在运行时根据用户输入或系统状态构建查询语句。

动态查询的典型场景

电商系统中的商品搜索是动态查询的典型场景。假设我们有一个商品搜索页面,用户可以: - 输入商品名称进行模糊搜索 - 选择商品分类 - 设置价格范围 - 筛选仅展示已上架商品 - 按照价格、上架时间或销量排序

要实现这样的功能,传统的静态SQL语句是无法满足需求的,因为查询条件是不确定的——用户可能只填写价格范围而不选择分类,也可能只选择分类而不设置价格范围,甚至可能所有条件都不填写。

动态查询的挑战

实现高质量的动态查询机制面临以下挑战:

  1. 安全性:动态拼接SQL容易导致SQL注入漏洞
  2. 类型安全:字符串拼接的SQL无法在编译时检查语法和类型错误
  3. 可维护性:复杂的条件组合容易导致代码混乱难以维护
  4. 性能问题:不当的动态查询可能导致数据库性能问题,如无法使用索引
  5. 可测试性:动态SQL难以进行全面的单元测试

要应对这些挑战,各ORM框架采用了不同的策略。接下来,我们将深入分析几种主流框架的动态查询实现方式,并通过具体示例对比它们的优缺点。

2.5.2 Jimmer的类型安全动态查询

Jimmer提供了一套完全类型安全的动态查询API,它基于Java的强类型特性和编译时代码生成,提供了高度直观且类型安全的查询构建体验。

核心API特性

Jimmer的动态查询建立在以下核心概念之上:

  1. 类型安全的表达式:所有查询条件都通过强类型的表达式构建
  2. 流畅的API链式调用:通过方法链实现直观的查询构建
  3. 编译时生成的表元数据:通过代码生成避免运行时反射
  4. 谓词组合器:支持条件的逻辑组合(AND、OR等)

基本动态查询示例

以商品搜索为例,Jimmer实现动态查询的方式如下:

// jimmer-in-action-samples/chapter2/jimmer/src/main/java/org/lijma/jimmer/samples/repository/ProductRepositoryImpl.java
public List<Product> findByDynamicConditions(
        String name, 
        Long categoryId, 
        BigDecimal minPrice, 
        BigDecimal maxPrice, 
        Boolean published) {

    // 获取表元数据
    ProductTable table = ProductTable.$;

    // 构建查询
    return sqlClient.createQuery(table)
        .where(whereBuilder -> {
            // 动态添加条件
            if (name != null && !name.isEmpty()) {
                whereBuilder.and(table.name().like("%" + name + "%"));
            }

            if (categoryId != null) {
                whereBuilder.and(table.category().id().eq(categoryId));
            }

            if (minPrice != null) {
                whereBuilder.and(table.price().ge(minPrice));
            }

            if (maxPrice != null) {
                whereBuilder.and(table.price().le(maxPrice));
            }

            if (published != null) {
                whereBuilder.and(table.isPublished().eq(published));
            }
        })
        .select(table.fetch(ProductFetcher.$.allScalarFields().category()))
        .execute();
}

这种方式的特点是: - 完全类型安全:表名、列名都通过编译时生成的类引用,避免了拼写错误 - 清晰直观:条件构建逻辑简洁明了 - 易于维护:可以轻松添加或删除查询条件 - 避免SQL注入:条件值通过参数化方式传入,不直接拼接SQL字符串

复杂条件组合

Jimmer还支持更复杂的条件组合,例如OR、嵌套条件等:

// 复杂条件组合示例
return sqlClient.createQuery(table)
    .where(whereBuilder -> {
        // 基本条件
        if (categoryId != null) {
            whereBuilder.and(table.category().id().eq(categoryId));
        }

        // 构建OR条件组
        Predicate orPredicate = Predicates.or(
            table.name().like("%" + keyword + "%"),
            table.description().like("%" + keyword + "%")
        );
        whereBuilder.and(orPredicate);

        // 嵌套AND条件组
        if (minPrice != null && maxPrice != null) {
            whereBuilder.and(Predicates.and(
                table.price().ge(minPrice),
                table.price().le(maxPrice)
            ));
        }
    })
    .select(table)
    .execute();

排序与分页

动态查询通常还需要支持排序和分页:

// 添加动态排序和分页
public List<Product> findWithSortAndPaging(
        Map<String, Object> conditions, 
        String sortField,
        boolean ascending,
        int pageIndex,
        int pageSize) {

    ProductTable table = ProductTable.$;

    // 构建查询
    ConfigurableRootQuery<Product> query = sqlClient.createQuery(table)
        .where(buildDynamicPredicate(table, conditions));

    // 添加动态排序
    if ("price".equals(sortField)) {
        query = ascending ? 
            query.orderBy(table.price().asc()) : 
            query.orderBy(table.price().desc());
    } else if ("name".equals(sortField)) {
        query = ascending ? 
            query.orderBy(table.name().asc()) : 
            query.orderBy(table.name().desc());
    } else {
        query = query.orderBy(table.id().desc()); // 默认排序
    }

    // 添加分页
    return query
        .select(table.fetch(ProductFetcher.$.allScalarFields().category()))
        .limit(pageSize)
        .offset(pageIndex * pageSize)
        .execute();
}

Jimmer的动态查询API自然地支持链式调用,使得查询构建过程流畅直观。

优势与特点

Jimmer动态查询方案的关键优势在于:

  1. 编译时安全:所有表达式都是类型安全的,拼写错误和类型错误在编译时就能被发现
  2. DSL流畅性:查询构建过程直观易读,接近自然语言描述
  3. 高度可组合:条件、排序、分页等组件可以灵活组合
  4. 代码简洁:相比其他框架,代码量显著减少
  5. 性能优化:Jimmer会自动优化生成的SQL,确保高效执行

2.5.3 JPA/Hibernate的Criteria查询

JPA提供了Criteria API作为动态查询的标准解决方案。它是一个面向对象的API,允许以Java代码而非字符串构建查询。

Criteria API基础

Criteria API基于元模型概念,通过CriteriaBuilderCriteriaQuery等组件构建查询:

// jimmer-in-action-samples/chapter2/hibernate/src/main/java/org/lijma/hibernate/samples/repository/ProductRepositoryImpl.java
public List<Product> findByDynamicConditions(
        String name, 
        Long categoryId,
        BigDecimal minPrice, 
        BigDecimal maxPrice,
        Boolean published) {

    try (Session session = openSession()) {
        Transaction tx = session.beginTransaction();
        try {
            // 创建CriteriaBuilder
            CriteriaBuilder builder = session.getCriteriaBuilder();
            CriteriaQuery<Product> criteria = builder.createQuery(Product.class);
            Root<Product> root = criteria.from(Product.class);

            // 初始化谓词列表
            List<Predicate> predicates = new ArrayList<>();

            // 根据条件动态添加谓词
            if (name != null && !name.isEmpty()) {
                predicates.add(builder.like(root.get("name"), "%" + name + "%"));
            }

            if (categoryId != null) {
                predicates.add(builder.equal(root.get("category").get("id"), categoryId));
            }

            if (minPrice != null) {
                predicates.add(builder.greaterThanOrEqualTo(root.get("price"), minPrice));
            }

            if (maxPrice != null) {
                predicates.add(builder.lessThanOrEqualTo(root.get("price"), maxPrice));
            }

            if (published != null) {
                predicates.add(builder.equal(root.get("published"), published));
            }

            // 将所有谓词组合为一个AND条件
            if (!predicates.isEmpty()) {
                criteria.where(builder.and(predicates.toArray(new Predicate[0])));
            }

            // 执行查询
            List<Product> result = session.createQuery(criteria).getResultList();
            tx.commit();
            return result;
        } catch (Exception e) {
            tx.rollback();
            throw e;
        }
    }
}

高级条件组合

JPA Criteria API可以处理更复杂的条件组合:

// 复杂条件组合示例
CriteriaBuilder builder = session.getCriteriaBuilder();
CriteriaQuery<Product> criteria = builder.createQuery(Product.class);
Root<Product> root = criteria.from(Product.class);

// OR条件组
Predicate namePredicate = builder.like(root.get("name"), "%" + keyword + "%");
Predicate descPredicate = builder.like(root.get("description"), "%" + keyword + "%");
Predicate keywordPredicate = builder.or(namePredicate, descPredicate);

// AND条件组
Predicate categoryPredicate = builder.equal(root.get("category").get("id"), categoryId);
Predicate pricePredicate = builder.between(root.get("price"), minPrice, maxPrice);
Predicate finalPredicate = builder.and(keywordPredicate, categoryPredicate, pricePredicate);

criteria.where(finalPredicate);

排序与分页

JPA Criteria API同样支持动态排序和分页:

// 添加排序
if ("price".equals(sortField)) {
    criteria.orderBy(ascending ? 
        builder.asc(root.get("price")) : 
        builder.desc(root.get("price")));
} else if ("name".equals(sortField)) {
    criteria.orderBy(ascending ? 
        builder.asc(root.get("name")) : 
        builder.desc(root.get("name")));
}

// 添加分页
List<Product> result = session.createQuery(criteria)
    .setFirstResult(pageIndex * pageSize)
    .setMaxResults(pageSize)
    .getResultList();

优点与不足

JPA Criteria API的优势: - 标准化:作为JPA规范的一部分,具有良好的兼容性 - 类型相对安全:比拼接字符串更安全,能避免SQL注入 - 面向对象:使用Java代码构建查询,无需拼接SQL字符串

不足之处: - 冗长复杂:代码量大,可读性相对较差 - 类型安全不完全:属性名仍使用字符串字面量,容易出现拼写错误 - 学习曲线陡峭:API相对复杂,不够直观 - 调试困难:错误信息通常难以理解 - 难以维护:当查询条件变得复杂时,代码可读性大幅下降

2.5.4 MyBatis的动态SQL

MyBatis采用XML配置文件中的特殊标签来实现动态SQL,这是一种声明式的方法,将SQL逻辑与Java代码分离。

动态SQL标签

MyBatis的动态SQL主要依赖以下标签: - <if>:条件判断 - <choose><when><otherwise>:多条件分支 - <where>:动态WHERE子句,自动处理AND/OR - <set>:动态SET子句,用于更新 - <foreach>:循环处理集合 - <trim>:自定义前缀/后缀处理

基本动态查询示例

以商品搜索为例,MyBatis实现动态查询的方式如下:

<!-- jimmer-in-action-samples/chapter2/mybatis/src/main/resources/mapper/ProductMapper.xml -->
<select id="findByDynamicConditions" resultMap="productResultMap">
    SELECT id, name, price, description, category_id, stock, published, created_time, modified_time
    FROM product
    <where>
        <if test="name != null and name != ''">
            AND name LIKE CONCAT('%', #{name}, '%')
        </if>
        <if test="categoryId != null">
            AND category_id = #{categoryId}
        </if>
        <if test="minPrice != null">
            AND price >= #{minPrice}
        </if>
        <if test="maxPrice != null">
            AND price &lt;= #{maxPrice}
        </if>
        <if test="published != null">
            AND published = #{published}
        </if>
    </where>
</select>

对应的Java接口:

// jimmer-in-action-samples/chapter2/mybatis/src/main/java/org/lijma/mybatis/samples/mapper/ProductMapper.java
List<Product> findByDynamicConditions(
    @Param("name") String name,
    @Param("categoryId") Long categoryId,
    @Param("minPrice") BigDecimal minPrice,
    @Param("maxPrice") BigDecimal maxPrice,
    @Param("published") Boolean published
);

复杂条件组合

MyBatis可以通过嵌套标签处理复杂条件组合:

<select id="findByComplexConditions" resultMap="productResultMap">
    SELECT id, name, price, description, category_id, stock, published, created_time, modified_time
    FROM product
    <where>
        <if test="categoryId != null">
            AND category_id = #{categoryId}
        </if>

        <!-- OR条件组 -->
        <if test="keyword != null and keyword != ''">
            AND (
                name LIKE CONCAT('%', #{keyword}, '%')
                OR description LIKE CONCAT('%', #{keyword}, '%')
            )
        </if>

        <!-- 嵌套条件组 -->
        <if test="minPrice != null or maxPrice != null">
            AND (
                <if test="minPrice != null">
                    price >= #{minPrice}
                </if>
                <if test="minPrice != null and maxPrice != null">
                    AND
                </if>
                <if test="maxPrice != null">
                    price &lt;= #{maxPrice}
                </if>
            )
        </if>
    </where>
</select>

排序与分页

MyBatis也支持动态排序和分页:

<select id="findWithSortAndPaging" resultMap="productResultMap">
    SELECT id, name, price, description, category_id, stock, published, created_time, modified_time
    FROM product
    <where>
        <!-- 动态条件... -->
    </where>

    <!-- 动态排序 -->
    <choose>
        <when test="sortField == 'price' and ascending">
            ORDER BY price ASC
        </when>
        <when test="sortField == 'price' and !ascending">
            ORDER BY price DESC
        </when>
        <when test="sortField == 'name' and ascending">
            ORDER BY name ASC
        </when>
        <when test="sortField == 'name' and !ascending">
            ORDER BY name DESC
        </when>
        <otherwise>
            ORDER BY id DESC
        </otherwise>
    </choose>

    <!-- 分页在Java代码中通过PageHelper实现 -->
</select>

在Java代码中使用PageHelper实现分页:

// 开启分页
PageHelper.startPage(pageIndex, pageSize);

// 执行查询,返回的是Page<Product>对象
List<Product> products = productMapper.findWithSortAndPaging(
    conditions, sortField, ascending);

优点与不足

MyBatis动态SQL的优势: - 直观:SQL语句直接可见,便于SQL优化 - 分离关注点:SQL与Java代码分离,职责清晰 - 强大的模板功能:动态SQL标签功能丰富 - 容易调试:最终执行的SQL可以直接通过日志查看

不足之处: - 字符串拼接:本质上仍是字符串拼接,缺乏类型安全 - XML维护:大型项目中XML文件管理复杂 - 学习额外语法:需要学习特定的XML标签语法 - 编辑器支持有限:相比Java代码,XML的IDE支持较弱 - 重构困难:当实体类变更时,需要手动修改XML

2.5.5 JDBC的动态查询方案

使用JDBC实现动态查询通常需要手动构建SQL字符串和参数列表,这是最底层也是最灵活的方式。

字符串拼接方式

最简单的实现是通过字符串拼接构建SQL:

// jimmer-in-action-samples/chapter2/jdbc/src/main/java/org/lijma/jdbc/samples/repository/ProductRepositoryImpl.java
public List<Product> findByDynamicConditions(
        String name, 
        Long categoryId,
        BigDecimal minPrice, 
        BigDecimal maxPrice,
        Boolean published) {

    StringBuilder sql = new StringBuilder(
        "SELECT id, name, price, description, category_id, stock, published, created_time, modified_time " +
        "FROM product WHERE 1=1 ");

    List<Object> params = new ArrayList<>();

    if (name != null && !name.isEmpty()) {
        sql.append("AND name LIKE ? ");
        params.add("%" + name + "%");
    }

    if (categoryId != null) {
        sql.append("AND category_id = ? ");
        params.add(categoryId);
    }

    if (minPrice != null) {
        sql.append("AND price >= ? ");
        params.add(minPrice);
    }

    if (maxPrice != null) {
        sql.append("AND price <= ? ");
        params.add(maxPrice);
    }

    if (published != null) {
        sql.append("AND published = ? ");
        params.add(published);
    }

    return jdbcTemplate.query(sql.toString(), 
        params.toArray(), 
        new ProductRowMapper());
}

参数化查询构建器

更好的实现是使用专门的查询构建器,如Spring JDBC的JdbcTemplate结合SqlParameterSource

public List<Product> findByDynamicConditionsImproved(
        String name, 
        Long categoryId,
        BigDecimal minPrice, 
        BigDecimal maxPrice,
        Boolean published) {

    MapSqlParameterSource params = new MapSqlParameterSource();
    String baseSql = "SELECT id, name, price, description, category_id, stock, published, " +
                    "created_time, modified_time FROM product WHERE 1=1 ";

    StringBuilder conditions = new StringBuilder();

    if (name != null && !name.isEmpty()) {
        conditions.append("AND name LIKE :name ");
        params.addValue("name", "%" + name + "%");
    }

    if (categoryId != null) {
        conditions.append("AND category_id = :categoryId ");
        params.addValue("categoryId", categoryId);
    }

    if (minPrice != null) {
        conditions.append("AND price >= :minPrice ");
        params.addValue("minPrice", minPrice);
    }

    if (maxPrice != null) {
        conditions.append("AND price <= :maxPrice ");
        params.addValue("maxPrice", maxPrice);
    }

    if (published != null) {
        conditions.append("AND published = :published ");
        params.addValue("published", published);
    }

    String finalSql = baseSql + conditions.toString();

    return namedParameterJdbcTemplate.query(
        finalSql, 
        params, 
        new ProductRowMapper());
}

优点与不足

JDBC动态查询的优势: - 完全控制:对SQL和参数有最大的控制权 - 高度灵活:可以实现任何复杂的查询逻辑 - 无额外依赖:不需要额外的ORM框架 - 直接:直接调用数据库API,没有抽象层的开销

不足之处: - 冗长:需要大量样板代码 - 容易出错:手动字符串拼接容易引入错误 - SQL注入风险:如果实现不当,容易导致SQL注入 - 难以维护:条件复杂时代码变得混乱 - 缺乏类型安全:完全依赖字符串,没有类型检查

2.5.6 动态查询方案对比与选型指南

通过前面的分析,我们可以对各框架的动态查询能力进行综合对比:

特性 Jimmer JPA/Hibernate MyBatis JDBC
类型安全 ★★★★★ ★★★☆☆ ★☆☆☆☆ ☆☆☆☆☆
代码简洁性 ★★★★★ ★★☆☆☆ ★★★★☆ ★☆☆☆☆
API直观性 ★★★★☆ ★★☆☆☆ ★★★★☆ ★★☆☆☆
灵活性 ★★★★☆ ★★★☆☆ ★★★★☆ ★★★★★
学习曲线 ★★★☆☆ ★★☆☆☆ ★★★☆☆ ★★★★☆
性能 ★★★★☆ ★★★☆☆ ★★★★☆ ★★★★★
可维护性 ★★★★★ ★★★☆☆ ★★☆☆☆ ★☆☆☆☆
SQL可见性 ★★★☆☆ ★★☆☆☆ ★★★★★ ★★★★★

方案选型指南

  1. 项目规模与复杂度
  2. 小型项目:MyBatis或JDBC可能足够,实现简单直接
  3. 中型项目:Jimmer或JPA提供更好的抽象和可维护性
  4. 大型项目:Jimmer的类型安全和可维护性优势更为明显

  5. 团队技术背景

  6. 擅长SQL:MyBatis可能更适合
  7. 擅长Java:Jimmer或JPA更合适
  8. 全栈团队:Jimmer提供最佳的开发体验和可维护性

  9. 性能要求

  10. 极致性能:JDBC直接控制或MyBatis
  11. 平衡性能与开发效率:Jimmer
  12. 快速开发原型:任何框架都可以

  13. 查询复杂度

  14. 简单CRUD:任何框架都适用
  15. 复杂动态查询:Jimmer和MyBatis更有优势
  16. 特殊数据库功能:MyBatis或JDBC更直接

最佳实践

无论选择哪种框架,以下是实现高质量动态查询的通用最佳实践:

  1. 参数化查询:始终使用参数化查询而非直接拼接值,避免SQL注入
  2. 业务逻辑分离:将查询条件的构建逻辑与业务逻辑分离
  3. 分页处理:总是实现分页功能,避免大结果集影响性能
  4. 性能监控:监控生成的SQL和执行计划,确保高效查询
  5. 条件预处理:对用户输入的查询条件进行预处理和验证
  6. 模块化:将复杂查询分解为小的、可重用的组件
  7. 适当抽象:创建适当的抽象层,避免重复代码

小结

本节我们深入探讨了动态查询的实现方式,从Jimmer的类型安全API,到JPA的Criteria查询,再到MyBatis的XML标签和JDBC的手动拼接。每种方案都有其独特的优势和适用场景。

Jimmer凭借其类型安全、直观的API和卓越的开发体验,代表了现代ORM框架的发展方向。它成功地在类型安全、代码简洁性和性能之间取得了平衡,特别适合构建复杂的企业应用。

然而,动态查询仅解决了数据获取的一半问题。在现代应用架构中,特别是前后端分离的设计模式下,如何将查询结果高效地转换为API响应格式,如何设计灵活的数据传输对象(DTO)以满足不同场景的需求,同样是重要的挑战。

传统框架通常要求开发者手动创建大量DTO类和转换逻辑,而现代ORM框架,特别是Jimmer,提供了更高效的解决方案。在下一节中,我们将探讨数据传输对象的设计策略,重点关注Jimmer提供的动态DTO生成能力,以及如何将这些技术应用到RESTful API的构建中,实现更灵活、高效的前后端数据交互。

2.6 数据传输对象(DTO)设计与实现

在现代企业应用开发中,数据传输对象(Data Transfer Object, DTO)是连接领域模型与外部接口的关键桥梁。DTO模式的核心思想是通过专门设计的数据结构,在系统边界之间传递数据,而不直接暴露内部领域模型。然而,传统DTO的实现往往导致大量重复代码和繁琐的转换逻辑,这正是现代ORM框架尝试解决的问题。

本节将深入探讨数据传输对象的设计策略,重点分析Jimmer提供的创新性解决方案,并与传统框架的实现方式进行对比,揭示现代ORM框架如何简化和优化DTO处理。

2.6.1 DTO模式的挑战与价值

DTO的核心价值

数据传输对象作为一种设计模式,提供了多方面的价值:

  1. 关注点分离:将展示层需求与领域模型解耦,使每一层专注于自己的职责
  2. 数据保护:避免直接暴露敏感数据和内部实现细节
  3. 性能优化:只传输必要的数据,减少网络负载
  4. 版本兼容性:支持API版本演进,而不影响内部模型
  5. 定制化视图:为不同场景提供最适合的数据结构

传统DTO实现的挑战

然而,传统的DTO实现方式存在诸多痛点:

  1. 大量模板代码:为每个视图创建专门的DTO类,导致代码膨胀
  2. 繁琐的映射逻辑:在实体和DTO之间编写重复的转换代码
  3. 类型安全缺失:映射过程中的错误直到运行时才能发现
  4. 难以维护:当实体模型变化时,需要同步更新多个DTO类
  5. 关联处理复杂:处理嵌套对象和集合关系时代码冗长复杂

这些挑战促使我们重新思考DTO的实现方式,特别是在微服务和响应式系统中,客户端对灵活数据结构的需求日益增长,传统的静态DTO模式显得力不从心。

2.6.2 Jimmer的DTO解决方案

Jimmer通过其创新的设计,提供了两种主要的DTO处理方式:1) 基于接口和注解的DTO定义;2) 专用的DTO语言。这两种方式都基于编译时代码生成和类型安全的DSL API,大幅简化了DTO的创建和使用。

DTO处理的核心概念

Jimmer的DTO处理基于以下核心概念:

  1. 实体视图:将DTO定义为实体的一种"视图",指定包含哪些属性
  2. 输入/输出区分:明确区分用于数据读取和创建/修改的DTO
  3. 声明式定义:使用类型安全的DSL或专用语言定义DTO结构
  4. 编译时生成:基于定义自动生成DTO实现代码
  5. 动态投影:运行时根据需求动态选择属性

基于接口的DTO定义

以商品实体为例,使用Jimmer基于接口定义DTO的方式如下:

// 定义一个商品列表视图DTO
@Dto
public interface ProductListItemDto {
    @Nullable
    Long id();

    String name();

    BigDecimal price();

    @Nullable
    @ManyToOne
    CategoryView category();

    // 分类的嵌套DTO定义
    @Dto
    interface CategoryView {
        Long id();
        String name();
    }
}

使用DTO语言(.dto文件)

除了使用Java/Kotlin接口定义DTO外,Jimmer还提供了专用的DTO语言,可以在.dto文件中定义DTO:

// ProductDto.dto文件
export org.example.entity.Product

ProductListView {
    id
    name
    price
    category {
        id
        name
    }
}

input ProductInput {
    name!
    price!
    stock
    description
    categoryId
}

DTO语言的优势在于语法更加简洁,专注于DTO结构的定义,同时支持三种DTO类型: - 视图(view):用于输出数据,默认类型 - 输入(input):用于接收客户端提交的数据 - 规格(specification):用于支持高级查询条件

无论使用哪种方式定义,使用DTO的方式都非常简单:

// 执行查询,自动将实体投影为DTO
List<ProductListView> products = sqlClient
    .createQuery(ProductTable.$)
    .select(ProductTable.$.fetch(ProductListView.class))
    .execute();

这种方式的优势立即可见: - 类型安全:所有属性引用都是类型安全的 - 代码简洁:无需手动编写转换逻辑 - 自动关联处理:嵌套DTO自动处理关联关系 - 按需加载:只查询和加载DTO中定义的属性

输入与输出DTO

Jimmer明确区分了输入DTO和输出DTO的概念:

// 输入DTO - 用于创建新商品
@Dto
public interface ProductInputDto {
    String name();
    BigDecimal price();
    int stock();
    String description();
    Long categoryId();
}

// 输出DTO - 用于详细视图
@Dto
public interface ProductDetailDto {
    long id();
    String name();
    BigDecimal price();
    int stock();
    String description();
    LocalDateTime createdTime();

    @ManyToOne
    CategoryDto category();

    @OneToMany(mappedBy = "product")
    List<ReviewDto> reviews();
}

这种声明式定义极大地提高了代码的可读性和可维护性。值得注意的是,Jimmer认为在大多数情况下,输出DTO可以通过动态实体和Fetcher API解决,不一定需要专门定义输出DTO类型。

2.6.3 运行时动态投影

Jimmer的一个独特优势是支持运行时动态投影,开发者可以根据请求参数动态决定返回哪些字段,这在构建灵活的API时非常有价值。

定义动态Fetcher

使用Jimmer的Fetcher API可以动态定义数据结构:

// 根据客户端请求动态构建数据结构
public Product findProductWithDynamicFetcher(long id, String fields) {
    // 解析客户端请求的字段
    Set<String> requestedFields = new HashSet<>(Arrays.asList(fields.split(",")));

    // 动态构建Fetcher
    ProductFetcher fetcher = ProductFetcher.$.allScalarFields();

    if (requestedFields.contains("category")) {
        fetcher = fetcher.category(CategoryFetcher.$.allScalarFields());
    }

    if (requestedFields.contains("reviews")) {
        fetcher = fetcher.reviews(
            ReviewFetcher.$.allScalarFields()
                .user(UserFetcher.$.name().avatar())
        );
    }

    // 执行查询
    return sqlClient
        .findById(fetcher, id)
        .orElseThrow(() -> new EntityNotFoundException("Product not found: " + id));
}

这种方式支持客户端指定需要的字段,类似GraphQL的能力,但实现更加简洁。

自动DTO转换

Jimmer还支持将实体自动转换为任意DTO类型:

// 将实体转换为指定DTO
ProductDetailDto detailDto = sqlClient
    .findById(Product.class, id)
    .orElseThrow(() -> new EntityNotFoundException("Product not found"))
    .fetchAs(ProductDetailDto.class);

// 将集合转换为指定DTO列表
List<ProductListItemDto> listDtos = products.stream()
    .map(product -> product.fetchAs(ProductListItemDto.class))
    .collect(Collectors.toList());

这种灵活性使开发者能够轻松应对各种复杂的业务场景。

2.6.4 与传统DTO方案对比

为了更全面地理解Jimmer的DTO处理能力,我们将其与传统框架的实现方式进行对比。

JPA/Hibernate的DTO处理

在JPA/Hibernate中,处理DTO通常有以下几种方式:

  1. 手动映射:最常见但也最繁琐的方式
// 手动创建DTO类
public class ProductDto {
    private Long id;
    private String name;
    private BigDecimal price;
    private CategoryDto category;

    // 构造器、getter和setter...
}

// 手动映射逻辑
public List<ProductDto> findAllProducts() {
    List<Product> products = productRepository.findAll();
    return products.stream()
        .map(product -> {
            ProductDto dto = new ProductDto();
            dto.setId(product.getId());
            dto.setName(product.getName());
            dto.setPrice(product.getPrice());

            if (product.getCategory() != null) {
                CategoryDto categoryDto = new CategoryDto();
                categoryDto.setId(product.getCategory().getId());
                categoryDto.setName(product.getCategory().getName());
                dto.setCategory(categoryDto);
            }

            return dto;
        })
        .collect(Collectors.toList());
}
  1. 使用JPQL构造器表达式:减少一些映射代码,但限制较多
@Query("SELECT new com.example.dto.ProductSummaryDto(" +
       "p.id, p.name, p.price, c.name) " +
       "FROM Product p JOIN p.category c")
List<ProductSummaryDto> findAllProductSummaries();
  1. 借助工具库:如MapStruct或ModelMapper
// 使用MapStruct定义映射
@Mapper
public interface ProductMapper {
    ProductDto toDto(Product product);
    List<ProductDto> toDtoList(List<Product> products);
}

// 使用映射器
@Service
public class ProductService {
    private final ProductRepository repository;
    private final ProductMapper mapper;

    public List<ProductDto> findAll() {
        return mapper.toDtoList(repository.findAll());
    }
}

与Jimmer相比,JPA方案的不足之处: - 手动维护:需要手动维护DTO类和映射逻辑 - 编译时安全性较弱:字符串表达式不提供编译时检查 - 级联映射复杂:处理嵌套关系需要显式编码 - 按需加载困难:难以针对特定DTO优化查询

MyBatis的DTO处理

MyBatis处理DTO通常依赖XML映射:

<!-- 直接映射到DTO -->
<select id="findAllProductDtos" resultType="com.example.dto.ProductDto">
    SELECT 
        p.id, p.name, p.price,
        c.id as "category.id", c.name as "category.name"
    FROM product p
    LEFT JOIN category c ON p.category_id = c.id
</select>

<!-- 或使用ResultMap -->
<resultMap id="productDtoMap" type="com.example.dto.ProductDto">
    <id property="id" column="id"/>
    <result property="name" column="name"/>
    <result property="price" column="price"/>
    <association property="category" javaType="com.example.dto.CategoryDto">
        <id property="id" column="category_id"/>
        <result property="name" column="category_name"/>
    </association>
</resultMap>

MyBatis的优势在于SQL的灵活性,但也有明显缺点: - XML维护负担:需要维护大量XML映射 - 类型安全缺失:XML中的属性名没有编译时检查 - 重构困难:修改DTO结构需要同步更新XML - 动态性受限:难以实现真正的运行时动态投影

2.6.5 Jimmer DTO的高级特性

除了基本的DTO能力,Jimmer还提供了许多高级特性,进一步增强了DTO的灵活性和功能性。

多层次DTO定制

Jimmer允许为一个实体定义多种层次的DTO视图:

// 最小视图 - 仅包含ID和名称
@Dto
public interface ProductMiniDto {
    long id();
    String name();
}

// 列表视图 - 包含基本信息
@Dto
public interface ProductListDto extends ProductMiniDto {
    BigDecimal price();
    CategoryMiniDto category();
}

// 详情视图 - 包含完整信息
@Dto
public interface ProductDetailDto extends ProductListDto {
    int stock();
    String description();
    boolean isPublished();
    List<ReviewDto> reviews();
}

这种方式支持根据不同场景需求返回不同粒度的数据,同时保持代码的一致性和可复用性。

计算属性

Jimmer DTO支持添加实体中不存在的计算属性:

@Dto
public interface ProductDetailDto {
    // 基本属性...

    // 计算属性:折扣价格
    @DtoComputed("price * 0.9")
    BigDecimal discountPrice();

    // 更复杂的计算属性
    @DtoComputed
    default String fullName() {
        return String.format("%s (%s)", name(), category() != null ? category().name() : "Uncategorized");
    }
}

计算属性极大地增强了DTO的表达能力,可以直接在DTO层处理一些简单的业务逻辑。

条件性包含

Jimmer支持根据条件动态包含属性:

@Dto
public interface ProductDto {
    long id();
    String name();
    BigDecimal price();

    // 仅当用户有ADMIN角色时包含成本信息
    @DtoAdvice(when = @When(role = "ADMIN"))
    BigDecimal cost();

    // 仅当产品已发布时包含评论
    @DtoAdvice(when = @When(prop = "isPublished"))
    List<ReviewDto> reviews();
}

这种声明式的访问控制简化了权限管理,确保只向授权用户暴露敏感数据。

2.6.6 灵活的数据获取API

Jimmer的DTO能力与数据获取API的结合,可以提供极其灵活的数据获取体验,让客户端能够精确指定所需的数据结构,从而优化网络传输和后端处理。

自定义字段选择API

对于需要高度灵活性的场景,可以实现类似GraphQL的字段选择功能:

public Product findWithDynamicFields(long id, String fields) {
    // 解析字段并构建动态Fetcher
    ProductFetcher fetcher = createFetcherFromFields(fields);

    // 执行查询
    return sqlClient
        .findById(fetcher, id)
        .orElseThrow(() -> new EntityNotFoundException("Product not found: " + id));
}

private ProductFetcher createFetcherFromFields(String fields) {
    ProductFetcher fetcher = ProductFetcher.$;
    Set<String> fieldSet = new HashSet<>(Arrays.asList(fields.split(",")));

    // 基本字段
    if (fieldSet.contains("id")) fetcher = fetcher.id();
    if (fieldSet.contains("name")) fetcher = fetcher.name();
    if (fieldSet.contains("price")) fetcher = fetcher.price();
    if (fieldSet.contains("stock")) fetcher = fetcher.stock();
    if (fieldSet.contains("description")) fetcher = fetcher.description();

    // 关联字段
    if (fieldSet.contains("category")) {
        fetcher = fetcher.category(CategoryFetcher.$.allScalarFields());
    }
    if (fieldSet.contains("reviews")) {
        fetcher = fetcher.reviews(ReviewFetcher.$.allScalarFields());
    }

    return fetcher;
}

这种实现使客户端能够精确指定需要的数据,优化网络传输和服务器处理。

集成到服务层的示例

在服务层中,可以轻松实现动态字段选择:

public class ProductService {

    private final JSqlClient sqlClient;

    public ProductService(JSqlClient sqlClient) {
        this.sqlClient = sqlClient;
    }

    public List<Object> findAllWithDynamicFields(
            ProductFilter filter, 
            String fields, 
            int page, 
            int size) {

        // 1. 构建动态查询条件
        ProductTable table = ProductTable.$;
        Specification<Product> spec = buildSpecification(filter);

        // 2. 构建动态Fetcher
        ProductFetcher fetcher = createFetcherFromFields(fields);

        // 3. 执行查询并按需获取字段
        return sqlClient.createQuery(table)
            .where(spec)
            .limit(page, size)
            .select(table.fetch(fetcher))
            .execute();
    }

    // 其他实现方法...
}

与其他框架的对比

相比于其他框架的数据获取方式,Jimmer的动态字段选择具有显著优势:

框架 字段选择方式 优缺点
Jimmer Fetcher API + DTO投影 ✅ 类型安全
✅ 按需加载
✅ 关联自动处理
⚠️ 需要预定义Fetcher
GraphQL 查询DSL ✅ 客户端完全控制
✅ 内省能力
⚠️ 额外的架构层
⚠️ 性能优化复杂
REST+JPA 自定义接口或字段参数 ⚠️ 通常需要多个端点
⚠️ N+1问题常见
❌ 缺乏类型安全性
Spring Data REST 投影 + 参数 ✅ 约定优于配置
⚠️ 有限的定制能力
❌ 关联处理不灵活

Jimmer的方案在保持类型安全的同时,提供了接近GraphQL的灵活性,且没有引入额外的架构复杂性。

安全考虑

在实现动态字段选择API时,需要注意以下安全考虑:

  1. 字段访问控制:根据用户权限限制可访问的字段
  2. 深度限制:防止过深的关联查询导致性能问题
  3. 数量限制:限制返回的记录数量和关联集合大小
  4. 输入验证:验证客户端请求的字段名是否有效

Jimmer的条件性包含特性(@DtoAdvice)可以帮助实现基于角色的字段访问控制,确保敏感数据的安全。

2.6.7 DTO最佳实践与设计指南

基于我们的讨论,总结出以下DTO设计的最佳实践:

  1. 分层设计
  2. 区分输入DTO(创建/更新)和输出DTO(查询)
  3. 根据场景创建不同粒度的DTO视图
  4. 使用继承复用常见字段定义

  5. 性能优化

  6. 只包含真正需要的字段
  7. 通过动态Fetcher精确控制加载的数据
  8. 合理设置默认关联加载深度

  9. 安全考虑

  10. 使用条件性包含保护敏感字段
  11. 在DTO层过滤敏感信息,而非依赖视图层
  12. 区分内部DTO和外部API DTO

  13. 可维护性

  14. 保持DTO定义靠近实体定义
  15. 为复杂计算属性添加清晰注释
  16. 使用一致的命名约定(例如后缀)

  17. 测试策略

  18. 测试DTO转换逻辑
  19. 验证条件性包含是否正确工作
  20. 确保计算属性返回预期结果

小结

本节我们深入探讨了数据传输对象的设计与实现,重点关注了Jimmer提供的创新性DTO解决方案。相比传统框架的繁琐DTO处理方式,Jimmer凭借其动态DTO生成能力和灵活的查询能力,大幅简化了开发流程,同时提供了更高的灵活性和类型安全性。

值得注意的是,Jimmer对DTO的处理采取了更为完整的方法:

  1. 对于输出DTO,Jimmer认为通过动态实体和Fetcher API已经能解决大部分问题,通常不需要定义专门的输出DTO类型。只有在特殊情况下,才需要使用DTO语言或注解来定义专用的输出视图。

  2. 对于输入DTO,则确实难以避免。因此,Jimmer提供了DTO语言和注解两种机制,使创建和维护输入DTO变得极其简单,同时保持了高度的类型安全性。

Jimmer的DTO处理不仅解决了传统DTO模式的痛点,还通过动态投影、计算属性、条件包含等高级特性,为现代应用开发提供了强大支持。这种方式特别适合构建前后端分离的应用,能够轻松应对不同客户端的多样化数据需求。

此外,Jimmer还提供了DTO语言作为替代方案,开发者可以使用专门的.dto文件来定义DTO,通过更为简洁的语法实现DTO定义,进一步提高开发效率。

当然,DTO只是构建完整应用的一部分。在实际项目中,我们需要将实体模型、关联关系、数据操作和API设计等多个方面综合考虑,形成一个统一的解决方案。

在下一节中,我们将回顾第二章的全部内容,总结Jimmer与传统ORM框架在各个方面的差异和优势,并探讨如何根据不同的项目需求选择合适的ORM技术。通过这个综合回顾,我们将对本章介绍的各种概念和技术有更加全面和深入的理解,为后续章节的学习打下坚实基础。

2.7 章节总结:现代ORM的新视角

回顾这一章的旅程,你是否感到一种醍醐灌顶的体验?从实体建模、基础数据操作、关联数据处理到动态查询和DTO设计,我们不仅全面对比了各种持久化技术,更见证了数据访问思想的革命性转变。Jimmer并不仅仅是"另一个ORM框架",而是代表了一种全新的数据访问范式。

2.7.1 持久化技术的范式革命

想象你正驾驶一辆汽车。传统的ORM框架就像一辆手动档车:你需要关注换挡时机、离合器控制,甚至担心引擎转速;而Jimmer则像一辆现代的电动车:你只需专注于方向和目的地,车辆自动处理一切复杂的底层控制。这种根本性的转变体现在四个关键维度:

  1. 从命令式到声明式

传统方式中,开发者需要详细指定"如何"获取数据:

// JPA的命令式查询
CriteriaBuilder cb = entityManager.getCriteriaBuilder();
CriteriaQuery<Book> query = cb.createQuery(Book.class);
Root<Book> root = query.from(Book.class);
query.select(root).where(cb.equal(root.get("name"), "Java编程"));
List<Book> books = entityManager.createQuery(query).getResultList();

而在Jimmer中,你只需声明"要什么"数据:

// Jimmer的声明式查询
List<Book> books = sqlClient
    .createQuery(BookTable.$)
    .where(BookTable.$.name().eq("Java编程"))
    .select(BookTable.$)
    .execute();

这就像是从"手工烹饪"(控制每一个步骤)转变为"点菜式烹饪"(只需告诉厨师你想要什么菜)。声明式风格不仅代码更简洁,更重要的是它将"意图"和"实现"清晰分离,让框架有更大空间进行优化。

  1. 从可变到不可变

传统ORM的可变对象模型像是一个随时可能被修改的共享白板,任何人都可能改变其状态:

// 传统可变对象 - 状态可能在任何地方被改变
Book book = bookRepository.findById(1L).get();
book.setName("新书名"); // 直接修改原对象

// 在复杂应用中,很难追踪是谁、何时、为何修改了对象
// 并发环境下更可能导致不可预期的行为

而Jimmer的不可变模型则如同版本控制系统,每次修改都创建新的"版本",原对象永远不变:

// Jimmer不可变对象 - 修改总是返回新对象
Book book = sqlClient.findById(Book.class, 1L);
Book newBook = book.withName("新书名"); // 原对象不变,返回新对象

// 可以安全地在多个组件间传递对象,无需担心意外修改
// 线程安全,无需额外同步措施

这种设计模式在高并发场景中特别有价值,它消除了数据竞争,简化了状态管理,就像一把"无需锁的锁"。

  1. 从隐式到显式

传统ORM中的延迟加载机制常常导致意外的性能问题:

// JPA中的隐式加载 - N+1问题的温床
@Entity
public class Book {
    @ManyToMany(fetch = FetchType.LAZY)
    private List<Author> authors;
}

// 看似无害的代码可能触发大量额外查询
List<Book> books = bookRepository.findAll();
for (Book book : books) {
    // 每循环一次都可能触发额外的SQL查询!
    System.out.println(book.getAuthors().size());
}

Jimmer则强制开发者显式声明需要加载的数据:

// Jimmer的显式加载 - 预先定义数据形状
BookFetcher fetcher = BookFetcher.$
    .allScalarFields()
    .authors(AuthorFetcher.$.name());

List<Book> books = sqlClient
    .createQuery(BookTable.$)
    .select(BookTable.$.fetch(fetcher))
    .execute();

// 所有数据一次性加载完成,不会有额外查询

这就像是在旅行前规划行程表,而不是走到哪算到哪。显式声明避免了意外的性能陷阱,同时提供了更可预测的行为。

  1. 从运行时到编译时

传统框架依赖运行时反射和字符串参数,错误只能在运行时被发现:

// MyBatis的XML查询 - 运行时才知道是否有错
<select id="findByName" resultType="Book">
    SELECT * FROM book WHERE name = #{name}
</select>

// 如果列名或表名有变化,只有运行时才会发现错误

Jimmer将大量检查前移到编译时,利用强类型API捕获潜在问题:

// Jimmer的类型安全查询 - 编译时检查
List<Book> books = sqlClient
    .createQuery(BookTable.$)
    .where(BookTable.$.name().eq(name)) // 属性错误将导致编译失败
    .select(BookTable.$)
    .execute();

这就像是从"试错式编程"转变为"证明式编程",编译通过即可大幅增强对代码正确性的信心。

这四个维度的变革不是孤立的,它们相互呼应、相互强化,共同构成了Jimmer革命性的设计理念。正如从手工作坊到现代工厂的转变,这种范式转换重新定义了我们与数据交互的方式。

2.7.2 Jimmer的核心优势:实际场景解析

让我们走进几个真实场景,看看Jimmer如何解决开发者日常面临的挑战:

场景一:电商平台的商品详情页

电商平台的商品详情页需要整合来自多个表的数据:商品基本信息、分类、品牌、规格、评论等。

传统解决方案的困境

使用JPA/Hibernate,你可能会面临艰难的选择: - 方案A:使用延迟加载,结果在展示页面时触发大量额外查询(N+1问题) - 方案B:使用EAGER加载,但可能获取过多不需要的数据,浪费资源 - 方案C:编写复杂的JPQL或Criteria查询,代码冗长且难以维护

在MyBatis中,你需要编写复杂的联表查询:

<select id="getProductDetail" resultMap="productDetailResultMap">
  SELECT p.*, c.name as category_name, b.name as brand_name,
         r.rating, r.content as review_content, ...
  FROM product p
  LEFT JOIN category c ON p.category_id = c.id
  LEFT JOIN brand b ON p.brand_id = b.id
  LEFT JOIN review r ON p.id = r.product_id
  WHERE p.id = #{id}
</select>
这样的查询不仅复杂,而且难以动态调整——如果某个页面不需要评论数据,你可能需要再写一个不同的查询。

Jimmer的优雅解决方案

在Jimmer中,你可以为不同场景精确定义数据加载形状:

// 商品详情页的数据形状
ProductFetcher detailFetcher = ProductFetcher.$
    .allScalarFields()
    .category(CategoryFetcher.$.name())
    .brand(BrandFetcher.$.name().logo())
    .specs(SpecFetcher.$.name().value())
    .reviews(ReviewFetcher.$.rating().content().author(UserFetcher.$.name()));

// 使用定义好的形状获取数据
Product product = sqlClient
    .findById(detailFetcher, id)
    .orElseThrow(() -> new ProductNotFoundException(id));

这种方式的优势显而易见: 1. 按需加载:精确控制加载哪些数据,不多不少 2. 一次查询:Jimmer自动优化为一次或最少次数的SQL查询 3. 代码清晰:数据结构直观可见,易于理解和维护 4. 可复用:Fetcher可以模块化组合,适应不同场景

就像是定制西装vs成衣,Jimmer让每次查询都能完美匹配业务需求。

场景二:灵活的搜索条件

电商平台的商品搜索功能需要支持多种筛选条件:名称、价格区间、品牌、分类、评分等,这些条件全部是可选的。

传统解决方案的困境

在JDBC中,你可能需要手动拼接SQL字符串:

StringBuilder sql = new StringBuilder("SELECT * FROM product WHERE 1=1");
List<Object> params = new ArrayList<>();

if (name != null) {
    sql.append(" AND name LIKE ?");
    params.add("%" + name + "%");
}
if (minPrice != null) {
    sql.append(" AND price >= ?");
    params.add(minPrice);
}
// 更多条件...

// 容易出错的字符串操作,且难以维护

在MyBatis中,情况略好一些,但仍需在XML中编写大量动态SQL标签:

<select id="searchProducts" resultType="Product">
    SELECT * FROM product
    WHERE 1=1
    <if test="name != null">
        AND name LIKE CONCAT('%', #{name}, '%')
    </if>
    <if test="minPrice != null">
        AND price >= #{minPrice}
    </if>
    <!-- 更多条件... -->
</select>
这种方式虽然功能完备,但缺乏编译时检查,重构时也容易出错。

Jimmer的优雅解决方案

Jimmer提供了类型安全的动态查询构建API:

List<Product> products = sqlClient
    .createQuery(ProductTable.$)
    .where(table -> {
        List<Predicate> predicates = new ArrayList<>();

        if (name != null) {
            predicates.add(table.name().like("%" + name + "%"));
        }
        if (minPrice != null) {
            predicates.add(table.price().ge(minPrice));
        }
        if (categoryId != null) {
            predicates.add(table.category().id().eq(categoryId));
        }
        // 更多条件...

        return Predicate.and(predicates);
    })
    .select(ProductTable.$)
    .execute();

这种实现方式的优势在于: 1. 类型安全:IDE可提供自动完成,重构也会自动应用于查询代码 2. 编译时检查:属性名或类型错误在编译时就能发现 3. 代码即文档:查询条件清晰可见,无需切换到XML文件 4. 强大表达能力:可以构建任意复杂的条件组合

Jimmer的动态查询就像是给SQL添加了强类型系统,让错误无处可藏。

场景三:数据传输层设计

电商平台需要为前端和API消费者提供不同格式的数据视图。例如,商品列表页需要简要信息,而详情页需要完整数据。

传统解决方案的困境

使用手动DTO转换是常见做法,但代码冗长且易错:

// 需要为每种DTO编写转换逻辑
public ProductListDTO toListDTO(Product product) {
    ProductListDTO dto = new ProductListDTO();
    dto.setId(product.getId());
    dto.setName(product.getName());
    dto.setPrice(product.getPrice());
    dto.setImageUrl(product.getMainImage());
    // 更多字段...
    return dto;
}

public ProductDetailDTO toDetailDTO(Product product) {
    ProductDetailDTO dto = new ProductDetailDTO();
    // 重复的基本字段映射
    dto.setId(product.getId());
    dto.setName(product.getName());
    // 更多字段...

    // 关联对象映射
    if (product.getCategory() != null) {
        CategoryDTO categoryDTO = new CategoryDTO();
        categoryDTO.setId(product.getCategory().getId());
        categoryDTO.setName(product.getCategory().getName());
        dto.setCategory(categoryDTO);
    }
    // 更多复杂映射...
    return dto;
}

这种手动转换既浪费时间,又增加了维护负担——每次实体类变化都需要同步修改多个转换方法。

Jimmer的优雅解决方案

Jimmer提供了多种DTO定义和自动转换方式:

接口式DTO:

// 简单声明式定义
@Dto
public interface ProductListDTO {
    long id();
    String name();
    BigDecimal price();
    String mainImage();
}

// 包含关联的复杂DTO
@Dto
public interface ProductDetailDTO {
    long id();
    String name();
    BigDecimal price();
    String description();

    @ManyToOne
    CategoryDTO category();

    @OneToMany
    List<SpecificationDTO> specifications();
}

// 自动转换
ProductListDTO listDTO = product.fetchAs(ProductListDTO.class);
ProductDetailDTO detailDTO = product.fetchAs(ProductDetailDTO.class);

专用DTO语言(更灵活的方式):

// ProductDTOs.dto
input ProductInput {
    String name
    BigDecimal price
    long categoryId
    -long id
}

ProductBriefView {
    id
    name
    price
    mainImage
}

ProductDetailView {
    id
    name
    price
    description
    category {
        id
        name
    }
    specifications {
        id
        name
        value
    }
}

这种方式的优势无需赘述: 1. 声明式定义:只需声明结构,无需手写转换逻辑 2. 自动优化查询:Jimmer根据DTO结构优化数据获取 3. 类型安全:编译时检查确保DTO结构与实体兼容 4. 易于维护:实体变化时,编译器会提示需要更新的DTO

Jimmer的DTO机制就像是从手工裁剪布料制作衣服,转变为直接通过3D打印定制成品——节省时间,提高精确度,还能轻松适应变化。

2.7.3 框架选型的整体思考

回顾本章对四种技术的全面对比,我们可以从系统角度总结它们的适用场景:

框架 代表特性 最佳应用场景 挑战
JDBC 完全控制、性能稳定 性能极限场景、通用接口层 代码量大、可维护性差
MyBatis SQL可见、半自动化 复杂报表查询、遗留系统集成 对象映射有限、动态SQL繁琐
JPA/Hibernate 完整ORM、JPA标准 传统企业应用、领域驱动设计 N+1问题、性能调优复杂
Jimmer 不可变对象、动态查询、编译时安全 复杂数据结构、高并发系统、API密集型应用 新框架生态、学习曲线

框架选型不仅是技术问题,更是战略决策。选择Jimmer就像选择电动汽车——前期可能有适应成本,但长期来看将获得更好的性能和用户体验。当你的应用面临以下挑战时,Jimmer尤其值得考虑:

  1. 复杂数据关系:当你的领域模型包含大量一对多、多对多关系,尤其是树形或图形结构
  2. API灵活性需求:当你需要为相同数据提供多种视图,且视图结构经常变化
  3. 并发和扩展性考量:当系统需要支持高并发访问,且对数据一致性有严格要求
  4. 长期可维护性:当你希望降低技术债务,提高代码质量和团队效率

2.7.4 实战路径与最佳实践

基于本章的学习和案例分析,这里提供一套Jimmer实战的最佳实践指南:

  1. 领域模型设计
  2. 使用接口定义实体,专注于表达业务概念和关系
  3. 避免过度规范化,适当冗余可以简化查询和提高性能
  4. 利用编译时注解表达约束,如@NotNull、@Min、@Max等
  5. 考虑使用软删除(@LogicalDeleted)而非物理删除

  6. 查询优化策略

  7. 为频繁使用的查询场景定义可复用的Fetcher
  8. 使用批量处理API避免循环查询(如newExecutor().findByIds或batchUpdate)
  9. 在查询条件中充分利用索引列,避免过度使用like '%xxx%'等非索引友好操作
  10. 对大结果集使用分页API,并考虑keyset分页优化滚动加载场景

  11. 事务与并发控制

  12. 尽量使用乐观锁(@Version)而非悲观锁,提高并发性
  13. 合理设置事务边界,避免过长事务
  14. 对高频访问数据考虑使用Jimmer的缓存支持
  15. 使用immutable的特性简化多线程环境下的对象共享

  16. 系统集成考量

  17. 与Spring Boot无缝集成,利用自动配置简化设置
  18. 在微服务架构中,考虑使用Jimmer的远程关联能力简化服务间数据聚合
  19. 对接前端时充分利用动态DTO,减少接口数量和版本变更
  20. 使用Jimmer的代码生成工具自动生成TypeScript类型,实现前后端类型统一

2.7.5 未来展望与技术趋势

当我们站在Java持久化技术的演进路线上,Jimmer代表了一种面向未来的解决方案。以下趋势值得关注:

  1. 函数式设计与不可变性:函数式编程的思想正在影响整个行业,Jimmer的不可变设计完美契合这一趋势。

  2. GraphQL式数据获取:按需获取的数据形状定义在GraphQL中得到广泛应用,Jimmer的Fetcher机制提供了类似能力,但无需额外的GraphQL层。

  3. 编译时增强与代码生成:从运行时反射到编译时处理的转变可提升性能和安全性,Jimmer在这方面走在前列。

  4. 全栈类型安全:前后端类型统一是提升开发效率的关键,Jimmer的自动类型生成为此提供了基础。

  5. AI辅助开发:未来,AI助手可能会帮助开发者优化查询和数据模型,而Jimmer的声明式API和丰富元数据为此类工具提供了良好基础。

2.7.6 你的Jimmer之旅:从这里起航

如果你正在阅读这些文字,相信你已经对Jimmer有了相当深入的了解。与其他任何强大工具一样,掌握Jimmer需要实践与探索。以下是开启你的Jimmer之旅的建议:

  1. 渐进式采用:不必急于全面改造现有系统。选择一个新功能或子模块,尝试用Jimmer实现。这种增量式的方法可以降低风险,同时累积实战经验。

  2. 构建原型:创建一个简单的原型项目,复现本章中的示例,亲身体验Jimmer解决各类问题的方式。正如学习游泳必须下水一样,真正理解Jimmer的优势需要亲自动手。

  3. 加入社区:Jimmer拥有活跃的开发者社区,通过GitHub、讨论组和社交媒体,你可以获取最新信息、分享经验并解决问题。记住,每个专家都曾是初学者。

  4. 思维转变:充分发挥Jimmer的优势需要调整思维方式——从命令式到声明式,从可变到不可变。这种转变虽然需要时间,但回报丰厚。

  5. 持续学习:技术永远在发展,保持学习的热情。关注Jimmer的更新,尝试新特性,不断完善你的技术栈。

想象一下,当你成功应用Jimmer构建了第一个功能,体验到代码量的显著减少、类型安全带来的信心、以及性能的自然提升,那种成就感是无与伦比的。更令人兴奋的是,随着项目规模增长,Jimmer的优势会变得更加明显——传统方案中线性增长的复杂度,在Jimmer中可能只是温和的上升。

Jimmer不仅是工具,更是一种思想的转变,它代表了数据访问技术的新范式。就像从纸质地图过渡到GPS导航一样,一旦适应了新方式,你会惊讶于之前为何要忍受那么多不必要的复杂性。

你已经掌握了知识,现在是时候将它转化为能力。当你创建第一个Jimmer项目,编写第一个实体接口,定义第一个Fetcher,构建第一个类型安全的动态查询时,你不仅在学习一个框架,更是在拥抱软件开发的未来。

开始你的Jimmer之旅吧!在下一章,我们将深入探索Jimmer的更多高级特性,帮助你进一步掌握这个强大工具的全部潜力。但现在,最重要的是迈出第一步——因为每一次伟大的技术飞跃,都始于简单的"Hello World"。