在电商系统里,持久层就像连接业务逻辑和数据库的 "桥梁",商品信息的增删改查、订单数据的存储流转,都得靠它来高效处理。ZKmall 开源商城挑了 MyBatis 做基础,又配上 MyBatis-Plus 这个增强工具,搭出了一套既灵活又省心的持久层体系。这么一来,开发人员不用天天写重复的 SQL,能把心思放在业务上,同时系统在高并发场景下也能稳稳当当处理数据,真正做到了 "省力不省能"。

基础 CRUD:让简单操作 "一键搞定"
电商系统里,用户注册、商品上架、订单查询这些基础操作天天都在用。MyBatis-Plus 把这些操作打包成现成的接口,不用写一行 SQL 就能直接用,省了不少功夫。
实体类和表的映射是第一步。ZKmall 用 MyBatis-Plus 的注解把 Java 类和数据库表绑在一起:@TableName 告诉程序这个类对应哪个表,@TableId 标记主键字段,@TableField 定义普通字段的对应关系。比如商品表(product)对应的 Product 类,字段名、类型、是否允许为空这些信息,都通过注解写得明明白白,不用再搞个 XML 映射文件单独配置。这样实体类既是数据载体,又说明了表结构,代码简洁多了。
插入数据变得特别简单。用户注册时,创建一个 User 对象,把用户名、密码这些信息填好,调用 userMapper.insert (user),数据就存进数据库了。最方便的是自增主键,比如创建订单后得知道订单 ID,只要在主键字段上加 @TableId (type = IdType.AUTO),插入成功后,这个 ID 会自动填回到实体对象里,直接就能用,不用再查一遍数据库。
查询操作能应付各种场景。查单个商品详情用 selectById,购物车要查多个商品就用 selectBatchIds,想按分类查商品就用 selectByMap 传个条件。复杂点的条件也不怕,QueryWrapper 能像搭积木一样组合条件。比如要找价格在 100 到 500 之间、库存大于 0 的商品,就用 queryWrapper.between ("price", 100, 500).gt ("stock", 0),一行代码搞定,不用写 SQL。有次开发一个商品筛选功能,运营提了七八个条件,用 QueryWrapper 几下就拼好了,要是写 SQL 估计得调来调去半天。
更新操作能精准控制。全量更新用 updateById,改部分字段就用 UpdateWrapper。比如订单超时未支付要改成 "已取消",用 updateWrapper.set ("status", "CANCELLED").eq ("id", orderId).eq ("status", "UNPAID"),执行后只有符合条件的订单会被更新。库存扣减的时候更关键,用 set ("stock", "stock - 1").eq ("id", productId).gt ("stock", 0),保证库存够了才扣,不会出现超卖。
删除操作其实是 "假删除"。电商系统里订单、商品这些数据不能真删,万一以后要查历史记录呢?MyBatis-Plus 的逻辑删除正好派上用场:在 deleted 字段上加 @TableLogic,配置 0 代表没删、1 代表删了,调用 deleteById 时,实际执行的是 update ... set deleted = 1。这样数据还在库里,查询的时候会自动加上 deleted = 0 的条件,不影响正常使用。有次误删了一个商品,因为用了逻辑删除,直接把 deleted 改回 0 就恢复了,要是物理删除就麻烦了。

