news 2026/11/2 20:56:56

Doma框架常见问题排查:编译期SQL校验与工程实践

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Doma框架常见问题排查:编译期SQL校验与工程实践

接手一个用Doma框架维护的存量项目时,最先撞上的往往不是业务逻辑,而是满屏的编译期报错:SQL文件找不到、绑定变量对不上、生成的DAO实现类飘红。Doma这套Java数据库访问框架的思路很特别,它把SQL从Java代码里彻底抽出来放进独立的SQL文件,再靠注解处理器在编译阶段就把SQL和方法签名对齐,等于把很多原本要跑到运行期才暴露的问题提前到了构建期。这个设计对喜欢把SQL攥在自己手里的团队非常友好,但代价是项目结构、编译配置、IDE联动都得按它的规矩来,一步没踩对就是一连串的"为什么编译不过"。这篇内容整理了我在多个项目里反复遇到的Doma常见问题,从环境搭建、SQL文件路径、实体映射、动态SQL到事务和批量处理,每个问题都给出现象、原因和可复现的处理方式,适合刚接手Doma项目的新人,也适合用了两三年但一直没系统梳理过踩坑点的老手。

1. Doma框架的定位与核心设计再认识

1.1 为什么选Doma:编译期SQL校验的取舍

很多人第一次接触Doma,会拿它和MyBatis、JPA做对比。我的理解是,Doma走的是"SQL完全可控 + 编译期强校验"这条中间路线。MyBatis把SQL写在XML或注解里,灵活但拼写错误、参数名不匹配往往要等到运行时才报错;JPA把SQL藏起来,开发效率高但对复杂查询的控制力弱。Doma的做法是:SQL写在独立的.sql文件里,DAO接口用注解声明方法,注解处理器在编译时读取SQL文件,逐条检查绑定变量是否和方法参数对得上、返回类型是否能映射到结果集。这就意味着一个拼错的列别名、一个少写的#{},在mvn compile阶段就会失败,而不是等到测试环境跑接口时才炸。

这个取舍带来的好处很直接:SQL review变得可行,代码评审时能直接看.sql文件;重构方法参数名时,编译器会逼着你同步改SQL;团队里不熟悉ORM黑盒的人也能一眼看懂数据访问层在干什么。代价同样明显:构建流程多了一步注解处理,IDE必须正确开启注解处理器,SQL文件的位置和命名有硬性约定,任何一环没配好,报错信息往往不够直白。我在实际项目里见过最典型的情况是,本地Windows开发一切正常,推到Linux构建机就报SQL文件找不到,原因仅仅是大写字母的文件名在大小写敏感的机器上对不上。

所以选Doma之前要想清楚一件事:团队愿不愿意为"SQL可控"付出构建配置上的学习成本。如果项目里复杂查询多、DBA要求SQL必须可审计,Doma很合适;如果项目以简单CRUD为主、追求开箱即用,JPA或MyBatis可能更省心。这不是框架优劣问题,是团队工作方式的匹配问题。

1.2 注解处理器的工作机制与项目结构约束

Doma的核心机制是Java的注解处理器(Annotation Processor)。你在DAO接口上标@Dao,在方法上标@Select、@Insert这些注解,编译时doma-processor会扫描这些接口,生成一个名为XxxDaoImpl的实现类,同时读取对应的SQL文件做校验。生成的实现类默认落在target/generated-sources/annotations目录下,和手写代码一起参与编译。理解这一点很关键,因为后面遇到的"找不到DaoImpl""改动SQL不生效""IDE飘红但命令行能编译"这些问题,根源几乎都和注解处理器的执行时机或输出目录有关。

项目结构上有几个硬约束必须记住。第一,DAO接口必须是接口而不是抽象类,方法返回值、参数类型要符合Doma的约定。第二,SQL文件的默认位置是和DAO接口相同的包路径下,文件名格式为DAO接口简单名_方法名.sql,比如com.example.dao.EmployeeDao的selectById方法,对应com/example/dao/EmployeeDao_selectById.sql。第三,实体类用@Entity标注,字段访问还是属性访问要统一,默认是字段访问,这意味着实体不一定需要getter/setter,但需要无参构造。第四,Config实现类要通过ServiceLoader机制注册,否则运行时会抛配置找不到的异常。

