news 2026/10/7 10:50:17

Java后端必会:MyBatis实战指南,从动态SQL到缓存机制一次讲透

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Java后端必会:MyBatis实战指南,从动态SQL到缓存机制一次讲透

这些年做Java后端,项目里十套有七八套用的都是MySQL加MyBatis这套组合。说实话,MyBatis刚出来那会儿,很多人觉得它就是"写SQL的工具",没什么技术含量,但等你真正在业务里跑过几轮,就会发现它把SQL的掌控权完全交还给了开发者,在复杂查询、报表统计、多表联动这些场景里,这种掌控感有多重要。

这篇文章不打算复读官方文档,而是从我自己项目里的真实经验出发,聊聊在Java + MySQL这套技术栈下,MyBatis到底该怎么快速上手、怎么避坑、怎么把动态SQL和缓存这些能力真正用起来。不管你是刚接触Mapper接口的新手,还是已经写过一段时间XML映射文件但总遇到诡异问题的朋友,这篇文章应该都能给你一些参考。内容偏实战,跟着步骤走,基本能直接落到项目里用。

1. 为什么是MyBatis,而不是别的ORM框架

1.1 从JDBC裸写说起

早年间我用原生JDBC写过一阵子数据访问层,印象太深了。每次查询都要自己管理Connection、PreparedStatement、ResultSet,写完查询还要手动关闭资源,稍微漏一个close,连接池很快就被拖垮。当时一个简单的用户查询方法,代码里大半篇幅都在处理"数据库连接的获取与释放",真正业务逻辑没几行。后来项目里引入了MyBatis,这些样板代码基本全部消失,连接管理、参数绑定、结果集映射这些脏活累活,框架全包了。数据访问层从"写JDBC工具类"变成了"写SQL和结果映射规则",开发效率提升的幅度是肉眼可见的。

1.2 MyBatis与Hibernate的核心差异

不少初学者会在MyBatis和Hibernate之间纠结。Hibernate的理念是"全自动ORM",你只要定义好实体类和表结构的关系,它帮你生成SQL,甚至帮你维护表结构。MyBatis的理念则是"半自动",SQL你自己写,框架只负责把参数传进去、把结果集映射成对象。差别在于掌控力。Hibernate自动生成的SQL在简单场景下很好用,可一旦遇到多表关联、子查询、复杂条件分支,调优就很痛苦,因为SQL不是自己写的,性能出了问题你得先猜它生成了什么。MyBatis把SQL明明白白摆在你面前,慢查询、索引失效、扫描行数过大,一眼就能在SQL层面定位。我个人的观点是,在业务逻辑复杂、查询路径不可预测的中大型系统里,MyBatis这种"看得见摸得着"的方式更稳。

1.3 我在项目里感受到的优势

实际项目跑下来,MyBatis有几个点是我特别喜欢的。第一是SQL复用能力,用SQL片段可以把常用的查询列、where条件抽出来,多张Mapper共享,改一处全局生效。第二是动态SQL非常灵活,if、where、foreach、choose这些标签组合起来,几乎能覆盖所有条件拼装场景,不用像字符串拼接一样担心SQL注入。第三是性能可控,MyBatis对SQL执行过程和缓存都有比较明确的干预入口,大促前做压测,能清楚知道每个查询是怎么执行的,该加缓存加缓存,该改SQL改SQL,心里有底。对比起来,MyBatis就像手动挡的车,操作繁琐一点但动力响应直接;Hibernate像自动挡,省心但急加速时总觉得隔了一层。

2. 十分钟搭起第一个可运行的MyBatis项目

2.1 环境准备与依赖引入

要跑起一个MyBatis项目,前提是本地有JDK(1.8以上)、Maven和MySQL。MySQL安装我建议直接下载官方安装包,Windows环境用MySQL Installer,Linux环境用tar包解压或者包管理器安装,装完后记得确认3306端口能连通。项目这边我习惯用Maven管理依赖,pom.xml里需要引入mybatis核心依赖和MySQL驱动,版本号我建议保持相对较新,避免老版本对字符集排序规则支持不完善。第一次搭项目时,依赖冲突是最常见的坑,比如驱动包版本和MySQL服务端版本不匹配会出现"Public Key Retrieval is not allowed"之类的报错,后续排查章节会细说。