复杂查询:灵活应对 "花样需求"
电商的查询场景经常很复杂,比如订单列表要关联用户信息、商品列表要分页排序、搜索功能要支持动态条件。MyBatis-Plus 在这些场景下既能简化操作,又不丢灵活性。
多表关联查询靠 "混合模式" 解决。简单的单表查询用 MyBatis-Plus 的现成方法,多表关联就自己写 SQL。比如查订单详情,得把 order、user、product 三张表拼起来,就在 OrderMapper.xml 里写个 JOIN 语句,用 ResultMap 把查询结果映射到 OrderDetailVO 里 —— 这个 VO 里既有订单字段,又有用户名、商品名这些关联信息。有次做一个 "用户订单汇总" 功能,关联了 5 张表,用这种方式写 SQL 既清晰又好调试,比全靠框架生成要靠谱。
分页查询配置完就不用管了。商品列表、订单记录这些肯定要分页,MyBatis-Plus 的分页插件一配,查询时传个 Page 对象就行。比如查第 2 页的商品,每页 20 条,就 new Page<>(2, 20),再用 QueryWrapper 加条件,调用 selectPage 方法,返回的结果里总条数、总页数、当前页数据全有了。不管是 MySQL 的 LIMIT 还是 Oracle 的 ROWNUM,插件会自动适配,换数据库也不用改代码。有次大促前,运营说要改分页大小,前端改个参数就行,后端一点不用动。
动态条件靠 QueryWrapper 灵活拼接。用户搜索商品时,可能输关键词、选分类、设价格范围,也可能什么都不选,这时候 SQL 得跟着变。用 QueryWrapper 的条件方法就能搞定:如果用户输了关键词,就加 like ("name", keyword);选了分类,就加 eq ("category_id", categoryId);没选的条件就跳过。框架会自动处理 AND 关系,不会出现多余的条件导致查不到数据。之前手写动态 SQL 经常少个 AND 或者多个括号,用这个就没出过这种错。
统计分析就直接写 SQL。销售报表要算每个分类的销量、平均价格,这种带 GROUP BY 和聚合函数的查询,直接在 XML 里写 SQL 更清楚。比如统计商品销量,写个 SELECT product_id, COUNT (*) as sales FROM order_item GROUP BY product_id,结果映射到 ProductSalesDTO 里。MyBatis-Plus 也支持用 QueryWrapper 做子查询,但复杂的统计还是手写 SQL 更直观,维护起来也方便。

性能优化:让数据操作 "跑起来"
电商系统一到促销就人山人海,持久层性能跟不上可不行。ZKmall 从 SQL、缓存、批量操作这些方面下手,把数据操作的速度提了一大截。
SQL 优化是基础中的基础。MyBatis-Plus 的 SQL 分析插件能把执行的 SQL 打出来,还能看执行计划,慢查询一眼就能发现。商品列表查得多,就给 category_id、price 这些字段加索引;查询的时候只查需要的字段,别用 SELECT *;关联查询少用 JOIN,必须用的话保证关联字段有索引。有个订单查询原来要 2 秒多,加了个索引后降到 100 毫秒以内。数据量大了就分表,比如订单表按月份拆分,用 MyBatis-Plus 的动态表名功能,查哪个月的订单就自动切换到对应的表。
缓存机制能省不少事。MyBatis 自带一级缓存(同一个会话里查相同数据不重复查库)和二级缓存(同一个 Mapper 共享),再配上 Redis 分布式缓存,效果更好。商品详情页访问量特别大,就开二级缓存,设置 30 分钟过期,用户查同款商品直接从缓存拿,数据库压力小多了。数据更新的时候,用 @CacheEvict 把相关缓存清掉,保证用户看到的是最新数据。有次商品降价,缓存及时清了,没出现显示旧价格的情况。
批量操作比循环快太多。批量上架 1000 个商品,用 saveBatch 方法生成一条带多个值的 INSERT 语句,比循环调用单条插入快 5 倍以上。处理十万级数据的时候,就分片批量处理,一次处理 1000 条,配合事务控制,既高效又安全。之前有个运营导入商品的功能,用循环插入要十几分钟,改成批量操作后一分钟就搞定了。
避免 N+1 查询很关键。查订单列表的时候,每个订单都要查一下用户信息,如果循环查的话,100 个订单就要查 101 次库(1 次查订单,100 次查用户)。ZKmall 用两种办法解决:要么写关联查询 SQL 一次查完,要么先查所有订单,再把用户 ID 汇总起来批量查用户,然后在内存里拼结果。这两种办法都能把查询次数降到 2 次,列表加载速度明显变快。

