Skip to content

MyBatis 配置设计原则(会话工厂与 Mapper) ​

本文档只讲 ORM 层:SqlSessionFactory、SqlSessionTemplate、TypeHandler 多聚合装配与 Mapper XML 加载。数据源与事务管理器见 MySQL 配置设计原则;MyBatis 模块内部实现(枚举 / JSON / 集合三通道)见 MyBatis 集成。

1. 职责边界 ​

  • MyBatisConfig(本文):产 SqlSessionFactory 与 SqlSessionTemplate,聚合各聚合的 TypeHandlerContext,并把框架固定 Mapper与业务 Mapper 一并纳入扫描。
  • MySqlConfig(见 MySQL 配置设计原则):只产 DataSource 与 PlatformTransactionManager。
  • 聚合专属 {Agg}MybatisTypeHandlerConfig:每个聚合一个,只登记本聚合的枚举与值对象,落在 infrastructure/config/{agg}/。

依赖方向单向:MyBatisConfig → MySqlConfig。MyBatisConfig 的 sqlSessionFactory 以 DataSource 为入参,Spring 按类型注入,与类名无关。

2. SqlSessionFactory 构建 ​

构建要点集中在三处:原生 Configuration 注入 TypeHandler、全局开关、Mapper XML 加载策略。

2.1 原生 Configuration 对象,而不是 configLocation ​

不使用 setConfigLocation 加载 mybatis-config.xml 来装配 TypeHandler,而是创建原生 Configuration 对象,在构建阶段把各聚合 TypeHandlerContext 逐个 registerInto,再通过 setConfiguration 注入会话工厂。

java
@Bean
public SqlSessionFactory sqlSessionFactory(DataSource dataSource,
                                           List<TypeHandlerContext> typeHandlerContexts) throws Exception {
    org.apache.ibatis.session.Configuration configuration = new org.apache.ibatis.session.Configuration();

    // 各聚合 TypeHandler 必须在 XML 解析前灌入
    typeHandlerContexts.forEach(context -> context.registerInto(configuration));

    configuration.setMapUnderscoreToCamelCase(true);
    configuration.setCacheEnabled(true);
    configuration.setLazyLoadingEnabled(true);
    configuration.setAggressiveLazyLoading(false);
    configuration.setLogImpl(Slf4jImpl.class);

    SqlSessionFactoryBean sessionFactory = new SqlSessionFactoryBean();
    sessionFactory.setDataSource(dataSource);
    sessionFactory.setConfiguration(configuration);
    sessionFactory.setMapperLocations(loadMapperLocations());
    return sessionFactory.getObject();
}

setConfiguration 与 setConfigLocation 互斥,同时设置会抛 IllegalStateException。既然 TypeHandler 必须通过原生 Configuration 注入,就不要再用 mybatis-config.xml。

2.2 全局开关 ​

开关值说明
mapUnderscoreToCamelCasetrue下划线列 → 驼峰属性自动映射
cacheEnabledtrue开启全局缓存
lazyLoadingEnabledtrue开启延迟加载(支撑子集合懒加载)
aggressiveLazyLoadingfalse按需加载,不激进触发
logImplSlf4jImpl.class统一走 SLF4J 日志

2.3 TypeHandler 多聚合装配 ​

入参必须是 List<TypeHandlerContext>:每个聚合各产出一个 TypeHandlerContext Bean(如 orderTypeHandlerContext),声明为 List<T> 时 Spring 会把这些 Bean 全部收集注入。若声明为单个 TypeHandlerContext,多聚合时会因无法确定注入哪一个而抛 NoUniqueBeanDefinitionException。

  • 单聚合:列表只有一个元素,行为不变;
  • 多聚合:自动收集全部并逐个注册,新增聚合只需新增一个 TypeHandlerContext Bean,MyBatisConfig 零改动;
  • 零聚合:注入空列表、循环空转,不报错。

