新闻详情

新闻详情

首页 / 资讯中心 / 详情

MyBatis注解映射实战:核心注解、动态SQL与最佳实践

发布时间:2026/9/15 4:02:33来源:尧图网络
MyBatis注解映射实战:核心注解、动态SQL与最佳实践
1. 注解映射的整体思路与选型背后那些事1.1 注解映射到底解决了什么问题先聊点实际的。以前做MyBatis项目最常见的姿势是写一个Mapper接口再配一个XML文件接口里定义方法XML里写SQL然后通过namespace把两者绑起来。这套玩法本身没毛病但有一个很现实的问题项目一旦大起来Mapper接口和XML文件的数量会快速增长每次改动接口方法签名都得同步去翻XML稍不留神就出现“接口方法加了参数XML里忘了写”这种低级报错。注解映射就是为了解决这个割裂感出现的。它的核心思路很简单把SQL直接写在接口方法上用注解代替XML里的标签。比如你要查一条用户记录直接在接口方法上写Select(select * from user where id #{id})MyBatis就明白这个方法要执行什么SQL返回什么类型。接口即SQLSQL即接口不再需要维护两套文件。从实际开发场景看注解映射特别适合下面几类情况简单CRUD占了绝大多数单表操作居多SQL一眼能看明白接口数量多但每个方法都很短XML文件往往几十行就为了包一个select团队规范要求代码审查效率高注解方式能在一个文件里看全所有数据库操作快速原型和中小型项目不想引入一层额外的XML解析和路径配置。当然注解映射不是银弹。复杂动态SQL、超长联表查询、需要复用同一段SQL片段的场景XML反而更合适。这也是为什么很多团队采用“注解XML”混合模式——简单操作用注解复杂操作用XML。关于这个选型我后面会详细讲。1.2 注解和XML怎么选我的判断标准很多新手一上来就问“到底用注解还是用XML”其实这个问题没有标准答案得看场景。我先说下自己的判断标准也算是一点经验第一SQL复杂程度。如果SQL里出现大量动态判断比如if、choose、foreach这些标签嵌套注解里写起来非常痛苦。MyBatis注解虽然也支持动态SQL但要借助SelectProvider或者SqlProvider写起来比XML里的标签复杂得多。这时候果断用XML别硬扛。第二SQL复用程度。同一个查询条件可能在不同查询里反复出现XML里可以用sql标签定义片段然后include引用。注解方式没有对应的简洁处理只能把公共SQL写成一个常量字符串然后用Java的字符串拼接。说实话拼起来容易出错也不直观。第三团队习惯和代码评审流程。如果团队里大部分人都习惯打开XML看SQL那统一XML风格对维护更友好。如果是小型团队或者个人项目我倾向于注解因为文件少定位快。第四性能上两者没有本质区别。这点经常有人误解觉得注解SQL解析会慢其实MyBatis对注解和XML的SQL解析都发生在启动阶段运行时走的是同一套执行器性能差距可以忽略。所以我的建议是以注解为主XML兜底。简单操作用注解遇到复杂动态SQL再单独写XML文件两者可以通过ResultMap互相引用不存在非此即彼的问题。这个混合思路接下来会贯穿全文实操部分我会演示具体怎么切换。2. 核心注解逐个拆解看完就能上手2.1 四个基础SQL注解Select、Insert、Update、DeleteMyBatis注解映射的基石就是这四个注解分别对应查询、插入、更新、删除四种操作。用法直接写在接口方法上value值就是SQL语句。public interface UserMapper { Select(select * from user where id #{id}) User selectById(Long id); Insert(insert into user(name, age, email) values(#{name}, #{age}, #{email})) int insert(User user); Update(update user set name #{name}, age #{age} where id #{id}) int update(User user); Delete(delete from user where id #{id}) int deleteById(Long id); }写入数据库时这几个注解要注意几个容易被忽略的细节Select的返回类型可以是实体类、Map、List、Integer、String等任意类型MyBatis会自动完成映射和转换。如果查询结果有多条记录但返回类型是普通对象MyBatis会报TooManyResultsException这个坑后面排查章节会详细说。Insert的返回值是int类型表示受影响的行数。如果插入时还需要拿到数据库自动生成的自增主键光靠Insert不够要配合Options注解设置useGeneratedKeys和keyProperty这个我在3.3小节里专门演示。Update和Delete同理返回值都是影响行数。一个常见的误区是有的同学把返回值写成booleanMyBatis虽然支持但含义是“影响行数大于0则为true”如果有更新0行的情况返回false可能会误导判断建议统一用int。四个注解的SQL里都可以用#{}占位符MyBatis会创建PreparedStatement参数占位符防止SQL注入。这一点比字符串拼SQL安全得多务必养成用#{}的习惯不要为了省事用${}除非是动态传递表名、列名这类无法用占位符的场景。2.2 参数传递的细节Param与多参数绑定用注解写SQL参数传递是最容易踩坑的地方。单参数场景没什么问题比如上面的selectById方法只有一个Long id参数SQL里的#{id}能直接对应上。但一旦方法有多个参数情况就变了。先看一个反面例子Select(select * from user where name #{name} and age #{age}) ListUser selectByNameAndAge(String name, Integer age);这段代码运行时会报错提示找不到参数name或age。原因是MyBatis对多参数方法默认使用param1、param2这样的命名规则不会智能到自动去匹配方法参数名。除非你编译时加了-parameters参数并且MyBatis开启了相关配置否则老老实实加Param注解最稳妥。正确的写法Select(select * from user where name #{name} and age #{age}) ListUser selectByNameAndAge(Param(name) String name, Param(age) Integer age);加了Param之后SQL里的#{name}和#{age}就能准确绑定到对应参数了。这里多说一句即使只有一个参数如果参数是Map或者List也有讲究。传Map的时候SQL里的#{key}会去Map里按key取值传List或者数组时通常配合foreach动态SQL使用此时需要在Param里指定一个别名否则MyBatis默认以list或array作为参数名。Select(scriptselect * from user where id in foreach collectionids itemid open( separator, close) #{id} /foreach/script) ListUser selectByIds(Param(ids) ListLong ids);这里虽然用了字符串拼SQL但注意#{}仍然是预编译占位符foreach是MyBatis的XML标签在注解里用script标签包起来之后就能识别SQL注入风险依然可控。2.3 结果映射全家桶Results、Result、ResultMap查询结果如何映射成对象这是注解映射里最核心也最容易出问题的地方。先说最简单的场景如果数据库列名和实体类属性名完全一致比如数据库列name对应实体类属性nameMyBatis会自动映射什么都不用写。麻烦的是字段名对不上的情况。比如数据库列叫user_name实体类属性叫userName。以前用XML时要么开启mapUnderscoreToCamelCase自动驼峰转换要么在resultMap里手动映射。注解方式对应的就是Results和Result。Select(select id, user_name, age, email from user where id #{id}) Results(id userResultMap, value { Result(id true, column id, property id), Result(column user_name, property userName), Result(column age, property age), Result(column email, property email) }) User selectByIdWithResultMap(Long id);几个要点解释一下Results的id属性给这组映射规则起个名字方便其他地方复用。复用时用ResultMap(userResultMap)注意这个注解引用的是上面定义的id值。如果你在另一个查询方法上想复用同一套映射直接写Select(select id, user_name, age, email from user where user_name like concat(%, #{name}, %)) ResultMap(userResultMap) ListUser selectByNameLike(String name);Result的id true表示这个字段是主键对后面讲到的嵌套映射和二级缓存都有影响。column对应数据库列名property对应实体类属性名方向别搞反了。还有一个常见痛点查询返回MapString, Object时列名会以数据库原生列名作为key即使开了驼峰转换也不会自动变成驼峰风格这个需要注意。如果需要按实体类风格返回要么建一个接收对象要么在SQL里给列名起别名比如select user_name as userName。2.4 关联查询与嵌套映射One和Many关联查询在XML时代用association和collection标签实现注解方式对应的是One和Many配合Result使用。先看一个典型的场景一个订单对应一个用户查订单时希望把用户信息也带出来。用注解实现如下public class Order { private Long id; private String orderNo; private Long userId; private User user; // getter/setter 省略 } Select(select * from orders where id #{id}) Results(id orderResultMap, value { Result(id true, column id, property id), Result(column order_no, property orderNo), Result(column user_id, property userId), Result(property user, column user_id, one One(select com.example.mapper.UserMapper.selectById, fetchType FetchType.LAZY)) }) Order selectOrderWithUser(Long id);这里的逻辑是查完订单后把user_id这一列的值作为参数调用UserMapper.selectById方法再查一次用户信息填充到Order.user属性里。fetchType有LAZY和EAGER两种LAZY代表懒加载即真正访问到user属性时才去执行第二个查询EAGER则立即查询。一对多场景类似用Manypublic class User { private Long id; private String name; private ListOrder orders; // getter/setter 省略 } Select(select * from user where id #{id}) Results(id userWithOrdersResultMap, value { Result(id true, column id, property id), Result(column name, property name), Result(property orders, column id, many Many(select com.example.mapper.OrderMapper.selectByUserId, fetchType FetchType.LAZY)) }) User selectUserWithOrders(Long id);这里column id表示把查询结果里的id列值作为selectByUserId方法的参数传入的是当前用户的id以此查询该用户的所有订单。One和Many的嵌套查询要注意N1问题。懒加载能缓解一部分性能压力但如果循环遍历集合挨个触发子查询数据库压力会很大。复杂报表场景建议还是用XML写一次性联表查询或者用SelectProvider写动态SQL来控制。3. 从零搭建一个注解映射的完整实操3.1 环境准备和项目结构这一节我用一个Spring Boot MyBatis的完整示例带你把注解映射从依赖引入到跑通CRUD整个流程走一遍。为了减少干扰这里不引入MyBatis-Plus等增强框架就用原生MyBatis把注解映射的本真逻辑看清楚。项目基础环境JDK 8Spring Boot 2.x3.x也兼容主要是mybatis-spring-boot-starter的版本要对应MySQL 5.7/8.0Maven 3.6pom.xml里核心依赖如下dependency groupIdorg.mybatis.spring.boot/groupId artifactIdmybatis-spring-boot-starter/artifactId version2.3.2/version /dependency dependency groupIdcom.mysql/groupId artifactIdmysql-connector-j/artifactId scoperuntime/scope /dependency注意Spring Boot 3.x和Spring Boot 2.x对mybatis starter的版本要求不一样3.x要用mybatis-spring-boot-starter 3.0否则可能出现兼容问题。我这里用的是2.3.2对应Spring Boot 2.7.x稳定性最好。application.yml里配置数据源和MyBatis相关参数spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/demo?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai username: root password: your_password mybatis: configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImplmap-underscore-to-camel-case建议开启这样数据库user_name到userName的转换自动完成Result映射可以少写很多。log-impl配置成StdOutImpl控制台直接打印SQL和参数排查问题非常方便。项目结构上接口和实体类分层清晰就行com.example.demo ├── DemoApplication.java ├── entity │ └── User.java └── mapper └── UserMapper.java启动类加MapperScan(com.example.demo.mapper)扫描Mapper接口或者每个Mapper接口上单独加Mapper注解两种方式二选一。接口多了建议用MapperScan省得每个接口都写一遍。3.2 单表CRUD的注解实现建一张简单的用户表CREATE TABLE user ( id bigint(20) NOT NULL AUTO_INCREMENT, user_name varchar(50) NOT NULL, age int(11) DEFAULT NULL, email varchar(100) DEFAULT NULL, create_time datetime DEFAULT NULL, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;注意表名我用了user这在某些数据库里是保留字如果报错可以改成t_user或者加上反引号。这里为了演示简洁假设环境允许。对应的实体类public class User { private Long id; private String userName; private Integer age; private String email; private Date createTime; // getter/setter 省略 }UserMapper接口完整的单表CRUDpublic interface UserMapper { Select(select * from user where id #{id}) User selectById(Long id); Select(select * from user where user_name #{userName}) User selectByUserName(String userName); Select(select * from user order by id desc limit #{limit}) ListUser selectRecentList(Param(limit) int limit); Insert(insert into user(user_name, age, email, create_time) values(#{userName}, #{age}, #{email}, #{createTime})) int insert(User user); Update(update user set user_name #{userName}, age #{age}, email #{email} where id #{id}) int update(User user); Delete(delete from user where id #{id}) int deleteById(Long id); }这里有个细节值得展开selectByUserName只有一个String参数但没有加Param。这个能不能正常工作答案是能。MyBatis对单个简单类型参数有默认处理#{userName}会从唯一参数对象中取值。但为了统一规范我建议单参数也写上Param尤其是参数名和SQL占位符不一致的时候能少踩很多坑。插入时createTime如果为nullMyBatis会原样插入null不会报错。如果数据库字段有默认值实体类里没设置MyBatis仍然会把null传给SQL导致默认值不生效。解决方法是插入时判断一下或者用数据库的NOW()函数比如Insert(insert into user(user_name, age, email, create_time) values(#{userName}, #{age}, #{email}, NOW())) int insert(User user);这样createTime就可以完全不依赖实体类属性数据库自动填当前时间。3.3 返回自增主键的几种写法插入后马上要用到自增主键这个需求太常见了。注解方式有两种主流写法我一个个说。第一种Options注解Insert(insert into user(user_name, age, email, create_time) values(#{userName}, #{age}, #{email}, #{createTime})) Options(useGeneratedKeys true, keyProperty id) int insert(User user);执行完insert后MyBatis会把数据库生成的自增id回填到user.getId()里。keyProperty指定回填到实体类的哪个属性这里就是id。注意是回填不是返回值。方法的返回值int仍然是受影响行数。第二种SQL里写SELECT LAST_INSERT_ID()然后配合SelectKey注解Insert(insert into user(user_name, age, email, create_time) values(#{userName}, #{age}, #{email}, #{createTime})) SelectKey(statement SELECT LAST_INSERT_ID(), keyProperty id, before false, resultType Long.class) int insert(User user);before false表示在insert执行之后查询主键resultType要跟主键字段类型对应。实际开发中Options的方式更简洁推荐优先使用。这里还要提醒一个细节如果表的主键不是自增的而是应用层生成的ID比如雪花算法生成的Long型ID那就不需要Options直接把ID设到实体类属性上SQL里正常写入即可。不要画蛇添足去配置useGeneratedKeys。把简单场景复杂化这是新手很容易犯的毛病。3.4 动态SQL与Provider注解处理复杂查询前面说过注解方式写动态SQL不如XML直观但也不是不行。MyBatis提供了SelectProvider、InsertProvider、UpdateProvider、DeleteProvider四个Provider注解把SQL的构建逻辑抽到单独的类里用Java代码拼接。看一个按条件查询用户的例子。用户传入的参数是可选的可能传name可能传age也可能都不传public interface UserMapper { SelectProvider(type UserSqlProvider.class, method selectByCondition) ListUser selectByCondition(UserQuery query); } public class UserSqlProvider { public String selectByCondition(UserQuery query) { return new SQL() {{ SELECT(*); FROM(user); if (query.getName() ! null) { WHERE(user_name #{name}); } if (query.getAge() ! null) { WHERE(age #{age}); } ORDER_BY(id desc); }}.toString(); } }这里用了MyBatis自带的org.apache.ibatis.jdbc.SQL类提供了一种类似流式的SQL构建方式。SQL类的好处是能自动处理空格和逗号拼接不容易出错。如果你不习惯这种链式风格也可以返回纯字符串拼接但那种方式可读性差、容易拼接出错不建议。SelectProvider的type指向Provider类method指向类里的方法方法返回值是String类型的SQL。注意Provider方法的参数如果Mapper方法有Param注解Provider方法可以声明对应的参数比如SelectProvider(type UserSqlProvider.class, method selectByNameAndAge) ListUser selectByNameAndAge(Param(name) String name, Param(age) Integer age); public String selectByNameAndAge() { return new SQL() {{ SELECT(*); FROM(user); WHERE(user_name #{name}); AND(); WHERE(age #{age}); }}.toString(); }Provider方法本身不一定要声明参数因为SQL里的#{name}、#{age}占位符是通过MyBatis的参数绑定的Provider只需要返回SQL模板就行。但说实话如果条件组合特别多、嵌套特别深我还是倾向于建一个XML文件来处理。比如查询条件有十几个可选字段、需要多表关联、还有排序分页组合Provider里写起来头大XML里用where、if天然支持这些场景阅读和维护都更省心。这跟我前面说的“注解为主XML兜底”策略一致。3.5 一对多关联查询的完整示例继续用订单和用户的例子把一对多关联查询完整跑通。先建订单表CREATE TABLE orders ( id bigint(20) NOT NULL AUTO_INCREMENT, order_no varchar(50) NOT NULL, user_id bigint(20) NOT NULL, amount decimal(10,2) DEFAULT NULL, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;实体类public class Order { private Long id; private String orderNo; private Long userId; private BigDecimal amount; private User user; // getter/setter 省略 } public class User { private Long id; private String userName; private Integer age; private ListOrder orders; // getter/setter 省略 }OrderMapperpublic interface OrderMapper { Select(select * from orders where user_id #{userId}) ListOrder selectByUserId(Param(userId) Long userId); }UserMapper中增加查用户并带出订单列表的方法Select(select * from user where id #{id}) Results(id userWithOrdersResultMap, value { Result(id true, column id, property id), Result(column user_name, property userName), Result(column age, property age), Result(property orders, column id, many Many(select com.example.mapper.OrderMapper.selectByUserId, fetchType FetchType.LAZY)) }) User selectUserWithOrders(Long id);这段嵌套查询的调用链路是先执行select * from user where id #{id}拿到用户信息然后把这一行的id列值取出来作为参数去执行OrderMapper.selectByUserId最后把返回的订单列表设置到user.orders属性上。我实际测试过这个例子配合log-impl配置控制台会打印两条SQL第一条查用户第二条在访问user.getOrders()时才触发查订单。这就是懒加载的效果。需要注意懒加载默认情况下未必生效。MyBatis的懒加载需要满足两个条件一是fetchType设置为LAZY二是在全局配置里没有关闭懒加载aggressiveLazyLoading默认值为false才是预期行为如果被改成true懒加载就变成了“访问任意字段就加载全部”。建议在application.yml里显式配置一下mybatis: configuration: aggressive-lazy-loading: false还有一个常见问题One和Many中的select属性写的是Mapper接口的全限定名加方法名必须保证方法名拼写正确。如果接口和注解方法不在同一个包写错路径时会报BindingException提示找不到对应的映射语句。排查时重点核对是否为“接口全限定名.方法名”。4. 常见问题与排查技巧实录4.1 注解和XML同名方法冲突怎么办有同学在Mapper接口里用注解写了一个selectById同时又创建了XML文件里面也定义了selectById启动时发现直接报错。这是正常的因为MyBatis规定同一个Mapper接口的同一个方法只能绑定一种SQL定义方式重复定义会抛出IllegalArgumentException之类的错误。实际项目里最稳妥的做法是明确规约同一个Mapper接口要么全注解要么全XML或者一个接口里部分方法用注解、部分方法用XML关联但绝对不能出现“同一个方法既有注解又有XML”的情况。接口里SQL简单、没有动态判断的用注解有复杂动态SQL、需要sql片段复用的用XML。如果确实需要在注解方法上引用XML里的resultMap可以用ResultMap注解它接受的是XML中resultMap的id值。这种情况下注解方法负责写SQLXML只负责结果映射定义两者职责不同不算冲突。4.2 结果集映射不上的那些经典原因注解映射跑起来最容易遇到的就是查出来的列映射不到Java对象属性上字段全是null。我总结了几类高频原因排个序第一列名和属性名不一致而且没开驼峰转换。比如数据库列user_name实体类属性userNameMyBatis不会自动把下划线风格转成驼峰风格除非你设置了map-underscore-to-camel-case: true或者用Result手写映射关系。这个最简单也最容易忽略。第二嵌套查询的目标方法本身没写对SQL或者目标方法返回类型和期望类型不一致。比如Many指向的方法返回的是ListOrder但实际写返回了Order类型不匹配启动时不一定报错运行时可能出现类型转换异常。第三Results定义了一组映射后下方的方法只对指定的列做映射其他列保持默认Auto Mapping。当autoMappingBehavior设置为NONE时那些没在Result里显式指定的列就映射不上了。默认的autoMappingBehavior是PARTIAL会自动映射没有显式指定的列但如果你手动改过配置就可能漏掉很多字段。检查一下这个配置项。第四实体类属性名写错了比如userName写成了username而数据库列是user_name。开启驼峰转换的前提下username和user_name无法对应上会得到null。这种低级错误尤其在复制粘贴时容易出排查半天不如直接打印实体类toString看一眼。给一个建议排查结果映射问题时先把MyBatis的日志打开StdOutImpl看SQL打印出来的列名再对照实体类属性名一目了然。我遇到过很多次纠结了半天配置最后发现就是列名和属性名大小写差了一个字母。4.3 用到注解后缓存和事务还能正常工作吗这个问题经常被问到我可以明确回答注解方式没有改变MyBatis的底层执行流程一级缓存、二级缓存、Spring事务照常生效。先看一级缓存。MyBatis的一级缓存是SqlSession级别的默认开启无法直接关闭可以调localCacheScope为STATEMENT来达到类似效果。注解方式执行SQL同样走SqlSession所以同一个SqlSession内执行两次相同查询第二次会命中缓存。Spring管理下每次请求通常对应一个独立的SqlSession一级缓存的生命周期跟这个SqlSession绑定。二级缓存默认关闭需要手动开启。注解方式开启二级缓存在Mapper接口上加CacheNamespace注解即可类似XML里的cache标签CacheNamespace(eviction LruCache.class, flushInterval 60000, size 512, readWrite true) public interface UserMapper { // ... }配置项含义eviction是缓存回收策略默认LRUflushInterval是刷新间隔单位毫秒size是缓存对象个数readWrite指定缓存是否序列化存取。开启后同一个Mapper的查询结果会进入二级缓存注意缓存的key包含SQL语句、参数和rowBounds所以相同SQL相同参数才能命中。二级缓存有个坑如果开启了CacheNamespace但表数据被其他Mapper更新而其他Mapper没有加入同一个缓存区域就会产生脏读。因为MyBatis可能不知道这张表的数据已经被改了返回给用户的是旧缓存。解决方案是让所有操作同一张表的Mapper共享同一个缓存namespace可以用CacheNamespaceRef指向同一个Mapper或者统一用XML的cache-ref。这块稍不注意就会踩雷建议小项目干脆别开二级缓存省心。再看事务。注解方式的Mapper方法一旦被Spring管理和XML方式没有区别直接在Service层方法上加Transactional(rollbackFor Exception.class)即可。事务的开启、提交、回滚都由Spring的DataSourceTransactionManager管理跟SQL是写在注解里还是XML里毫无关系。Service public class UserService { private final UserMapper userMapper; private final OrderMapper orderMapper; public UserService(UserMapper userMapper, OrderMapper orderMapper) { this.userMapper userMapper; this.orderMapper orderMapper; } Transactional(rollbackFor Exception.class) public void createUserWithOrder(User user, Order order) { userMapper.insert(user); orderMapper.insert(order); } }这里有一个需要留意的点如果在同一个类里内部调用createUserWithOrder事务注解其实是失效的因为Spring事务基于AOP代理内部调用绕过了代理对象。这是Spring事务的经典问题跟MyBatis无关但很多人在Mapper用注解后遇到事务不生效容易误解成注解映射的问题。排查事务问题时先确认是不是内部调用导致代理失效。5. 一些值得收藏的实战建议代码写到后面拼的不是会不会用某个注解而是能不能稳定地不出问题。分享几个我在注解映射上积累的小习惯。第一每个Mapper接口的Results尽量给id命名后续方法用ResultMap复用。这样即使实体类字段很多映射规则也可以集中维护不会每个方法都复制一大段Result。改一次所有引用到的地方都生效。第二SQL字符串里出现多个空格、换行时用script标签包围然后像写XML一样写SQL。MyBatis会把它当XML解析if、where、foreach都能用。虽然代码看着像“注解里套XML”但总比用Provider写一堆Java拼接要直观Select(script select * from user where if testname ! nulland user_name like concat(%, #{name}, %)/if if testage ! nulland age #{age}/if /where /script) ListUser selectByCondition(Param(name) String name, Param(age) Integer age);第三接口方法命名尽量见名知意SQL注解上的注释别省。注解方式把SQL和Java放在一起信息密度高但如果不写注释后来维护的人要一行行看SQL才能知道查询意图。我习惯在每个方法上加一行中文注释写明业务含义和参数说明对团队协作很有帮助。第四配置打印SQL的日志locally只在开发环境保留生产环境改成NoLoggingImpl或者去掉。日志打太多也有性能损耗线上排查问题可以用动态开关的方式临时打开记得用完关掉。第五一个容易被忽视的小点Select返回int或Integer时查询结果为空会返回null而不是0。如果业务上需要判断是否存在更推荐用select count(1)带LIMIT 1的方式或者直接返回对象判空语义更清晰。最后再分享一个扩展方向。注解映射用熟了以后可以尝试把SQL里反复出现的片段抽成常量甚至写一个简单的SQL构建工具类。比如所有查询都要过滤deleted 0这种逻辑删除条件就定义成常量字符串插入到各方法SQL里。用Provider方式做就更灵活了能做到同一套查询逻辑兼容不同表结构。但记住一个原则方法越简单越不容易出错过度抽象反而让维护成本上升。注解映射本身就是为了降低复杂度别把它再搞复杂了。
网站建设高端定制企业官网
RELATED

相关资讯

更多精彩内容,欢迎继续阅读

较早相关资讯

最新相关资讯

SQL注入攻击原理、类型与防御实战指南 2026/9/15 4:53:36

SQL注入攻击原理、类型与防御实战指南

1. SQL注入攻击的本质与危害解析SQL注入(SQL Injection)是Web安全领域最古老却依然活跃的攻击手段之一。简单来说,就是攻击者通过构造特殊输入,改变后端SQL语句的原始逻辑。我处理过的真实案例中,有个电商网站因为一个…

阅读更多 →
工业桌面云选型横评:算力、适配与安全实测 2026/9/15 4:53:36

工业桌面云选型横评:算力、适配与安全实测

1. 为什么工业桌面云的选型逻辑和办公场景完全不同过去这一年,身边越来越多制造企业朋友问我同一个问题:办公桌面云已经跑得好好的,工业桌面云是不是直接照着买就行?我的回答通常是一盆冷水:千万别。办公桌面云和工业桌…

阅读更多 →
邮箱验证怎么做?从RFC 5322到前后端校验实践 2026/9/15 4:53:36

邮箱验证怎么做?从RFC 5322到前后端校验实践

写邮箱验证代码这么多年,我一直信奉一条老理儿:能用标准解决的,别自己发明;能用户友好的,别故意刁难。可现实是,随便一个论坛注册页的邮箱校验正则,都能让我血压升高——要么把ab.c这种明显不合…

阅读更多 →
JSP商品评价系统实战:从SmartUpload到事务处理的完整解析 2026/9/15 4:53:36

JSP商品评价系统实战:从SmartUpload到事务处理的完整解析

简介:一份基于JSPServlet的商品管理与评价系统完整项目,面向Java Web初学者与课程设计人群,适合用来理解商品增删改查、用户评分、登录验证等典型电商功能。压缩包共275个文件,约1.39MB,核心代码由58个Java源文件与对应…

阅读更多 →
三维点云焊锡缺陷检测:从采集到部署的全链路解析 2026/9/15 4:53:36

三维点云焊锡缺陷检测:从采集到部署的全链路解析

简介:面向制造业质检与机器视觉开发者,提供一套基于三维点云的焊锡缺陷检测完整工程方案,涵盖相机数据采集、焊锡外观检测、正/侧面体积计算、飞锡检测及可视化界面。系统通过消息队列传递npy点云文件地址,由算法完成体积计算&…

阅读更多 →
Flink安装与配置实战:从单机到集群部署详解 2026/9/15 4:50:36

Flink安装与配置实战:从单机到集群部署详解

1. Flink核心定位与安装价值解析作为分布式流处理框架的标杆,Apache Flink在实时计算领域占据着不可替代的位置。我亲历过从Storm到Spark Streaming再到Flink的技术演进,Flink之所以能成为行业标准,关键在于其独特的架构设计:基于…

阅读更多 →

今日资讯

本周资讯

本月资讯

看完文章仍有疑问?

联系尧图顾问,获取一对一建站咨询

立即免费咨询 📞 400-888-8888
📞