一、MyBatis环境搭建
1. 引入相关Maven依赖
在 pom.xml 中引入 MyBatis + 数据库驱动(本文使用MySQL为例):
<dependencies>
<!-- MyBatis 核心依赖 -->
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.5.15</version>
</dependency>
<!-- MyBatis 与 SpringBoot 整合 -->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>3.0.3</version>
</dependency>
<!-- MySQL 驱动 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.33</version>
</dependency>
<!-- Lombok(简化实体类) -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.32</version>
</dependency>
</dependencies>
2. 数据库连接配置(application.yml)
spring:
datasource:
url: jdbc:mysql://localhost:3306/mybatis_demo?useSSL=false&serverTimezone=UTC
username: root
password: root
driver-class-name: com.mysql.cj.jdbc.Driver
mybatis:
mapper-locations: classpath:mapper/*.xml
type-aliases-package: com.example.demo.domain二、准备测试表(单表 & 多表)
以下建表语句和相关的测试数据由ChatGPT5生成。
-- 建库
CREATE DATABASE IF NOT EXISTS mybatis_demo DEFAULT CHARSET utf8mb4;
USE mybatis_demo;
-- 用户表
DROP TABLE IF EXISTS user;
CREATE TABLE user (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
username VARCHAR(50) NOT NULL,
email VARCHAR(100),
age INT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- 订单表
DROP TABLE IF EXISTS orders;
CREATE TABLE orders (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
user_id BIGINT NOT NULL,
order_no VARCHAR(50) NOT NULL,
amount DECIMAL(10,2),
status TINYINT DEFAULT 0, -- 0:待支付 1:已支付 2:已取消
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES user(id)
);
-- 产品表
DROP TABLE IF EXISTS product;
CREATE TABLE product (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(100) NOT NULL,
price DECIMAL(10,2) NOT NULL,
category VARCHAR(50)
);
-- 订单明细表
DROP TABLE IF EXISTS order_item;
CREATE TABLE order_item (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
order_id BIGINT NOT NULL,
product_id BIGINT NOT NULL,
quantity INT NOT NULL,
item_amount DECIMAL(10,2) GENERATED ALWAYS AS (quantity * 1.0) VIRTUAL,
FOREIGN KEY (order_id) REFERENCES orders(id),
FOREIGN KEY (product_id) REFERENCES product(id)
);
-- 测试数据
INSERT INTO user (username,email,age) VALUES
(\'alice\',\'alice@example.com\',23),
(\'bob\',\'bob@example.com\',28),
(\'charlie\',\'charlie@example.com\',31),
(\'dora\',\'dora@example.com\',26);
INSERT INTO product (name,price,category) VALUES
(\'iPhone 15\', 8999.00, \'phone\'),
(\'iPad Air\', 4999.00, \'tablet\'),
(\'ThinkPad X1\', 12999.00, \'laptop\'),
(\'AirPods Pro\', 1999.00, \'audio\'),
(\'Kindle Scribe\', 2999.00, \'reader\');
INSERT INTO orders (user_id, order_no, amount, status) VALUES
(1, \'A202501-0001\', 1999.00, 1),
(1, \'A202501-0002\', 10998.00, 1),
(2, \'B202501-0003\', 4999.00, 0),
(3, \'C202501-0004\', 12999.00, 2),
(3, \'C202501-0005\', 1999.00, 1);
INSERT INTO order_item (order_id, product_id, quantity) VALUES
(1, 4, 1), -- alice 买 AirPods Pro
(2, 1, 1), (2, 2, 1), -- alice 买 iPhone + iPad
(3, 2, 1), -- bob 预下 iPad
(4, 3, 1), -- charlie 取消 ThinkPad X1
(5, 4, 1); -- charlie 买 AirPods设计以上几个表可以覆盖:
- 单表查询 →
user - 一对多 →
user和orders - 多对多 →
orders和product通过order_item
三、使用MyBatis进行CRUD回顾
1. 使用Lombok快速编写实体类
// User.java
@Data
public class User {
private Long id;
private String username;
private String email;
private Integer age;
private LocalDateTime createdAt;
private List<Order> orders; // 一对多
}
// Order.java
@Data
public class Order {
private Long id;
private Long userId;
private String orderNo;
private BigDecimal amount;
private Integer status;
private LocalDateTime createdAt;
private User user; // 一对一(下单用户)
private List<Product> products; // 多对多(通过明细)
}
// Product.java
@Data
public class Product {
private Long id;
private String name;
private BigDecimal price;
private String category;
}
// OrderItem.java
@Data
public class OrderItem {
private Long id;
private Long orderId;
private Long productId;
private Integer quantity;
}四、Mapper 接口定义
public interface UserMapper {
User selectById(Long id);
List<User> selectAll();
int insert(User u);
int update(User u);
int delete(Long id);
// 动态查询
List<User> query(@Param(\"username\") String username,
@Param(\"minAge\") Integer minAge,
@Param(\"maxAge\") Integer maxAge,
@Param(\"ids\") List<Long> ids);
// 一对多(结果集映射)
User selectUserWithOrders(Long id);
// 分页(简单 LIMIT)
List<User> page(@Param(\"offset\") int offset, @Param(\"size\") int size);
}
public interface OrderMapper {
Order selectOrderWithUser(Long id); // association 嵌套对象
Order selectOrderWithProducts(Long id); // collection 多对多
int insert(Order o);
int insertUseSelectKey(Order o);
}
public interface ProductMapper {
List<Product> selectByCategory(@Param(\"category\") String category);
}五、使用XML标签编写SQL语句
放在
resources/mapper/*.xml。以下示例以UserMapper.xml/OrderMapper.xml为主,每个常用标签都有最小可运行示例。
0)顶层与命名空间:<mapper namespace=\"...\">
<!-- UserMapper.xml -->
<mapper namespace=\"com.example.demo.mapper.UserMapper\">
<!-- 具体语句见下 -->
</mapper>- namespace 必须与接口全限定名一致,方法名与子标签
id对应。
1)基础 CRUD:<select>/<insert>/<update>/<delete>
<!-- resultType 可用别名(配置了 type-aliases-package) -->
<select id=\"selectById\" parameterType=\"long\" resultType=\"User\">
SELECT id, username, email, age, created_at
FROM user
WHERE id = #{id}
</select>
<select id=\"selectAll\" resultType=\"User\">
SELECT id, username, email, age, created_at FROM user
</select>
<insert id=\"insert\" parameterType=\"User\" useGeneratedKeys=\"true\" keyProperty=\"id\">
INSERT INTO user (username, email, age)
VALUES (#{username}, #{email}, #{age})
</insert>
<update id=\"update\" parameterType=\"User\">
UPDATE user
<set>
<if test=\"username!=null\">username=#{username},</if>
<if test=\"email!=null\">email=#{email},</if>
<if test=\"age!=null\">age=#{age},</if>
</set>
WHERE id=#{id}
</update>
<delete id=\"delete\" parameterType=\"long\">
DELETE FROM user WHERE id=#{id}
</delete>要点:
useGeneratedKeys + keyProperty:MySQL/JDBC 自动回填主键。parameterType/resultType:参数/返回类型(类名或别名)。
2)结果映射:<resultMap>、<id>、<result>、<association>、<collection>、ofType、property、column
<!-- 一对多:用户 + 订单列表 -->
<resultMap id=\"UserWithOrdersRM\" type=\"User\">
<id property=\"id\" column=\"u_id\"/>
<result property=\"username\" column=\"username\"/>
<result property=\"email\" column=\"email\"/>
<result property=\"age\" column=\"age\"/>
<collection property=\"orders\" ofType=\"Order\">
<id property=\"id\" column=\"o_id\"/>
<result property=\"orderNo\" column=\"order_no\"/>
<result property=\"amount\" column=\"amount\"/>
<result property=\"status\" column=\"status\"/>
<result property=\"createdAt\" column=\"o_created_at\"/>
</collection>
</resultMap>
<select id=\"selectUserWithOrders\" resultMap=\"UserWithOrdersRM\">
SELECT
u.id u_id, u.username, u.email, u.age,
o.id o_id, o.order_no, o.amount, o.status, o.created_at o_created_at
FROM user u
LEFT JOIN orders o ON u.id = o.user_id
WHERE u.id = #{id}
</select>要点:
property:Java 字段名;column:SQL 列或别名。ofType:集合元素类型(常用于<collection>)。
3)嵌套查询(分步查询)与懒加载:association select / collection select、fetchType
<!-- OrderMapper.xml -->
<resultMap id=\"OrderWithUserRM\" type=\"Order\">
<id property=\"id\" column=\"id\"/>
<result property=\"orderNo\" column=\"order_no\"/>
<result property=\"amount\" column=\"amount\"/>
<association property=\"user\" javaType=\"User\"
column=\"user_id\" select=\"com.example.demo.mapper.UserMapper.selectById\"
fetchType=\"lazy\"/>
</resultMap>
<select id=\"selectOrderWithUser\" resultMap=\"OrderWithUserRM\">
SELECT id, user_id, order_no, amount FROM orders WHERE id=#{id}
</select>要点:
select=\"namespace.method\":把某列(通过column指定)作为参数传给另一个select。- 配合全局
lazy-loading-enabled: true可懒加载,避免一次性拉大对象图。
4)多对多(订单 → 产品列表):<collection> + 连接查询
<!-- OrderMapper.xml -->
<resultMap id=\"OrderWithProductsRM\" type=\"Order\">
<id property=\"id\" column=\"o_id\"/>
<result property=\"orderNo\" column=\"order_no\"/>
<result property=\"amount\" column=\"amount\"/>
<collection property=\"products\" ofType=\"Product\">
<id property=\"id\" column=\"p_id\"/>
<result property=\"name\" column=\"p_name\"/>
<result property=\"price\" column=\"price\"/>
<result property=\"category\" column=\"category\"/>
</collection>
</resultMap>
<select id=\"selectOrderWithProducts\" resultMap=\"OrderWithProductsRM\">
SELECT
o.id o_id, o.order_no, o.amount,
p.id p_id, p.name p_name, p.price, p.category
FROM orders o
LEFT JOIN order_item oi ON o.id=oi.order_id
LEFT JOIN product p ON oi.product_id=p.id
WHERE o.id=#{id}
</select>5)SQL 片段复用:<sql> + <include>
<sql id=\"UserColumns\">
id, username, email, age, created_at
</sql>
<select id=\"page\" resultType=\"User\">
SELECT <include refid=\"UserColumns\"/>
FROM user
ORDER BY id DESC
LIMIT #{offset}, #{size}
</select>6)动态 SQL(开发中使用较多)
6.1 <if> + <where>:自动处理 AND/OR
<select id=\"query\" resultType=\"User\">
SELECT id, username, email, age, created_at
FROM user
<where>
<if test=\"username != null and username != \'\'\">
AND username LIKE CONCAT(\'%\', #{username}, \'%\')
</if>
<if test=\"minAge != null\"> AND age >= #{minAge}</if>
<if test=\"maxAge != null\"> AND age <= #{maxAge}</if>
<if test=\"ids != null and ids.size() > 0\">
AND id IN
<foreach collection=\"ids\" item=\"id\" open=\"(\" separator=\",\" close=\")\">
#{id}
</foreach>
</if>
</where>
</select>6.2 <choose>/<when>/<otherwise>:分支
<select id=\"chooseExample\" resultType=\"User\">
SELECT id, username, email, age, created_at
FROM user
<where>
<choose>
<when test=\"username != null\"> username = #{username} </when>
<when test=\"minAge != null\"> age >= #{minAge} </when>
<otherwise> age IS NOT NULL </otherwise>
</choose>
</where>
</select>6.3 <foreach>:遍历 List / Map
<!-- List 参数:ids -->
<select id=\"selectByIds\" resultType=\"User\">
SELECT id, username, email, age, created_at
FROM user
WHERE id IN
<foreach collection=\"ids\" item=\"id\" open=\"(\" separator=\",\" close=\")\">
#{id}
</foreach>
</select>
<!-- Map 参数:map.put(\"k1\",\"v1\") -->
<select id=\"foreachMap\" resultType=\"map\">
SELECT #{map[\'k1\']} AS k1_value
</select>6.4 <set>:动态 UPDATE 字段
<update id=\"dynamicUpdate\" parameterType=\"User\">
UPDATE user
<set>
<if test=\"username!=null\"> username=#{username},</if>
<if test=\"email!=null\"> email=#{email},</if>
<if test=\"age!=null\"> age=#{age},</if>
</set>
WHERE id=#{id}
</update>6.5 <trim>:灵活控制前后缀
<update id=\"updateWithTrim\" parameterType=\"User\">
UPDATE user
<trim prefix=\"SET\" suffixOverrides=\",\">
<if test=\"username!=null\"> username=#{username},</if>
<if test=\"email!=null\"> email=#{email},</if>
<if test=\"age!=null\"> age=#{age},</if>
</trim>
WHERE id=#{id}
</update>6.6 <bind>:绑定变量(常用于 LIKE)
<select id=\"likeQuery\" resultType=\"User\">
<bind name=\"kw\" value=\"\'%\' + _parameter + \'%\'\" />
SELECT id, username, email, age, created_at
FROM user
WHERE username LIKE #{kw}
</select>传入参数是一个字符串时,
_parameter指它本身。
7)主键策略:useGeneratedKeys 与 <selectKey>
<!-- MySQL 推荐:useGeneratedKeys -->
<insert id=\"insert\" parameterType=\"User\" useGeneratedKeys=\"true\" keyProperty=\"id\">
INSERT INTO user (username,email,age) VALUES (#{username},#{email},#{age})
</insert>
<!-- 兼容序列的写法(例如 Oracle / PG 有 sequence) -->
<insert id=\"insertUseSelectKey\" parameterType=\"Order\">
<selectKey keyProperty=\"id\" resultType=\"long\" order=\"BEFORE\">
SELECT nextval(\'order_seq\')
</selectKey>
INSERT INTO orders (id, user_id, order_no, amount, status)
VALUES (#{id}, #{userId}, #{orderNo}, #{amount}, #{status})
</insert>8)构造器映射 & 鉴别器:<constructor>、<discriminator>
<!-- 用构造器给不可变对象赋值 -->
<resultMap id=\"UserCtorRM\" type=\"User\">
<constructor>
<idArg column=\"id\" javaType=\"long\"/>
<arg column=\"username\" javaType=\"string\"/>
<arg column=\"email\" javaType=\"string\"/>
<arg column=\"age\" javaType=\"int\"/>
</constructor>
</resultMap>
<!-- discriminator:根据列值映射不同结构(示例性) -->
<resultMap id=\"OrderWithStatusRM\" type=\"Order\">
<id property=\"id\" column=\"id\"/>
<result property=\"orderNo\" column=\"order_no\"/>
<result property=\"status\" column=\"status\"/>
<discriminator javaType=\"int\" column=\"status\">
<case value=\"0\">
<result property=\"amount\" column=\"amount\"/>
</case>
<case value=\"1\">
<result property=\"amount\" column=\"amount\"/>
</case>
<case value=\"2\">
<result property=\"amount\" column=\"amount\"/>
</case>
</discriminator>
</resultMap>
<select id=\"selectOrderDiscriminator\" resultMap=\"OrderWithStatusRM\">
SELECT id, order_no, amount, status FROM orders WHERE id=#{id}
</select>9)存储过程 / 多结果集:statementType=\"CALLABLE\"、resultSets(知道即可)
<select id=\"callProc\" statementType=\"CALLABLE\">
{ call proc_recalc_order_amount(#{orderId, mode=IN}) }
</select>多结果集时可用
resultSets=\"rs1,rs2\"配合多个resultMap(不同库支持度差异较大)。
10)缓存:<cache>、<cache-ref>(二级缓存)
<!-- 当前 mapper 开启二级缓存 -->
<cache eviction=\"LRU\" flushInterval=\"60000\" size=\"512\" readOnly=\"false\"/>
<!-- 引用别的命名空间的缓存 -->
<!-- <cache-ref namespace=\"com.example.demo.mapper.OtherMapper\"/> -->要点:
- 二级缓存是跨 SqlSession 的;更新会清理对应命名空间的缓存。
- 生产环境慎用,注意与数据一致性、事务提交时机的关系。
11)数据库差异:databaseId(同一语句不同方言)
<select id=\"selectNow\" resultType=\"string\" databaseId=\"mysql\">
SELECT DATE_FORMAT(NOW(), \'%Y-%m-%d %H:%i:%s\')
</select>
<select id=\"selectNow\" resultType=\"string\" databaseId=\"postgresql\">
SELECT TO_CHAR(NOW(), \'YYYY-MM-DD HH24:MI:SS\')
</select>需在全局配置里开启
databaseIdProvider(Spring Boot 多用 starter 自动识别)。
12)resultType 的多种返回:POJO / Map / 基本类型
<!-- 返回 Map -->
<select id=\"countByStatus\" resultType=\"map\">
SELECT status, COUNT(*) cnt FROM orders GROUP BY status
</select>
<!-- 返回基本类型 -->
<select id=\"countUser\" resultType=\"long\">
SELECT COUNT(*) FROM user
</select>六、综合实战片段(把常用点串一遍)
<!-- UserMapper.xml -->
<mapper namespace=\"com.example.demo.mapper.UserMapper\">
<cache eviction=\"LRU\" size=\"256\"/>
<sql id=\"BaseCols\">id, username, email, age, created_at</sql>
<select id=\"selectById\" parameterType=\"long\" resultType=\"User\">
SELECT <include refid=\"BaseCols\"/> FROM user WHERE id=#{id}
</select>
<select id=\"selectAll\" resultType=\"User\">
SELECT <include refid=\"BaseCols\"/> FROM user ORDER BY id
</select>
<insert id=\"insert\" parameterType=\"User\" useGeneratedKeys=\"true\" keyProperty=\"id\">
INSERT INTO user (username,email,age) VALUES (#{username},#{email},#{age})
</insert>
<update id=\"dynamicUpdate\" parameterType=\"User\">
UPDATE user
<trim prefix=\"SET\" suffixOverrides=\",\">
<if test=\"username!=null\">username=#{username},</if>
<if test=\"email!=null\">email=#{email},</if>
<if test=\"age!=null\">age=#{age},</if>
</trim>
WHERE id=#{id}
</update>
<delete id=\"delete\" parameterType=\"long\">
DELETE FROM user WHERE id=#{id}
</delete>
<select id=\"query\" resultType=\"User\">
SELECT <include refid=\"BaseCols\"/> FROM user
<where>
<if test=\"username!=null and username!=\'\'\">
AND username LIKE CONCAT(\'%\', #{username}, \'%\')
</if>
<if test=\"minAge!=null\"> AND age >= #{minAge}</if>
<if test=\"maxAge!=null\"> AND age <= #{maxAge}</if>
<if test=\"ids!=null and ids.size()>0\">
AND id IN
<foreach collection=\"ids\" item=\"id\" open=\"(\" separator=\",\" close=\")\">
#{id}
</foreach>
</if>
</where>
</select>
<!-- 一对多 -->
<resultMap id=\"UserWithOrdersRM\" type=\"User\">
<id property=\"id\" column=\"u_id\"/>
<result property=\"username\" column=\"username\"/>
<result property=\"email\" column=\"email\"/>
<result property=\"age\" column=\"age\"/>
<collection property=\"orders\" ofType=\"Order\">
<id property=\"id\" column=\"o_id\"/>
<result property=\"orderNo\" column=\"order_no\"/>
<result property=\"amount\" column=\"amount\"/>
<result property=\"status\" column=\"status\"/>
<result property=\"createdAt\" column=\"o_created_at\"/>
</collection>
</resultMap>
<select id=\"selectUserWithOrders\" resultMap=\"UserWithOrdersRM\">
SELECT
u.id u_id, u.username, u.email, u.age,
o.id o_id, o.order_no, o.amount, o.status, o.created_at o_created_at
FROM user u
LEFT JOIN orders o ON u.id = o.user_id
WHERE u.id = #{id}
</select>
<!-- 分页 -->
<select id=\"page\" resultType=\"User\">
SELECT <include refid=\"BaseCols\"/> FROM user
ORDER BY id DESC
LIMIT #{offset}, #{size}
</select>
</mapper><!-- OrderMapper.xml -->
<mapper namespace=\"com.example.demo.mapper.OrderMapper\">
<!-- association:订单带用户(分步 + 懒加载) -->
<resultMap id=\"OrderWithUserRM\" type=\"Order\">
<id property=\"id\" column=\"id\"/>
<result property=\"orderNo\" column=\"order_no\"/>
<result property=\"amount\" column=\"amount\"/>
<association property=\"user\" javaType=\"User\"
column=\"user_id\" select=\"com.example.demo.mapper.UserMapper.selectById\"
fetchType=\"lazy\"/>
</resultMap>
<select id=\"selectOrderWithUser\" resultMap=\"OrderWithUserRM\">
SELECT id, user_id, order_no, amount FROM orders WHERE id=#{id}
</select>
<!-- 多对多:订单带产品列表 -->
<resultMap id=\"OrderWithProductsRM\" type=\"Order\">
<id property=\"id\" column=\"o_id\"/>
<result property=\"orderNo\" column=\"order_no\"/>
<result property=\"amount\" column=\"amount\"/>
<collection property=\"products\" ofType=\"Product\">
<id property=\"id\" column=\"p_id\"/>
<result property=\"name\" column=\"p_name\"/>
<result property=\"price\" column=\"price\"/>
<result property=\"category\" column=\"category\"/>
</collection>
</resultMap>
<select id=\"selectOrderWithProducts\" resultMap=\"OrderWithProductsRM\">
SELECT
o.id o_id, o.order_no, o.amount,
p.id p_id, p.name p_name, p.price, p.category
FROM orders o
LEFT JOIN order_item oi ON o.id=oi.order_id
LEFT JOIN product p ON oi.product_id=p.id
WHERE o.id=#{id}
</select>
<!-- insert:主键回填 -->
<insert id=\"insert\" parameterType=\"Order\" useGeneratedKeys=\"true\" keyProperty=\"id\">
INSERT INTO orders (user_id, order_no, amount, status)
VALUES (#{userId}, #{orderNo}, #{amount}, #{status})
</insert>
<!-- insert:selectKey 示例(非 MySQL 必要) -->
<insert id=\"insertUseSelectKey\" parameterType=\"Order\">
<selectKey keyProperty=\"id\" resultType=\"long\" order=\"BEFORE\">
SELECT nextval(\'order_seq\')
</selectKey>
INSERT INTO orders (id, user_id, order_no, amount, status)
VALUES (#{id}, #{userId}, #{orderNo}, #{amount}, #{status})
</insert>
</mapper>七、事务实战(Service 层)
@Service
@RequiredArgsConstructor
public class OrderService {
private final OrderMapper orderMapper;
private final SqlSessionFactory sqlSessionFactory;
@Transactional
public Long createOrder(Order order, List<OrderItem> items) {
orderMapper.insert(order); // 回填 order.id
try (SqlSession session = sqlSessionFactory.openSession()) {
for (OrderItem it : items) {
session.insert(\"com.example.demo.mapper.OrderItemMapper.insert\", it);
}
// 模拟异常可测试回滚
// int x = 1/0;
}
return order.getId();
}
}
八、常见的坑
- 命名参数:多参数场景务必用
@Param指定名称,与 XML 中占位符一致。 - 驼峰映射:开启
map-underscore-to-camel-case或 SQL 中加别名。 - N+1:能用联表就联表,分步查询配合懒加载谨慎使用。
- 分页:MySQL 直接
LIMIT;生产建议用分页插件(PageHelper / MyBatis-Plus)。 - 缓存:二级缓存注意一致性与事务提交时机。
- 动态 SQL:优先
<where>/<set>/<trim>,避免字符串拼接。 - 返回 Map:
resultType=\"map\"可快速做统计类查询。 - 数据库差异:
databaseId+ 多语句兼容不同方言。
评论