这些约束单看都不难,组合在一起就容易出问题。比如多模块项目里,DAO接口在dao模块,SQL文件却被打包进了resources模块,编译期处理器在dao模块的classpath里找不到SQL文件,直接报错。再比如实体用了Lombok的@Data,字段访问模式下没问题,但一旦项目里有人切到属性访问,Lombok生成的getter命名和Doma预期的属性名对不上,映射就静默失败。我在一个项目里就吃过这个亏:实体字段叫userName,数据库列叫user_name,默认驼峰转下划线本来能对上,但有人加了@Accessors(chain = true),属性访问模式下Doma拿到的属性名带了链式返回类型的影响,映射结果直接为null,排查了半天才定位到访问模式。

1.3 版本与运行环境的对应关系

Doma 2.x是目前主流版本,基于javax命名空间,JDK 8及以上都能跑。如果项目要迁移到Jakarta EE 9+,需要留意Doma对应的适配版本,包名从javax切到jakarta不是改个依赖版本就完事,注解处理器的行为、Spring Boot starter的版本都要一起对齐。下面这张表是我整理的环境对应关系,实际选版本时建议先对照一遍,避免依赖冲突。

项目要素常见选择注意事项
Doma版本2.5x系列2.x基于javax,迁移Jakarta需确认适配版本
JDK8 / 11 / 1717下注解处理器需确认处理器兼容性
构建工具Maven 3.6+ / Gradle 7+注解处理器配置方式不同
Spring Boot集成doma-spring-boot-starter版本要与Spring Boot主版本匹配
数据库方言MysqlDialect / PostgresDialect / OracleDialect方言决定分页、引号、类型映射行为
连接池HikariCPDoma本身不管连接池,交给数据源

这张表看着简单,但每一项错配都可能引发连锁问题。我见过最隐蔽的一次是方言选错:项目连的是PostgreSQL,Config里却写了MysqlDialect,简单的增删改查完全正常,一到分页查询就报语法错误,因为两种方言生成分页SQL的方式不同。这类问题不会在启动时报错,只会在特定SQL上暴露,排查成本很高。

2. 环境搭建与依赖配置的常见坑

2.1 依赖版本与注解处理器配置

Maven项目里,Doma需要两个依赖:运行时依赖doma和编译期处理器doma-processor。很多人只加了前者,结果编译时根本不生成DaoImpl,运行时抛ClassNotFoundException。正确做法是在maven-compiler-plugin的annotationProcessorPaths里显式声明处理器,而不是简单地把doma-processor加进普通依赖。普通依赖的方式在某些Maven版本和IDE组合下会失效,尤其是多模块项目。

<plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <configuration> <annotationProcessorPaths> <path> <groupId>org.seasar.doma</groupId> <artifactId>doma-processor</artifactId> <version>2.53.0</version> </path> </annotationProcessorPaths> <compilerArgs> <arg>-parameters</arg> </compilerArgs> </configuration> </plugin>

上面这段配置里有两个关键点。第一,annotationProcessorPaths保证处理器只在编译期生效,不会被打进最终产物。第二,-parameters编译参数必须加,否则方法参数名在字节码里会丢失,Doma在处理SQL绑定变量时拿不到参数名,轻则报错,重则生成的代码里参数绑定错位。这个参数在Java 8之后默认是关闭的,很多人不知道要开,结果就是"本地idea能跑、命令行编译失败"的怪现象。

Gradle项目的写法不同,用annotationProcessor配置:

dependencies { implementation 'org.seasar.doma:doma-core:2.53.0' annotationProcessor 'org.seasar.doma:doma-processor:2.53.0' }

Gradle里同样要确保-parameters开启,可以在compileJava任务里加options.compilerArgs << '-parameters'。我建议把这段配置写进构建脚本的公共部分,而不是靠每个开发者本地配,否则新人拉代码第一件事就是编译失败。

注意:doma-processor的版本必须和doma-core保持一致,版本错配时处理器生成的代码可能引用不存在的方法,报错信息通常指向生成类而不是配置本身,很容易误判。

2.2 编译期生成目录与IDE联动问题