<dependencies> <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis</artifactId> <version>3.5.16</version> </dependency> <dependency> <groupId>com.mysql</groupId> <artifactId>mysql-connector-j</artifactId> <version>8.0.33</version> </dependency> </dependencies>

2.2 核心配置文件mybatis-config.xml

依赖引好后,需要一个全局配置文件。它相当于MyBatis的总控室,数据库连接、类型别名、驼峰映射、日志输出这些都在这里声明。我自己常用的配置大概长这样:

<?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "https://mybatis.org/dtd/mybatis-3-config.dtd"> <configuration> <settings> <setting name="mapUnderscoreToCamelCase" value="true"/> <setting name="logImpl" value="STDOUT_LOGGING"/> </settings> <environments default="development"> <environment id="development"> <transactionManager type="JDBC"/> <dataSource type="POOLED"> <property name="driver" value="com.mysql.cj.jdbc.Driver"/> <property name="url" value="jdbc:mysql://localhost:3306/mybatis_demo?useSSL=false&amp;serverTimezone=Asia/Shanghai"/> <property name="username" value="root"/> <property name="password" value="your_password"/> </dataSource> </environment> </environments> <mappers> <mapper resource="mapper/UserMapper.xml"/> </mappers> </configuration>

这段配置里mapUnderscoreToCamelCase是灵魂,开启后数据库的user_name字段能自动映射到Java里的userName属性,否则你会在实体类或者resultMap里手工写一堆column映射,很烦。连接串里的serverTimezone也不能省,MySQL 8之后时区问题会直接影响时间字段的读写,我之前不加这个参数,本地跑得好好的,部署到服务器就出现时间偏差八小时的诡异问题,排查了半天。

2.3 Mapper接口与XML映射文件的绑定

MyBatis的设计里,Mapper接口和XML映射文件是配对的。接口定义Java方法,XML里写对应的SQL和执行规则。很多新手刚接触时搞不懂这个绑定机制,这里多说一句:MyBatis启动时会把接口的全限定名和XML里的namespace关联起来,接口里的方法名就是XML中statement的id。所以XML的namespace一定写成接口全限定名,方法id一定要和接口方法名完全一致,否则启动直接报错,而且这类报错信息还不是很直观。

public interface UserMapper { User selectById(Integer id); }
<mapper namespace="com.demo.mapper.UserMapper"> <select id="selectById" resultType="com.demo.entity.User"> select * from user where id = #{id} </select> </mapper>

这里还有一个很容易踩的坑:resultType到底是写实体类全限定名还是别名。如果你在全局配置里注册了typeAlias,可以写别名,否则老老实实写全限定名。我自己一开始图省事写了别名又忘了注册,结果启动时直接报"Could not resolve type alias",后来统一使用全限定名,再也没出过这种问题。

2.4 参数传递的几种姿势

Mapper接口的方法参数传递,看起来简单,实际用起来的坑也不少。单个基础类型参数,XML里直接用#{任意名字}都能取到,因为MyBatis会把它当作一个单独的参数处理。多个参数就需要注意了,不写@Param注解的话,XML里只能用param1、param2这类位置命名,代码可读性很差,我强烈建议多个参数时都加上@Param注解显式命名。传对象时直接通过属性名取值。还有一种常见场景是传Map,适合字段不固定的情况。我整理了一个简单的对照表:

参数形式推荐写法XML取值方式
单个基础类型selectById(Integer id)#{id}
多个基础类型selectByNameAndAge(@Param("name") String name, @Param("age") Integer age)#{name}、#{age}
单个对象insert(User user)#{userName}、#{age}
MapselectByMap(Map param)#{keyName}

之前我在一个查询接口里同时传了userId和status,当时没写@Param,以为XML里写#{userId}就行,结果运行起来一直报"There is no getter for property named userId",加上@Param后秒解决。这个细节在面试题里也经常被问到,属于基础但高频的考点。

3. 动态SQL与结果映射:每天都要用的核心能力

3.1 if和where组合查询