事务与一致性:守护数据 "不出错"
电商交易涉及钱和货,数据一致性出问题就麻烦了 —— 比如库存扣了订单没创建,或者用户付了钱商品没发货。ZKmall 用事务管理和各种机制,把这些风险降到了最低。
声明式事务特别省心。在 Service 方法上加个 @Transactional 注解,Spring 就会自动管事务:方法里的操作要么全成功,要么全失败。订单创建的时候,既要扣库存又要插订单记录,万一中间抛异常,库存会自动回滚,不会出现扣了库存但没订单的情况。注解里还能设传播行为和隔离级别,比如查询方法不用开事务,就设 propagation = Propagation.SUPPORTS。
分布式事务搞定跨服务操作。微服务里创建订单可能要调用订单服务、商品服务、支付服务,每个服务都有自己的数据库。这时候用 Seata 分布式事务框架,配合 MyBatis 的事务机制,保证这些操作要么全成,要么全退。有次支付服务出了问题,订单服务和商品服务的操作都自动回滚了,没出现数据乱掉的情况。
乐观锁防并发冲突。多个用户同时买同一个商品,库存可能算错。在库存字段旁边加个版本号字段,用 @Version 注解标记,更新库存时会自动加 where version = ? 条件,更新成功后版本号加 1。两个用户同时抢最后一件库存,只有一个能成功,另一个会失败重试,不会出现超卖。这个机制在秒杀场景里特别管用。
防重复提交保证操作幂等。用户手快点了两次支付,不能创建两个订单。ZKmall 用 "令牌 + Redis" 解决:用户下单前先拿个令牌,提交的时候带上,后端验证令牌有效就处理,处理完就把令牌删了,第二次提交就会被拒绝。就算令牌验证漏了,数据库里订单号有唯一索引,用 INSERT ... ON DUPLICATE KEY UPDATE 语法,也只会更新不会重复创建。

最佳实践:踩过的坑与攒下的经验
用 MyBatis 和 MyBatis-Plus 久了,ZKmall 总结出不少实用经验,既能提高效率,又能少走弯路。
实体类设计有套路。所有实体类都继承 MyBatis-Plus 的 Model,主键统一用 Long 类型,创建时间、创建人这些字段放在 BaseEntity 基类里,用 @TableField (fill = FieldFill.INSERT) 配置自动填充,子类继承后不用再写这些重复代码。逻辑删除、版本号这些通用功能也放基类里,新加个实体类只要关注自己的业务字段就行。
Mapper 和 XML 分离更清晰。基础 CRUD 直接继承 BaseMapper,不用写接口方法;复杂查询就在 Mapper 接口里定义方法,对应的 XML 文件里写 SQL。XML 按 Mapper 分类放,比如 OrderMapper.xml 只放订单相关的 SQL,找起来方便。有次改一个查询逻辑,直接找到对应的 XML 文件改 SQL,不用动接口,特别顺手。
分页排序标准化省沟通成本。所有列表接口都返回 IPage 对象,前端拿到就能直接用;排序参数统一用 sort 和 order,后端转成 orderBy 条件。前后端对接的时候不用每次讨论返回格式,效率高多了。
SQL 审查不能少。自定义 SQL 必须经过审查,看看有没有全表扫描、有没有加索引、有没有 SQL 注入风险。用 LambdaQueryWrapper 代替字符串字段名,比如 Product::getName,能避免字段名拼错。禁止在循环里执行 SQL,这种代码一到数据量大的时候就出问题。
现在的 ZKmall 持久层,简单操作不用写 SQL,复杂场景能灵活控制,高并发下也能稳住,开发效率和系统性能都兼顾到了。对开源商城来说,这种技术选型特别合适 —— 新手容易上手,老手可灵活扩展,不管是小店铺还是大平台,都能满足需求。说到底,持久层做得好,系统才能跑得稳,用户才能用得爽,这大概就是技术选型的真谛吧。