注解处理器生成的代码默认在target/generated-sources/annotations,Maven会自动把这个目录加进编译路径,但IDE不一定认。IntelliJ IDEA里常见的情况是:命令行mvn compile成功,IDE里XxxDaoImpl一片飘红。解决方式有两种,一是开启IDE的注解处理设置,在Settings > Build > Compiler > Annotation Processors里勾选启用,并确认处理器路径;二是直接执行一次mvn clean compile后重新导入Maven项目,让IDE识别生成目录。我个人的习惯是每次拉新项目先跑一遍命令行编译,确认生成目录有内容,再让IDE重新索引,比在IDE里瞎点设置高效得多。

另一个高频问题是"改了SQL文件不生效"。Doma的SQL文件是在编译期被读取和校验的,但运行期加载的是classpath里的副本。如果你只改了源码目录下的.sql文件而没有重新编译,或者构建工具没有把.sql当作资源复制到target/classes,运行时就还是旧SQL。Maven默认只把src/main/resources下的文件当资源,如果SQL文件和DAO接口一起放在src/main/java下,需要在pom.xml里配置resources包含**/*.sql。这个坑我在三个项目里都遇到过,表现是"SQL明明改了,日志里打印的还是旧的",查半天以为是缓存,其实是资源没复制。

<build> <resources> <resource> <directory>src/main/java</directory> <includes> <include>**/*.sql</include> </includes> </resource> <resource> <directory>src/main/resources</directory> </resource> </resources> </build>

上面这段配置的意思是,把src/main/java下的.sql文件也当作资源复制到输出目录,同时保留src/main/resources的正常资源处理。配置完记得mvn clean一次,避免旧的class文件残留造成误判。

2.3 多模块项目中的处理器传递

多模块项目是Doma配置问题的重灾区。假设项目分common、dao、web三个模块,实体在common,DAO接口和SQL在dao,web依赖dao。常见错误是把注解处理器配置写在父POM的pluginManagement里,以为子模块会自动继承,实际上annotationProcessorPaths不会自动传递,每个需要生成代码的模块都得单独声明。更隐蔽的情况是,dao模块依赖common模块的实体,但common模块编译时没有开启注解处理,实体上的@Entity、@Column没有被处理,到了dao模块编译时处理器读取实体元数据就可能缺信息。

我的处理原则是:哪个模块有@Dao接口,哪个模块就配注解处理器;实体模块如果只放POJO,不需要配处理器,但要确保实体类本身编译正常。跨模块的SQL文件路径问题也要注意,SQL文件必须和DAO接口在同一个模块的相同包路径下,不能分散到别的模块的资源目录里,否则编译期找不到。曾经有个项目为了"资源统一管理",把所有SQL文件集中放到web模块的resources/sql下,结果dao模块编译时全部报SQL文件缺失,最后只能把SQL文件挪回dao模块。

3. SQL文件与注解处理器的高频报错排查

3.1 SQL文件路径与命名约定

SQL文件找不到是Doma最常见的一类报错。表现通常是编译失败,提示某个DAO方法对应的SQL文件不存在。根因几乎都出在路径或命名上,我总结了几条排查顺序。第一,确认SQL文件和DAO接口在同一个包下,目录结构要和包名完全一致。第二,确认文件名格式是接口简单名_方法名.sql,下划线不能少,大小写要和接口名、方法名严格一致。第三,确认构建时.sql被复制到了输出目录,尤其是SQL放在src/main/java下的项目。

大小写问题值得单独说。Windows文件系统不区分大小写,Linux区分,所以EmployeeDao_selectbyid.sql在Windows上可能被当成EmployeeDao_selectById.sql,到了Linux构建机就找不到。我现在的习惯是文件名一律用IDE的重命名功能生成,不手敲,避免大小写笔误。另一个容易忽略的点是方法重载。Doma对方法重载的支持有限,两个同名方法如果参数不同,SQL文件名相同会冲突,通常的做法是改用不同方法名,而不是硬扛重载。

如果项目确实需要把SQL文件放到非默认位置,可以用@SqlFile注解显式指定路径。这个注解在Doma 2.1之后提供,适合把SQL集中管理在META-INF下的场景。但我不建议一上来就用,默认约定能解决大部分问题,自定义路径反而增加了"路径写错但编译不报错"的风险。