为什么必须手动装配而非包扫描:UniversalEnumTypeHandler、GenericJsonTypeHandler、ListTypeHandler 都是运行时按类型动态构建的(泛型类需针对每个值对象 new 实例,集合处理器需运行期装配查表配置),包扫描只能发现"类存在",无法生成对应实例。

单点来源:枚举单列通道与 JSON 通道必须共用同一 resolver / serializer / enumRules,否则同一枚举在两处解析结果不一致。括号细节见 MyBatis 集成 §2。

空列表不报错,但 Mapper XML 解析期缺 handler 会抛 BuilderException。单独装配测试时,@Import 必须同时带上 MySqlConfig、MyBatisConfig 与对应聚合的 TypeHandler 配置。

3. 纯 XML Mapper 加载 ​

3.1 不使用 @MapperScan ​

本框架的 Mapper 走传统纯 XML 直调:不使用 @MapperScan,不在 Java 中持有 Mapper 接口类,仓储按 namespace.statementId 通过 sqlSession.insert/update/select/delete 调用语句,与 仓储设计原则 的纯 XML 约定一致。

3.2 必须使用 classpath*: ​

java
private Resource[] loadMapperLocations() throws IOException {
    PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
    return Stream.of(
                    resolver.getResources("classpath*:io/pragmatic/ddd/mybatis/outbox/*.xml"),
                    resolver.getResources("classpath*:io/pragmatic/ddd/mybatis/id/*.xml"),
                    resolver.getResources("classpath*:io/pragmatic/ddd/mybatis/compensation/*.xml"),
                    resolver.getResources("classpath*:mapper/**/*.xml"))
            .flatMap(Arrays::stream)
            .toArray(Resource[]::new);
}
  • 必须用 classpath*:(带星号):单 classpath: 只能在单个 classpath 根内匹配,跨 jar 时扫不到 pragmatic-ddd-mybatis 依赖内的框架 XML。
  • 不要只扫 mapper/**:框架 XML 的物理路径是 io/pragmatic/ddd/mybatis/...,与业务 mapper/ 并列,mapper/** 命中不了。
  • 框架 Mapper 建议无条件注册:MyBatis 解析 XML 不校验表是否存在,多注册安全;漏注册只会在对应功能首次调用时暴露。

3.3 框架固定 Mapper 清单 ​

pragmatic-ddd-mybatis 内置三组与业务无关的 Mapper,必须由使用方的 SqlSessionFactory 扫描加载:

能力Mapper XML(jar 内)表namespace(固定)
Outboxio/pragmatic/ddd/mybatis/outbox/OutboxMapper.xmloutbox_messageio.pragmatic.ddd.mybatis.outbox.OutboxMapper
补偿(durable)io/pragmatic/ddd/mybatis/compensation/CompensationMapper.xmlcompensation_logio.pragmatic.ddd.mybatis.compensation.CompensationMapper
号段 IDio/pragmatic/ddd/mybatis/id/IdSegmentMapper.xmlid_segmentio.pragmatic.ddd.mybatis.id.IdSegmentMapper

对应的语句常量类:OutboxStatements / CompensationStatements / IdSegmentStatements(与 XML 的 namespace.statementId 一一对应)。

触发矩阵:

使用的能力必须注册必须建表缺失症状
Outbox(OutboxCommandExecutor / OutboxRelay)outbox/*.xmloutbox_message启动正常,首发事件抛 Mapped Statements collection does not contain value for ...OutboxMapper.insertBatch
补偿 durable(pragmatic-ddd.compensation.mode=durable)compensation/*.xmlcompensation_log同上,...CompensationMapper.insert
DB 号段 ID(IdGeneratorDefinition / DbSegmentAllocator)id/*.xmlid_segment(含渠道初始行)取号时 ...IdSegmentMapper.selectForUpdate 找不到
纯内存补偿 / 不用 Outbox / 用 Redis 号段不需要不需要—

注意:pragmatic-ddd-spring-boot 的 DddOutboxAutoConfiguration / DddCompensationAutoConfiguration / DddIdGeneratorAutoConfiguration 只做 Bean 装配(以容器内 SqlSessionTemplate 为前置条件),不负责加载 Mapper XML。starter 不能替代本节配置。

PostgreSQL 方言:三组 Mapper 均提供 PG 版 XML,目录为 io/pragmatic/ddd/mybatis/{outbox,compensation,id}/postgres/*.xml(namespace 与 MySQL 版相同,每条语句带 databaseId="postgres")。使用 PG 时有三条硬约束:

  1. 扫描 glob 换成 {outbox,compensation,id}/postgres/*.xml,只扫一种方言;
  2. 必须为 SqlSessionFactory 设置 databaseId(Spring 用 VendorDatabaseIdProvider,原生用 configuration.setDatabaseId("postgres")),否则方言语句全部不注册;
  3. 可空参数逐个显式 jdbcType,否则 PG 报 could not determine data type of parameter $n。

完整装配骨架、PG 建表脚本、JSON 列策略与语句改写见 PostgreSQL 配置设计原则。

3.4 框架表建表脚本 ​

建表脚本同样在 jar 内,按所用能力取用:

text
io/pragmatic/ddd/mybatis/outbox/schema/outbox-schema-mysql.sql
io/pragmatic/ddd/mybatis/compensation/schema/compensation-log-schema-mysql.sql
io/pragmatic/ddd/mybatis/id/schema/id-segment-schema-mysql.sql

PostgreSQL 版路径为同目录 *-postgres.sql,类型映射与建表约束见 PostgreSQL 配置设计原则 §5。

三张表的索引要点:

  • outbox_message:主键 id;(status, created_at) 支撑 Relay 认领;claim_token 支撑批量取回。
  • compensation_log:主键 action_key;(status, updated_at) 支撑补偿轮询。
  • id_segment:主键 biz_key;建表脚本不含种子数据,使用前需为每个渠道插入初始行。

4. SqlSessionTemplate ​

SqlSessionTemplate 是 Spring 托管的线程安全 SqlSession,绑定上述工厂,供仓储实现(doInsert / doUpdate / doRemove)直接操作 MyBatis,并自动参与到 Spring 声明式事务中。

java
@Bean
public SqlSessionTemplate sqlSessionTemplate(SqlSessionFactory sqlSessionFactory) {
    return new SqlSessionTemplate(sqlSessionFactory);
}

5. 常见反模式 ​

反模式问题正确做法
用 mybatis-config.xml + setConfigLocation 装配 TypeHandler与 setConfiguration 互斥,装配分散难审查原生 Configuration 统一注入后 setConfiguration
sqlSessionFactory 用单个 TypeHandlerContext 入参多聚合时抛 NoUniqueBeanDefinitionException入参用 List<TypeHandlerContext>
用包扫描装配复杂 TypeHandler泛型/动态构建的处理器无法被发现TypeHandlerContext.registerInto 在构建阶段手动注入
只扫 classpath:mapper/**/*.xml跨 jar 扫不到框架 XML,Outbox/号段功能运行期报绑定异常用 classpath*: 并显式列出三个框架目录
认为 starter 会自动注册框架 Mapper自动配置只装配 Bean,不加载 XML在 MyBatisConfig 显式扫描框架 XML
只 @Import(MySqlConfig.class) 做单测缺 SqlSessionTemplate,或 Mapper XML 解析期缺 handler同时导入 MySqlConfig + MyBatisConfig + 聚合 TypeHandler 配置
枚举单列与 JSON 通道配置不一致同一枚举两处解析结果不同三者共用同一 resolver / serializer / enumRules
集合 TypeHandler 遇到多表同名列启动期抛 IllegalStateException用 SQL AS 别名隔离列类型
MySQL 用错 JSON 方言写 JSON 列失败MySQL 用 JdbcJsonValue.MYSQL,PG 用 PgJdbcJsonValue
PG 场景仍套用 MySQL 装配扫错方言 XML、未设 databaseId 或可空参数漏 jdbcType,运行期才报错按 PostgreSQL 配置设计原则 装配

下一步 ​