动态SQL是MyBatis最实用的能力,没有之一。最常见的场景就是多条件组合查询,用户可能按姓名筛选,也可能按状态筛选,也可能几个条件一起传。如果用字符串拼接实现,不仅要小心翼翼处理多余的where和and,还得防SQL注入,心累。MyBatis里用if加where标签就能优雅解决:

<select id="searchUsers" resultType="com.demo.entity.User"> select * from user <where> <if test="name != null and name != ''"> and name like concat('%', #{name}, '%') </if> <if test="status != null"> and status = #{status} </if> <if test="minAge != null"> and age >= #{minAge} </if> </where> order by create_time desc </select>

where标签会自动处理首个子句前的and或or,不会生成"where and status = 1"这种SQL。这里我踩过的一个小坑是test表达式里字符串比较建议写成'name',不要写成"name",单双引号在XML和OGNL表达式里处理起来容易出歧义,统一用单引号能少很多怪问题。

3.2 foreach批量处理

批量插入、批量删除是foreach标签的经典应用场景。以前用JDBC写批量插入,要么循环执行单条insert,数据库连接消耗很大,要么手工拼接几百个values,代码又丑又容易出错。MyBatis里foreach直接把集合展开到SQL里,配合批量提交或批处理模式,效率提升明显。

<insert id="batchInsert" parameterType="list"> insert into user(name, age) values <foreach collection="list" item="item" separator=","> (#{item.name}, #{item.age}) </foreach> </insert>

使用时有几个小细节要注意。collection属性的值取决于参数类型,用@Param("list")就写list,不写注解直接传List,就写collection="list"或"collection"。item是循环变量名,后面取值都用它来引用,别在循环体里写成别的名字。separator控制每个元素之间的分隔符,批量插入时是逗号,批量更新时可能是空字符串配合case when。还有一个常见问题是批量插入的SQL特别长,MySQL默认的max_allowed_packet如果太小会直接报错,几百上千条数据一起插入时,建议把MySQL这个参数调大到16M或者32M。

3.3 resultMap解决字段映射"最后一公里"

简单查询用resultType加驼峰映射就够了,但多表联查、字段重名、关联对象这些场景,resultType就有点顶不住了。这种时候需要resultMap,它是MyBatis里最灵活的结果映射手段。举一个例子,查询订单表关联用户表,我们希望返回的OrderVO里既包含订单信息,又包含用户姓名和手机号。

<resultMap id="OrderWithUserMap" type="com.demo.vo.OrderVO"> <id property="id" column="order_id"/> <result property="orderNo" column="order_no"/> <result property="totalAmount" column="total_amount"/> <association property="user" javaType="com.demo.entity.User"> <id property="id" column="user_id"/> <result property="name" column="user_name"/> <result property="phone" column="user_phone"/> </association> </resultMap> <select id="selectOrderWithUser" resultMap="OrderWithUserMap"> select o.id as order_id, o.order_no, o.total_amount, u.id as user_id, u.name as user_name, u.phone as user_phone from orders o left join user u on o.user_id = u.id where o.id = #{id} </select>

这种写法有个好处:SQL里的别名和resultMap的column一一对应,Java对象结构再复杂也能映射得清清楚楚。如果遇到连表查询还叠加动态条件,XML会变得很长,我的习惯是把公共的SQL片段抽出来,下一节就讲讲SQL片段的妙用。

3.4 自定义SQL片段复用

当某个查询列集合在多个查询里反复出现时,比如select详情和select列表都要查同样的几个字段,每次都重复写一遍列名很浪费时间。MyBatis的sql标签可以把这一坨提取出来,需要的地方include一下。这里有一个小坑:include引用的是同一个命名空间里的sql片段,如果跨Mapper使用,sql标签里要写上完整的refid做限定。我一般建议把通用的列片段放在各自的Mapper里,避免跨Mapper引用造成维护混乱。sql片段里还能配合include的property属性传参,这招做动态列查询时很实用,不过日常项目里简单复用一下就够了,别过度设计。

4. 缓存机制解析:一级缓存和二级缓存

4.1 一级缓存作用域与失效情况

MyBatis默认开启一级缓存,作用域是SqlSession。同一个SqlSession内执行同一条SQL且参数相同,第二次查询会直接命中缓存,不再访问数据库。这听起来很美好,但实际使用有几个坑。第一,如果SqlSession对象不是同一个,一级缓存就失效,Spring整合后每次请求都可能新建SqlSession,一级缓存基本指望不上。第二,如果两次查询之间执行了增删改操作或者手动调用了clearCache,缓存会被清空。第三,用了Spring整合之后一级缓存还会和事务绑定,事务内同一个SqlSession才能共享,事务结束缓存就没了。所以不要对一级缓存抱太多期望,它就是MyBatis顺手做的一层内存复用。

4.2 二级缓存配置与使用注意

二级缓存的作用域是Mapper namespace,跨SqlSession共享,需要手动开启。配置方式是在全局配置里设置cacheEnabled为true,同时在对应Mapper的XML里加一个cache标签。开启后,查询结果会以序列化形式存到缓存中,第二次查询不走数据库,性能提升明显。但副作用也很明显,最典型的是脏数据问题。只要这个namespace对应的表被其他途径修改了数据,缓存无法感知,就会读到陈旧数据。所以我个人的建议是:字典表、配置表这种低频变更的数据,可以放心开二级缓存;高频变更或对数据实时性要求高的业务表,别开。还有一个细节,开启二级缓存后,实体类必须实现Serializable接口,否则缓存写入时会报序列化异常,我见过有人在这里卡了半天的。

4.3 和Spring Boot整合后缓存行为的变化

在Spring Boot项目里,MyBatis和Spring整合后,SqlSession由SqlSessionTemplate管理,它会动态代理方法,每次方法调用可能获取新的SqlSession,所以一级缓存的作用范围进一步缩小,基本只存在于事务内。这也解释了为什么很多人在Spring Boot项目里开启了二级缓存,查询速度确实变快,因为跨方法查询时二级缓存还在生效。我的经验是,缓存这块配置宁可少开也不要乱开,业务量上来了用Redis做分布式缓存才是正路。MyBatis自带的本地缓存做做单机优化还行,多实例部署时一定要关闭或做好同步,否则同一份数据在不同机器上的状态不一致,出问题很难查。

5. 常见问题与排查技巧实录

5.1 出参为null但SQL有数据

遇到过这种情况吗?数据库表里明明有记录,接口查出来对象却是null或者某个字段是null。大多数时候是映射问题。第一,确认resultType是否写对,如果是集合结果却写成了实体类型,MyBatis只会取第一条数据封装,看起来像"数据丢失"。第二,确认字段映射是否匹配,开启驼峰映射后user_name对应userName,如果表字段和实体字段命名差异太大,得用resultMap或SQL别名处理。第三,检查SQL本身执行后在MySQL客户端里能否查到数据,有时候是因为表名写错、库选错,查了个空表。我在排查时有个习惯,先把MyBatis打印的SQL复制到客户端里执行一遍,数据层的问题就解决了一半。

5.2 自动映射下划线失败

这个问题的根源通常是mapUnderscoreToCamelCase没开或者配置没生效。Spring Boot整合下,配置文件里可以这样设置:

mybatis: configuration: map-underscore-to-camel-case: true

如果你在XML里同时写了resultMap,那么驼峰自动映射不会作用于resultMap里的字段,还是得在resultMap中手动写清楚映射关系。我遇到过一种情况,某些字段在实体类里是userName,数据库里是user_name,开了驼峰自动映射后大部分都对了,但有个字段还是null,排查发现是因为这个字段在resultMap里出现了,自动映射被覆盖了。所以规则记住了:resultMap里写的字段,就必须在resultMap里映射完整;用resultType的查询,才享受驼峰自动映射。

5.3 LocalDateTime类型的处理

Java 8的时间类型和MySQL的datetime类型映射,在旧版驱动里经常报错或者变成java.sql.Timestamp。解决办法有两个思路。一是在JDBC连接串里配置tinyInt1isBit等参数优化细节,时间类型一般不受影响。二是给实体类字段使用LocalDateTime,同时把驱动版本升级到8.0.23以上,新版驱动对JSR-310支持已经很完整了。如果还不行,可以在字段上添加@DateTimeFormat或@JsonFormat注解配合Jackson处理。这个坑多见于老项目迁移,新项目直接从LocalDateTime开始用基本不会有事。

5.4 慢SQL定位技巧

MyBatis本身不负责SQL性能优化,但能帮你快速定位慢SQL。首先把日志级别打开,打印出完整SQL和执行参数,然后复制到MySQL里用EXPLAIN分析。我看EXPLAIN时主要关注type列,如果出现ALL全表扫描,那基本就是没有合适的索引,优先看where条件和order by涉及字段的索引。还有Extra列出现Using filesort时,说明排序没走索引,数据量一大性能就会断崖式下跌。实际项目中我经常在Mapper方法名里就标清楚用途,比如selectUserByIdWithLock这种,后期排查SQL一看方法名就知道哪个语句是核心路径,不用一条条翻XML。

6. 和Spring Boot、MySQL实际整合的个人配置方案

6.1 Spring Boot整合配置

现在绝大多数新项目都是Spring Boot + MyBatis的组合,整合流程比纯MyBatis简单很多。pom里引入mybatis-spring-boot-starter,然后配置数据源和MyBatis相关参数就行。约定优于配置,很多参数都给了默认值,但有几个我习惯显式配置:

spring: datasource: url: jdbc:mysql://localhost:3306/mall?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true username: root password: your_password driver-class-name: com.mysql.cj.jdbc.Driver mybatis: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.demo.entity configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImpl

mapper-locations指定XML文件位置,type-aliases-package让XML里可以直接写实体类简单类名,map-underscore-to-camel-case处理驼峰映射。第一次整合时忘记写mapper-locations,结果启动没报错,但调用Mapper方法时一直提示Invalid bound statement,其实就是XML没被加载进来。这个报错在MyBatis整合场景下非常高频,新手遇到不要慌,先检查这个配置。

6.2 Mapper接口扫描与事务管理

Spring Boot整合后,需要在启动类或者配置类上加@MapperScan注解,指定Mapper接口所在包路径。加了之后框架会动态代理这些接口。也可以在每个Mapper接口上加@Mapper注解,但接口多了之后很分散,我更喜欢集中扫描。事务管理方面,在需要事务的Service方法上标注@Transactional即可,Spring将负责事务的开启、提交和回滚。有个容易忽略的点:事务默认只在RuntimeException时回滚,如果方法抛出受检异常,事务不会回滚。这个行为可以在@Transactional的rollbackFor属性里指定。业务代码里抛出异常时我习惯包装成RuntimeException来触发回滚,避免事务"悄悄地没生效"。

6.3 分页插件PageHelper的取舍

分页查询是Web项目绕不开的需求。PageHelper是我用过最多的MyBatis分页插件,用法也简单,在查询方法前调用PageHelper.startPage(pageNum, pageSize),后面执行的第一个查询就会被自动拼接limit。用起来很爽,但有两个坑必须记住。第一,startPage后紧接着的必须是Mapper查询,中间不能有其他数据库操作,否则分页可能作用到错误的SQL上。第二,多数据源或嵌套查询场景下,PageHelper可能拦截到意外语句,此时建议手动用Interceptor配置或者干脆写limit分页。数据量不大的后台管理系统,手写limit一清二楚;数据量大或者查询条件复杂的,PageHelper确实能省不少事。

6.4 控制台打印SQL与参数

Debug模式下的一个刚需就是把执行的SQL和参数打印出来。纯MyBatis时通过logImpl配置,Spring Boot整合时把log-impl设置为StdOutImpl,会在控制台打印完整的SQL预处理语句和参数列表。打印出来的SQL在MySQL客户端里执行时,需要手动把参数替换进去,注意字符串参数要加引号。另外这些日志在高并发生产环境里会产生大量IO开销,我只建议在开发环境和测试环境开启,生产环境可以把日志级别调到WARN,或者用动态开关配合日志框架管理。我这边的习惯是开发环境输出到控制台,测试环境输出到日志文件,生产环境默认关闭,需要排查问题时再临时开启。

7. 一些我认为最重要的实操心得

如果现在有人问我,刚开始接触Java + MySQL下的MyBatis,最应该抓住什么,我会说是"把SQL主动权握在自己手里"。MyBatis学习的核心不在于背几个标签,而是理解SQL与Java对象之间的映射逻辑,理解参数和返回值是怎么流动的。动态SQL和resultMap用熟了,复杂查询基本没有障碍,再遇到性能问题也能从SQL层面直接优化。

还有一个建议是养成看运行日志的习惯。很多诡异问题根本不涉及复杂原理,就是SQL拼错了、参数没传对、映射字段写错。把MyBatis的SQL日志打开,错误现场基本一目了然。早期我排查一个问题要反复改代码重启,后来发现直接看日志就能定位八九成,整个排查效率完全不同。

最后提醒一句,代码规范和命名统一比什么都重要。Mapper接口方法名、XML的id、参数注解,这三者之间保持清晰的对应关系,项目维护起来会舒服很多。你可以根据自己的偏好决定用注解式SQL还是XML式SQL,但在一个项目里尽量保持一致,不要今天写注解明天写XML,后期维护的人会非常感谢你。这套Java + MySQL + MyBatis的组合,我用了这么多年,谈不上有多少惊艳的设计,但胜在稳定、直接、可控,业务再复杂也能用简单的方式拆解掉。

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

LanBERT:面向中文短文本的轻量级BERT变体与工业部署实践

简介&#xff1a;本资源是一套面向航天工程专业学生、轨道力学研究者及MATLAB实践者的兰伯特问题求解工具包&#xff0c;聚焦于经典天体动力学中的轨道转移计算——即在给定起止位置与飞行时间约束下&#xff0c;求解满足牛顿引力定律的可行转移轨道。压缩包共15个MATLAB源文件…

作者头像 李华
网站建设 2026/10/7 10:49:50

FPGA高速接口SRIO回环测试与时序优化实战

做FPGA高速接口这一年多&#xff0c;SRIO&#xff08;Serial RapidIO&#xff09;是我觉得最值得花时间啃的一块硬骨头。不少朋友在群里问我&#xff0c;SRIO回环测试到底怎么搭、时序报红怎么查、IP核配置那么多选项到底怎么选。这期就把我实际调试SRIO的经验完整复盘一遍&…

作者头像 李华
网站建设 2026/10/7 10:49:48

青少年开源入门指南:从认识开源到贡献PR的完整成长路径

1. 一场关于“未来开发者”的论坛&#xff0c;到底在聊什么 COSCon‘25 的青少年开源论坛议程正式发布之后&#xff0c;我在开源社区群里看到不少朋友转发。有人感慨“终于有人认真带着孩子玩开源了”&#xff0c;也有人问“这些议程到底适合多大的孩子”。作为一个常年混迹开源…

作者头像 李华
网站建设 2026/10/7 10:49:32

Altium Designer差分对规则配置底层逻辑与实战避坑指南

1. 差分对不是“画两根线”&#xff1a;从信号完整性本质理解AD中规则配置的底层逻辑很多人第一次在Altium Designer里设置差分对&#xff0c;习惯性地打开PCB Rules & Constraints Editor&#xff0c;找到Differential Pairs Routing&#xff0c;点开就填个线宽、间距、长…

作者头像 李华
网站建设 2026/10/7 10:49:31

跨Git仓库迁移部分代码并保留提交历史的完整指南

上周有个同事跑来找我&#xff0c;说他那个维护了两年多的老项目里&#xff0c;有一套做权限校验的代码&#xff0c;现在新项目也要用&#xff0c;能不能直接从旧仓库把这块代码搬过去。我第一反应是问他&#xff1a;你们要不要保留提交历史&#xff1f;他说当然要&#xff0c;…

作者头像 李华
网站建设 2026/10/7 10:49:26

SSM薪酬管理系统实战:数据库设计、薪资计算与部署调试全解析

接手过不少类似的项目&#xff0c;但每次看到“SSM薪酬管理系统”这种标题&#xff0c;都还是觉得值得聊一聊。这类系统在课程设计、毕业设计里出现频率极高&#xff0c;企业实际开发里也经常拿来当基础框架用。说它简单吧&#xff0c;CRUD一把梭好像就能交差&#xff1b;说它难…

作者头像 李华