3.2 注解处理器报错信息逐条解读

Doma的报错信息有固定的前缀和编号,看懂编号能少走很多弯路。虽然不同版本编号略有差异,但大类是稳定的。下面这张表是我根据实际排查经验整理的常见报错方向和对应处理方式。

报错方向典型现象排查重点
SQL文件缺失编译提示某方法找不到SQL文件包路径、文件名大小写、资源复制配置
绑定变量不匹配提示SQL中的变量在方法参数中不存在-parameters是否开启、参数名拼写、@Parameter注解
返回类型不匹配提示结果集无法映射到返回类型实体字段与列名映射、@Column配置、构造函数
主键配置错误更新或删除时提示缺少主键实体是否有@Id、是否标在正确字段上
方言相关错误编译通过但运行时报SQL语法错Config中方言是否与数据库匹配

"绑定变量不匹配"这一类值得展开。Doma的SQL里用/* paramName */#{paramName}或者直接#{paramName}引用方法参数。如果编译时没开-parameters,处理器拿不到参数名,就会认为SQL里的变量不存在。还有一种情况是参数用了@Parameter注解重命名,SQL里却还在用原变量名。我的建议是:方法参数名和SQL变量名保持完全一致,不要玩重命名,减少心智负担;如果确实需要重命名,在参数上标@Parameter("newName"),并全局搜一遍SQL里的引用。

"返回类型不匹配"通常和实体映射有关。比如SQL查了user_name列,实体字段叫userName,默认驼峰转下划线能对上;但如果实体上标了@Accessors(accessType = AccessType.PROPERTY)切到属性访问,而getter命名不规范,映射就会失败。这类问题编译期不一定报,运行时才表现为字段为null,排查时优先看实体的访问模式和列名注解。

3.3 内联SQL与SQL文件的选择

Doma允许在@Select等注解里直接写sql属性,也就是内联SQL,适合非常简单的查询。比如按主键查一条记录,写内联SQL能省掉一个文件。但我不建议在业务项目里大量用内联SQL,原因有三个:一是内联SQL在Java字符串里,格式化和review都不方便;二是复杂SQL拼接可读性差;三是团队规范不统一时,一半人在文件里写、一半人在注解里写,维护成本翻倍。我的做法是,超过三行的SQL一律放文件,内联只用于极简的场景,并且在团队规范里写清楚边界。

内联SQL还有一个坑:字符串里的#{}和Java的转义容易混。比如SQL里要写\n或者引号,Java字符串得转义,稍不注意就编译不过。SQL文件没有这个问题,写起来更接近原生SQL。所以从长期维护角度,SQL文件是更稳妥的选择。

4. 实体映射与类型转换的疑难问题

4.1 命名策略与列名映射

Doma默认的命名策略是驼峰转下划线,实体字段userName对应数据库列user_name。这个默认值覆盖了大部分规范建表的场景,但现实项目里总有例外:有的表用全大写列名,有的带业务前缀,有的历史遗留用拼音。处理方式是用@Column(name = "实际列名")逐个覆盖,或者在Config里配置全局命名策略。逐个覆盖更可控,全局策略改错影响面太大,我倾向于前者。

@Table注解用于指定表名,如果实体名和表名不一致就必须标。这里有个细节:@Table支持catalog和schema属性,多schema的数据库里要写清楚,否则生成的SQL会缺schema前缀,运行时报表不存在。我在一个多租户项目里就遇到过,代码里@Table(name = "orders"),实际表在tenant_a.orders,没配schema导致查询全部失败,而且报错信息是数据库层的"表不存在",很容易误以为是连接配错。

主键映射用@Id,更新和删除依赖它。复合主键就在多个字段上标@Id。这里常见的错误是把@Id标在了getter上而不是字段上,字段访问模式下处理器读不到,更新时生成的条件缺主键,变成全表更新。这个错误极其危险,测试环境数据量小可能没感觉,生产环境一次全表更新就是事故。我的习惯是实体一律用字段访问,@Id紧贴字段声明,评审时重点看。

4.2 常见类型转换异常与自定义转换器

类型转换是Doma使用中第二大类问题。数据库的CHAR(n)定长类型在读取时会补空格,如果实体字段是String,拿到的值尾部带空格,比较和展示都会出问题。处理方式是在SQL里用TRIM,或者自定义Domain做清洗。TIMESTAMP和LocalDateTime的映射在新版本JDBC驱动下基本没问题,但老驱动可能需要显式注册类型处理器。布尔字段如果数据库用char(1)存Y/N,Doma默认的布尔映射对不上,需要自定义转换。

枚举映射用@Enum注解,可以指定按序号还是按名称存。按序号存的风险是枚举顺序一变,历史数据全错位,所以我一般要求按名称存。自定义类型用@Domain注解配合ExternalDomain或者实现转换逻辑,适合金额、身份证号这类有业务含义的值对象。下面是一个把char(1)的Y/N转成布尔值的简化思路:

@Domain(valueType = String.class, factoryMethod = "of") public class YesNoBoolean { private final String value; private YesNoBoolean(String value) { this.value = value; } public static YesNoBoolean of(String value) { return new YesNoBoolean(value); } public String getValue() { return value; } public boolean toBoolean() { return "Y".equalsIgnoreCase(value); } }

实体字段用这个Domain类型,数据库列还是char(1),读写时由Doma调用of和getValue完成转换。这种写法比在业务代码里到处写"Y".equals(...)干净得多,也把转换逻辑收口到了一处。需要注意的是,@Domain的factoryMethod必须是静态方法,参数类型要和数据库列类型对得上,否则运行时报转换失败。

提示:金额字段不要用double,用BigDecimal并配合@Column指定精度。Doma不会替你做精度校验,数据库列是decimal(10,2)而Java侧传了三位小数时,不同数据库的舍入行为不一致,容易在测试环境看不出、生产环境对不上账。

5. 动态SQL与条件构建的实操要点

5.1 动态SQL语法与常见误用

Doma的动态SQL用注释语法,/*%if condition */ ... /*%end*/、/*%for item : list */ ... /*%end*/,和MyBatis的标签风格完全不同。第一次用的人最容易把/*%if*/写成<if>,编译器不报错,运行时SQL里原样带着注释,条件永远不生效。我在代码评审里见过好几次这种写法,表现是"条件查询不生效,永远查全表",排查时看日志里的SQL才发现注释没被解析。

动态SQL的if条件里引用的是方法参数名,判断null用param != null,判断字符串空值要写param != null and param != ''。这里有个细节:Doma的条件表达式是有限制的,不是完整的Java表达式,复杂逻辑建议在Java侧算好布尔值再传进来,SQL里只做简单判断。比如多条件组合查询,可以先在Service层拼一个condition对象,SQL里判断对象字段是否为null,而不是在SQL里写一长串and。

for循环用于IN查询和批量条件。写法是/*%for id : idList */ #{id} /*%if id_has_next */,/*%end*/ /*%end*/,注意id_has_next是Doma提供的循环变量,用来判断是否还有下一个元素,少了它就会多一个逗号导致SQL语法错误。这个错误很常见,报错信息指向SQL语法,实际根因在循环写法。

5.2 条件分支与参数绑定陷阱

动态SQL里最容易出安全问题的是${}和#{}的混用。#{}是预编译绑定,安全;${}是直接字符串拼接,有注入风险。Doma里${}一般用于拼接表名、列名这类不能预编译的部分,业务参数绝对不能用。我见过有人为了"动态排序"把前端传来的排序字段直接${}拼进去,这就是典型的注入漏洞。正确做法是后端维护一个允许的排序字段白名单,前端传枚举值,后端映射到固定的列名再拼接。

另一个陷阱是if分支里参数绑定的完整性。比如更新语句:

UPDATE employee SET /*%if name != null */ name = #{name}, /*%end*/ /*%if age != null */ age = #{age}, /*%end*/ updated_at = CURRENT_TIMESTAMP WHERE id = #{id}

如果name和age都为null,生成的SQL里SET后面直接跟updated_at,语法没问题;但如果只有第一个条件成立,name = #{name},后面的逗号还在,也没问题。真正的问题出在最后一项:如果最后一个条件分支也带逗号,而后面没有其他字段,就会生成SET age = ?, , updated_at = ...这种双逗号。处理方式是把逗号放在字段前面而不是后面,或者用/*%if*/包住整个赋值段。这种细节不实测很难发现,建议每个动态更新SQL都写单元测试覆盖"全部参数为null"和"只有最后一个参数不为null"两种边界。

5.3 LIKE查询与特殊字符转义

LIKE查询的转义问题在Doma里没有内置处理。用户输入%或_时,如果直接拼进LIKE #{keyword},会被当作通配符,导致查询结果不符合预期。处理方式有两种:一是在SQL里用ESCAPE子句,Java侧把%、_、\转义;二是在Java侧拼好%keyword%再传参,同时做转义。我倾向于后者,逻辑集中在Service层,SQL保持干净。

public String escapeLike(String keyword) { if (keyword == null) { return null; } return keyword.replace("\\", "\\\\") .replace("%", "\\%") .replace("_", "\\_"); }

SQL里写LIKE CONCAT('%', #{keyword}, '%') ESCAPE '\'。这样用户输入的%会被当作普通字符匹配。这个细节在后台管理系统的搜索框里特别重要,不处理的话用户搜%会把全表查出来,既是性能问题也是体验问题。

6. 事务、连接池与批量处理的稳定性问题

6.1 事务边界与连接管理

Doma本身不管理连接池,它只负责从DataSource拿连接、执行SQL、管理事务边界。事务通过TransactionManager控制,手动模式下用LocalTransactionManager,Spring集成下用DomaTransactionManager。常见问题是事务边界没包住多条SQL,导致部分成功部分失败。比如先插主表再插子表,两条SQL分别在不同事务里执行,主表成功子表失败就产生脏数据。正确做法是把它们放进同一个TransactionTemplate或者Spring的@Transactional方法里。

连接泄漏是另一个高频问题。Doma的LocalTransaction如果忘记commit或rollback,连接不会归还连接池,跑一段时间后连接池耗尽,报"等待连接超时"。我排查过的一次是异常分支里直接return,没有走finally里的rollback,结果每次异常都泄漏一个连接。现在我的习惯是用try-with-resources或者TransactionTemplate,把提交和回滚交给框架,手写try-catch-finally容易漏。

嵌套事务也要注意。Doma的LocalTransactionManager不支持真正的嵌套事务,内层commit会把外层也提交掉,容易造成"以为回滚了实际提交了"的误判。需要嵌套语义时,要么用Spring的传播行为控制,要么把逻辑合并到一个事务方法里。这一点在复杂业务里必须提前想清楚,不能等出问题再改。

6.2 批量插入与性能调优

批量操作是Doma里坑最多的部分。@BatchInsert返回BatchResult,而不是直接抛异常。这意味着批量插入时如果某条记录违反唯一约束,方法正常返回,异常被吞进BatchResult里,不检查就以为全部成功。我见过最严重的一次是批量导入十万条数据,实际只成功了三万条,因为主键冲突的记录被静默跳过,业务方直到对账才发现。

BatchResult<Employee> result = employeeDao.batchInsert(employees); if (result.isError()) { RuntimeException e = result.getException(); // 记录日志,定位是哪条数据、什么原因 throw e; }

上面这段是必须写的防御代码。BatchResult还提供getUpdatedCounts(),可以拿到每条的成功状态,适合需要精确知道哪条失败导入的场景。批量大小也要调,不是越大越好。批量太大容易撑爆内存或者触发数据库的包大小限制,我一般在500到1000之间做压测,找到当前数据库和网络环境下的拐点。Doma的批量大小可以通过注解属性或者Config配置,具体看版本,配置后务必用真实数据量压一次,别拍脑袋。

批量插入的自增主键回填也要注意。数据库和驱动支持getGeneratedKeys时,Doma会把生成的主键回填到实体里,但批量模式下部分驱动行为不一致。如果业务依赖回填的主键做后续关联插入,建议先小批量验证,确认实体里的主键字段确实被赋值了,再放大批量。这个验证花不了几分钟,能避免后面一连串的空指针。

6.3 乐观锁与并发更新

@Version注解用于乐观锁,更新时自动带上版本号条件,影响行数为0时抛乐观锁异常。常见问题是实体忘了标@Version,或者版本字段类型和数据库对不上(比如Java用int,数据库用bigint)。还有一种情况是更新语句手写了SQL,没有带版本条件,绕过了乐观锁,并发下后写覆盖先写。我的建议是,凡是需要并发控制的表,实体必须标@Version,并且更新一律走Doma生成的条件更新,不手写绕过。

乐观锁异常的处理也要想清楚。捕获后是重试、提示用户还是记录冲突,取决于业务。重试要注意次数上限和幂等性,不能无限重试。我一般设置最多重试两次,超过就返回明确的冲突提示,让用户刷新后重试,比后台死循环重试更可控。

7. 常见异常速查与排查方法论

7.1 异常速查表

把前面散落的问题汇总成一张速查表,方便出问题时快速定位。这张表按"现象—可能原因—处理方向"组织,实际排查时从现象出发逐行对照,比从头读文档快得多。

现象可能原因处理方向
编译报SQL文件找不到路径、文件名大小写、资源未复制核对包路径与命名,检查resources配置
编译报绑定变量不存在未开-parameters、参数名不一致加编译参数,核对SQL变量名
运行时DaoImpl找不到注解处理器未生效检查annotationProcessorPaths配置
查询结果字段全为null命名策略不匹配、访问模式不一致核对@Column与实体访问模式
更新影响全表@Id缺失或标错位置检查实体主键注解
批量插入部分失败无感知未检查BatchResult增加异常检查与日志
连接池耗尽事务未提交/回滚导致连接泄漏检查事务边界与异常分支
分页SQL语法错误方言配置与数据库不匹配核对Config中的Dialect
乐观锁异常频发并发更新同一记录评估重试策略与业务冲突处理

表格里的每一行背后都是一类真实问题,但排查时不能只盯一行。比如"更新影响全表"可能是@Id缺失,也可能是动态SQL的WHERE条件被if吞掉了,要结合SQL日志一起看。我习惯在开发环境把SQL日志级别开到最详细,每次执行都打印最终SQL和参数,很多问题看一眼日志就能定位,比断点调试快。

7.2 日志配置与排查技巧

Doma的SQL日志通过Config里的JdbcLogger和SqlLogType控制。开发环境建议用SqlLogType.FORMATTED,打印格式化后的SQL和绑定参数,直观;生产环境用RAW或者关闭,避免日志量过大。日志实现可以接SLF4J,把Doma的日志输出到项目统一的日志框架里,方便按DAO方法名过滤。

public class AppConfig implements Config { @Override public SqlLogType getSqlLogType() { return SqlLogType.FORMATTED; } @Override public JdbcLogger getJdbcLogger() { return new Slf4jJdbcLogger(); } }

排查动态SQL问题时,重点看日志里最终生成的SQL。条件没生效、逗号多了、IN列表为空导致语法错误,这些在格式化日志里一目了然。另一个技巧是把DAO方法名和业务场景对应起来,日志里按方法名搜,能快速定位是哪个查询慢、哪个更新影响了异常行数。我还会在关键更新语句执行后打印影响行数,和预期对比,能在测试阶段就发现"条件写漏导致全表更新"这类问题。

注意:生产环境打印完整SQL和参数可能包含敏感信息,务必评估脱敏方案,或者只在排查问题时临时开启,排查完立即关闭。

8. 几个让我印象深刻的实操教训

第一个教训关于参数名。早期项目里我没加-parameters,本地IDE编译正常,CI上偶尔失败,一直以为是不稳定。后来才发现IDE默认开了参数保留,命令行没开,两边行为不一致。从那以后,构建脚本里的编译参数我都显式写死,并且在README里注明"不要依赖IDE默认配置",新人上手少走弯路。

第二个教训关于SQL文件位置。有个项目为了"资源集中",把SQL文件从src/main/java挪到了src/main/resources/sql,同时用@SqlFile注解指定路径。看似优雅,实际带来两个问题:一是路径字符串写错时编译期不报错,运行时才找不到;二是IDE里从DAO方法跳转到SQL文件的功能失效,review效率下降。后来还是挪回了默认位置,接受"SQL和接口同包"的约定,换来的是零配置和可跳转。

第三个教训关于批量操作的静默失败。那次问题之后,我在团队里定了一条规矩:所有BatchResult必须检查isError(),不检查的代码评审直接打回。这条规矩救过好几次,尤其是数据迁移和定时任务场景,静默失败比直接报错危险得多。

第四个教训关于事务边界。有个定时任务方法上没加事务注解,内部两次更新分别提交,中间抛异常导致数据不一致。修复方式是在方法入口加事务,把两次更新包进同一个事务。这件事让我养成一个习惯:凡是写操作超过一条,先问自己"这两条必须同生共死吗",答案是肯定的就加事务,不确定就按同生共死处理,宁可事务大一点,也别留下中间状态。

这些教训没有一条是文档里会重点写的,但每一条都在实际项目里造成过真实影响。Doma的机制决定了它对配置和约定比较敏感,把约定吃透、把边界测到,后面用起来其实很稳。我现在接手新项目,第一件事是跑一遍编译确认注解处理器正常,第二件事是打开SQL日志跑几个典型查询看生成的SQL,这两步做完,后面八成的问题都能提前发现。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/11/2 20:56:52

Doma常见问题排查:从构建期到运行期的SQL映射与事务实践

Doma 这个 Java 数据访问框架在国内技术圈的存在感一直不算高&#xff0c;但在一些对 SQL 可控性要求极高的老系统里&#xff0c;它出现的频率远比想象中高。我头一回接手这类代码库时的真实感受是&#xff1a;DAO 接口干净得吓人&#xff0c;只有一个方法签名和一行注解&#…

作者头像 李华
网站建设 2026/11/2 20:56:02

自建本地AI配音工作台VoiceStudio:从选型到踩坑全复盘

我给自己搭了一个本地AI配音工作台&#xff0c;代号VoiceStudio&#xff0c;前后折腾了差不多一个季度。之所以动手&#xff0c;是因为我每周至少要产出两期解说类视频&#xff0c;配音文本量上来了之后&#xff0c;云端TTS的账单、生硬的机器腔和脚本数据外流这三件事轮流折磨…

作者头像 李华
网站建设 2026/11/2 20:52:24

LoRa温湿度监测系统设计与低功耗优化实践

1. LoRa温湿度在线监测系统概述在工业物联网和智慧农业领域&#xff0c;温湿度监测一直是个经典但棘手的需求。传统有线方案布线成本高&#xff0c;无线方案又面临传输距离和功耗的平衡难题。我们团队最近落地的LoRa温湿度监测系统&#xff0c;采用Semtech的LoRa扩频技术&#…

作者头像 李华
网站建设 2026/11/2 20:50:34

Cesium后处理数据传递:uniform类型对齐与纹理实战

三维场景里做后处理&#xff0c;最让人抓耳挠腮的阶段往往不是写着色器本身&#xff0c;而是把数据从 JavaScript 那一侧准确无误地送进 GPU。我第一次给 Cesium 加自定义后处理时&#xff0c;颜色参数怎么调都不对&#xff0c;折腾了半天才发现是 uniform 声明的类型和传入的数…

作者头像 李华
网站建设 2026/11/2 20:45:29

Qt5/Qt6 工程接入 QFtp:FTP 客户端异步状态机与断点续传实践

简介&#xff1a;面向Qt5及以上版本开发者的QFtp移植与测试示例&#xff0c;针对新版Qt不再提供QFtp类的现实问题&#xff0c;将Qt4.8.7中的QFtp源码抽取为两个核心文件并配套可运行的FTP客户端工程&#xff0c;便于理解旧模块迁移、接口封装与工程适配思路。压缩包共17个文件、…

作者头像 李华
网站建设 2026/11/2 20:45:18

HDFS分布式存储核心原理与实战指南

1. 大数据分布式存储入门指南第一次接触HDFS时&#xff0c;我被它处理PB级数据的能力震撼到了。记得当时用三台二手服务器搭建集群&#xff0c;当看到文件被自动切块分布存储时&#xff0c;那种"原来大数据是这样玩的"的顿悟感至今难忘。作为大数据生态的基石&#x…

作者头像 李华