MyBatis 完整教程
MyBatis 完整教程
目錄
- MyBatis 簡(jiǎn)介
- 環(huán)境搭建
- 核心概念
- 注解方式開發(fā)
- XML方式開發(fā)
- 動(dòng)態(tài)SQL
- 結(jié)果映射
- 參數(shù)傳遞
- 完整實(shí)戰(zhàn)案例
- 最佳實(shí)踐
1. MyBatis 簡(jiǎn)介
MyBatis 是一款優(yōu)秀的持久層框架,它支持:
- 自定義 SQL
- 存儲(chǔ)過程
- 高級(jí)映射
優(yōu)勢(shì):
- 避免了幾乎所有的 JDBC 代碼
- 手動(dòng)設(shè)置參數(shù)和獲取結(jié)果集的工作
- 靈活的 SQL 編寫
- 注解和 XML 兩種配置方式
2. 環(huán)境搭建
2.1 Maven 依賴
<dependencies>
<!-- MyBatis Spring Boot Starter -->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>3.0.3</version>
</dependency>
<!-- MySQL 驅(qū)動(dòng) -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.29</version>
</dependency>
<!-- Lombok (可選,但推薦) -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
2.2 配置文件 (application.yml)
spring:
datasource:
url: jdbc:mysql://localhost:3306/mybatis_demo?useSSL=false&serverTimezone=UTC&characterEncoding=UTF-8
username: root
password: your_password
driver-class-name: com.mysql.cj.jdbc.Driver
# MyBatis 配置
mybatis:
# XML 文件位置 (classpath: = src/main/resources/)
# 實(shí)際對(duì)應(yīng): src/main/resources/mapper/*.xml
mapper-locations: classpath:mapper/*.xml
# 實(shí)體類包路徑 (配置后XML中可直接用類名,無需寫完整包名)
# 例如: resultType="User" 而不是 resultType="com.example.demo.domain.User"
type-aliases-package: com.example.demo.domain
configuration:
# 駝峰命名自動(dòng)轉(zhuǎn)換:數(shù)據(jù)庫(kù)下劃線命名 ? Java駝峰命名
# 數(shù)據(jù)庫(kù)字段: user_name, created_at, order_no
# Java屬性: userName, createdAt, orderNo
# 開啟后無需手動(dòng)映射,MyBatis自動(dòng)完成轉(zhuǎn)換
map-underscore-to-camel-case: true
# SQL 日志實(shí)現(xiàn)(開發(fā)時(shí)建議開啟,生產(chǎn)環(huán)境建議關(guān)閉)
# STDOUT_LOGGING - 標(biāo)準(zhǔn)輸出到控制臺(tái)
# SLF4J - 使用 SLF4J 日志框架(推薦)
# LOG4J2 - 使用 Log4j2
# NO_LOGGING - 不輸出日志
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
# 其他常用配置:
# cache-enabled: true # 開啟二級(jí)緩存(默認(rèn)true)
# lazy-loading-enabled: false # 延遲加載(默認(rèn)false)
# default-executor-type: SIMPLE # 執(zhí)行器類型:SIMPLE/REUSE/BATCH
# default-statement-timeout: 25 # 超時(shí)時(shí)間(秒)
# jdbc-type-for-null: NULL # 空值對(duì)應(yīng)的JDBC類型
2.3 主啟動(dòng)類
package com.example.demo;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
@MapperScan("com.example.demo.mapper") // 掃描 Mapper 接口
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
}
3. 核心概念
3.1 三層架構(gòu)
Controller (控制層)
↓
Service (業(yè)務(wù)層)
↓
Mapper/DAO (持久層) ← MyBatis 在這里工作
↓
Database (數(shù)據(jù)庫(kù))
3.2 核心組件
| 組件 | 說明 |
|---|---|
| Domain/Entity | 實(shí)體類,對(duì)應(yīng)數(shù)據(jù)庫(kù)表 |
| Mapper接口 | 定義數(shù)據(jù)庫(kù)操作方法 |
| Mapper.xml | SQL 語句配置文件 |
| SqlSession | 執(zhí)行SQL的會(huì)話對(duì)象(Spring Boot自動(dòng)管理) |
4. 注解方式開發(fā)
4.1 創(chuàng)建數(shù)據(jù)庫(kù)表
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,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);
INSERT INTO user (username, email, age) VALUES
('張三', 'zhangsan@example.com', 25),
('李四', 'lisi@example.com', 30),
('王五', 'wangwu@example.com', 28);
4.2 創(chuàng)建實(shí)體類
package com.example.demo.domain;
import lombok.Data;
import java.time.LocalDateTime;
@Data
public class User {
private Long id;
private String username;
private String email;
private Integer age;
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
}
4.3 創(chuàng)建 Mapper 接口(注解方式)
package com.example.demo.mapper;
import com.example.demo.domain.User;
import org.apache.ibatis.annotations.*;
import java.util.List;
@Mapper
public interface UserMapper {
// ========== 查詢操作 ==========
/**
* 查詢所有用戶
*/
@Select("SELECT * FROM user")
List<User> findAll();
/**
* 根據(jù)ID查詢用戶
* @Param("id") - 指定參數(shù)在SQL中的名稱,單個(gè)參數(shù)可省略,多個(gè)參數(shù)必須加
*/
@Select("SELECT * FROM user WHERE id = #{id}")
User findById(@Param("id") Long id);
/**
* 根據(jù)用戶名查詢
*/
@Select("SELECT * FROM user WHERE username = #{username}")
User findByUsername(@Param("username") String username);
/**
* 條件查詢(多參數(shù))
*/
@Select("SELECT * FROM user WHERE age >= #{minAge} AND age <= #{maxAge}")
List<User> findByAgeRange(@Param("minAge") Integer minAge,
@Param("maxAge") Integer maxAge);
/**
* 模糊查詢
*/
@Select("SELECT * FROM user WHERE username LIKE CONCAT('%', #{keyword}, '%')")
List<User> searchByUsername(@Param("keyword") String keyword);
/**
* 統(tǒng)計(jì)數(shù)量
*/
@Select("SELECT COUNT(*) FROM user")
int count();
/**
* 分頁(yè)查詢
*/
@Select("SELECT * FROM user LIMIT #{limit} OFFSET #{offset}")
List<User> findByPage(@Param("offset") int offset,
@Param("limit") int limit);
// ========== 插入操作 ==========
/**
* 插入用戶(返回受影響行數(shù))
*/
@Insert("INSERT INTO user (username, email, age) VALUES (#{username}, #{email}, #{age})")
int insert(User user);
/**
* 插入用戶(返回自增主鍵)
* @Options 配置項(xiàng)說明:
* - useGeneratedKeys = true: 使用數(shù)據(jù)庫(kù)自增主鍵
* - keyProperty = "id": 將自增的主鍵值回填到 user 對(duì)象的 id 屬性
* 插入后,user.getId() 就能獲取到數(shù)據(jù)庫(kù)自動(dòng)生成的ID
*
* 參數(shù)映射規(guī)則:#{屬性名} 會(huì)調(diào)用對(duì)象的 getter 方法
* #{username} → user.getUsername()
* #{email} → user.getEmail()
* #{age} → user.getAge()
*/
@Insert("INSERT INTO user (username, email, age) VALUES (#{username}, #{email}, #{age})")
@Options(useGeneratedKeys = true, keyProperty = "id")
int insertAndReturnId(User user);
// ========== 更新操作 ==========
/**
* 更新用戶信息
*/
@Update("UPDATE user SET username = #{username}, email = #{email}, age = #{age} WHERE id = #{id}")
int update(User user);
/**
* 更新部分字段
*/
@Update("UPDATE user SET email = #{email} WHERE id = #{id}")
int updateEmail(@Param("id") Long id, @Param("email") String email);
// ========== 刪除操作 ==========
/**
* 根據(jù)ID刪除
*/
@Delete("DELETE FROM user WHERE id = #{id}")
int deleteById(@Param("id") Long id);
/**
* 根據(jù)條件刪除
*/
@Delete("DELETE FROM user WHERE age < #{age}")
int deleteByAge(@Param("age") Integer age);
}
?? 返回類型說明
MyBatis 如何確定返回類型?
關(guān)鍵:通過方法簽名的返回類型聲明,而不是 SQL 語句!
MyBatis 會(huì)根據(jù)你在 Mapper 接口方法中聲明的返回類型,自動(dòng)處理查詢結(jié)果的映射。
常見返回類型對(duì)比:
| 返回類型 | 說明 | 使用場(chǎng)景 | 示例方法 |
|---|---|---|---|
User |
返回單個(gè)對(duì)象 | 按唯一鍵查詢(ID、username等),預(yù)期0或1條結(jié)果 | findById, findByUsername |
List<User> |
返回集合 | 條件查詢,可能返回0條、1條或多條結(jié)果 | findAll, findByAgeRange |
int/long |
返回?cái)?shù)值 | 統(tǒng)計(jì)查詢、增刪改操作返回受影響行數(shù) | count(), insert(), update() |
Map<K,V> |
返回鍵值對(duì) | 返回單行的動(dòng)態(tài)列 | findUserAsMap() |
List<Map> |
返回Map集合 | 返回多行的動(dòng)態(tài)列 | findAllAsMap() |
重要說明:
// ? 返回 List<User> - 返回所有符合條件的記錄
@Select("SELECT * FROM user WHERE age >= #{minAge} AND age <= #{maxAge}")
List<User> findByAgeRange(@Param("minAge") Integer minAge,
@Param("maxAge") Integer maxAge);
// ?? 返回 User - 只返回第一條記錄,其他記錄被忽略
@Select("SELECT * FROM user WHERE age >= #{minAge} AND age <= #{maxAge}")
User findByAgeRange(@Param("minAge") Integer minAge,
@Param("maxAge") Integer maxAge);
區(qū)別:
- 返回
User:如果 SQL 查詢匹配了 10 條記錄,MyBatis 只會(huì)返回第一條,其他 9 條被丟棄;如果沒有結(jié)果則返回null - 返回
List<User>:會(huì)返回所有匹配的記錄(空列表、1條或多條)
選擇原則:
-
確定只有一條結(jié)果 → 用
User// ID是主鍵,結(jié)果唯一 @Select("SELECT * FROM user WHERE id = #{id}") User findById(@Param("id") Long id); -
可能有多條結(jié)果 → 用
List<User>// 年齡范圍查詢,可能匹配多個(gè)用戶 @Select("SELECT * FROM user WHERE age >= #{minAge} AND age <= #{maxAge}") List<User> findByAgeRange(@Param("minAge") Integer minAge, @Param("maxAge") Integer maxAge); -
增刪改操作 → 用
int/long(返回受影響行數(shù))@Insert("INSERT INTO user (username, email, age) VALUES (#{username}, #{email}, #{age})") int insert(User user); // 返回插入的行數(shù)(通常是1) @Update("UPDATE user SET email = #{email} WHERE id = #{id}") int updateEmail(@Param("id") Long id, @Param("email") String email); @Delete("DELETE FROM user WHERE age < #{age}") int deleteByAge(@Param("age") Integer age); // 返回刪除的行數(shù) -
統(tǒng)計(jì)查詢 → 用
int/long@Select("SELECT COUNT(*) FROM user") int count();
實(shí)際案例:
// ? 錯(cuò)誤示例:業(yè)務(wù)需要查詢所有符合條件的用戶,卻用了 User
@Select("SELECT * FROM user WHERE status = 'active'")
User findActiveUsers(); // 只會(huì)返回第1個(gè)活躍用戶,其他的丟失了!
// ? 正確示例:使用 List<User>
@Select("SELECT * FROM user WHERE status = 'active'")
List<User> findActiveUsers(); // 返回所有活躍用戶
總結(jié):
- 返回類型是開發(fā)者根據(jù)業(yè)務(wù)需求和查詢特點(diǎn)主動(dòng)聲明的
- MyBatis 不會(huì)自動(dòng)判斷結(jié)果數(shù)量來改變返回類型
- 選擇錯(cuò)誤的返回類型可能導(dǎo)致數(shù)據(jù)丟失或空指針異常
4.4 Service 層
package com.example.demo.service;
import com.example.demo.domain.User;
import com.example.demo.mapper.UserMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
@Service
public class UserService {
@Autowired
private UserMapper userMapper;
public List<User> getAllUsers() {
return userMapper.findAll();
}
public User getUserById(Long id) {
return userMapper.findById(id);
}
/**
* @Transactional 事務(wù)管理注解
* 作用:保證方法內(nèi)的所有數(shù)據(jù)庫(kù)操作要么全部成功,要么全部回滾
* - 方法執(zhí)行成功 → 自動(dòng)提交事務(wù)(commit)
* - 方法拋出異常 → 自動(dòng)回滾事務(wù)(rollback)
*/
@Transactional
public User createUser(User user) {
userMapper.insertAndReturnId(user);
return user; // id 已經(jīng)被自動(dòng)填充
}
@Transactional
public boolean updateUser(User user) {
return userMapper.update(user) > 0;
}
@Transactional
public boolean deleteUser(Long id) {
return userMapper.deleteById(id) > 0;
}
}
4.5 Controller 層
package com.example.demo.controller;
import com.example.demo.domain.User;
import com.example.demo.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("/api/users")
public class UserController {
@Autowired
private UserService userService;
@GetMapping
public List<User> getAllUsers() {
return userService.getAllUsers();
}
@GetMapping("/{id}")
public User getUserById(@PathVariable Long id) {
return userService.getUserById(id);
}
@PostMapping
public User createUser(@RequestBody User user) {
return userService.createUser(user);
}
@PutMapping("/{id}")
public boolean updateUser(@PathVariable Long id, @RequestBody User user) {
user.setId(id);
return userService.updateUser(user);
}
@DeleteMapping("/{id}")
public boolean deleteUser(@PathVariable Long id) {
return userService.deleteUser(id);
}
}
5. XML方式開發(fā)
5.1 數(shù)據(jù)庫(kù)表(訂單示例)
CREATE TABLE `order` (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
order_no VARCHAR(50) NOT NULL UNIQUE,
user_id BIGINT NOT NULL,
total_amount DECIMAL(10, 2),
status VARCHAR(20),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);
CREATE TABLE order_item (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
order_id BIGINT NOT NULL,
product_name VARCHAR(100),
quantity INT,
price DECIMAL(10, 2),
FOREIGN KEY (order_id) REFERENCES `order`(id)
);
5.2 實(shí)體類
package com.example.demo.domain;
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.List;
@Data
public class Order {
private Long id;
private String orderNo;
private Long userId;
private BigDecimal totalAmount;
private String status;
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
// 一對(duì)多關(guān)聯(lián)
private List<OrderItem> items;
}
@Data
public class OrderItem {
private Long id;
private Long orderId;
private String productName;
private Integer quantity;
private BigDecimal price;
}
5.3 Mapper 接口
package com.example.demo.mapper;
import com.example.demo.domain.Order;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.util.List;
@Mapper
public interface OrderMapper {
// 所有方法都在 XML 中實(shí)現(xiàn)
Order findById(@Param("id") Long id);
List<Order> findByUserId(@Param("userId") Long userId);
List<Order> findByCondition(@Param("status") String status,
@Param("minAmount") BigDecimal minAmount);
int insert(Order order);
int update(Order order);
int deleteById(@Param("id") Long id);
// 關(guān)聯(lián)查詢:查詢訂單及其所有訂單項(xiàng)
Order findByIdWithItems(@Param("id") Long id);
}
5.4 Mapper XML 配置
創(chuàng)建文件:src/main/resources/mapper/OrderMapper.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.demo.mapper.OrderMapper">
<!-- ========== ResultMap 結(jié)果映射 ========== -->
<!-- 基礎(chǔ)結(jié)果映射 -->
<resultMap id="BaseResultMap" type="com.example.demo.domain.Order">
<id property="id" column="id"/>
<result property="orderNo" column="order_no"/>
<result property="userId" column="user_id"/>
<result property="totalAmount" column="total_amount"/>
<result property="status" column="status"/>
<result property="createdAt" column="created_at"/>
<result property="updatedAt" column="updated_at"/>
</resultMap>
<!-- 帶關(guān)聯(lián)的結(jié)果映射 -->
<resultMap id="OrderWithItemsMap" type="com.example.demo.domain.Order" extends="BaseResultMap">
<!-- collection: 一對(duì)多關(guān)聯(lián) -->
<collection property="items" ofType="com.example.demo.domain.OrderItem">
<id property="id" column="item_id"/>
<result property="orderId" column="order_id"/>
<result property="productName" column="product_name"/>
<result property="quantity" column="quantity"/>
<result property="price" column="price"/>
</collection>
</resultMap>
<!-- ========== 字段映射說明 ========== -->
<!--
?? 如果 ResultMap 中有些字段沒有映射會(huì)怎么樣?
**規(guī)則:**
1. ? 已映射的字段:按照 ResultMap 配置進(jìn)行映射
2. ? 未映射的字段:該屬性值為 null(不會(huì)自動(dòng)映射,即使字段名一致)
**重要特性:一旦使用 ResultMap,自動(dòng)映射將失效!**
示例:假設(shè) Order 類有 7 個(gè)屬性
public class Order {
private Long id;
private String orderNo;
private Long userId;
private BigDecimal totalAmount;
private String status;
private LocalDateTime createdAt;
private LocalDateTime updatedAt; // ← 假設(shè)這個(gè)字段沒有在 ResultMap 中映射
}
如果 ResultMap 中沒有映射 updatedAt:
<resultMap id="IncompleteMap" type="Order">
<id property="id" column="id"/>
<result property="orderNo" column="order_no"/>
<result property="userId" column="user_id"/>
<result property="totalAmount" column="total_amount"/>
<result property="status" column="status"/>
<result property="createdAt" column="created_at"/>
<!-- updatedAt 沒有映射! -->
</resultMap>
結(jié)果:
- order.getId() → 有值 ?
- order.getOrderNo() → 有值 ?
- order.getUserId() → 有值 ?
- order.getTotalAmount()→ 有值 ?
- order.getStatus() → 有值 ?
- order.getCreatedAt() → 有值 ?
- order.getUpdatedAt() → null ?(即使數(shù)據(jù)庫(kù)有值,也不會(huì)自動(dòng)映射)
**三種解決方案:**
方案1:補(bǔ)全所有字段映射(推薦用于字段名不一致的情況)
<resultMap id="CompleteMap" type="Order">
<id property="id" column="id"/>
<result property="orderNo" column="order_no"/>
<result property="userId" column="user_id"/>
<result property="totalAmount" column="total_amount"/>
<result property="status" column="status"/>
<result property="createdAt" column="created_at"/>
<result property="updatedAt" column="updated_at"/> ← 補(bǔ)全
</resultMap>
方案2:開啟 autoMappingBehavior(推薦,最常用)
<resultMap id="BaseResultMap" type="Order" autoMapping="true">
<!-- 只映射特殊字段(如字段名不一致的) -->
<id property="id" column="id"/>
<result property="orderNo" column="order_no"/> <!-- 數(shù)據(jù)庫(kù)是 order_no,需要映射 -->
<!-- 其他字段名一致的字段會(huì)自動(dòng)映射 -->
</resultMap>
方案3:使用 resultType 而不是 resultMap(適合簡(jiǎn)單場(chǎng)景)
<!-- 當(dāng)字段名一致(或開啟了駝峰轉(zhuǎn)換)時(shí),直接用 resultType -->
<select id="findById" resultType="Order">
SELECT id, order_no, user_id, total_amount, status, created_at, updated_at
FROM `order`
WHERE id = #{id}
</select>
**?? 什么叫"字段名一致"?**
"一致"的定義取決于是否開啟了駝峰命名轉(zhuǎn)換(map-underscore-to-camel-case):
情況1:未開啟駝峰轉(zhuǎn)換(map-underscore-to-camel-case: false)
┌─────────────────┬──────────────┬────────┐
│ 數(shù)據(jù)庫(kù)字段名 │ Java屬性名 │ 是否一致│
├─────────────────┼──────────────┼────────┤
│ id │ id │ ? 一致 │
│ username │ username │ ? 一致 │
│ order_no │ orderNo │ ? 不一致(需手動(dòng)映射)│
│ created_at │ createdAt │ ? 不一致(需手動(dòng)映射)│
└─────────────────┴──────────────┴────────┘
結(jié)論:必須完全相同才算一致(包括大小寫、下劃線)
情況2:開啟駝峰轉(zhuǎn)換(map-underscore-to-camel-case: true)← 推薦配置
┌─────────────────┬──────────────┬────────┐
│ 數(shù)據(jù)庫(kù)字段名 │ Java屬性名 │ 是否一致│
├─────────────────┼──────────────┼────────┤
│ id │ id │ ? 一致 │
│ username │ username │ ? 一致 │
│ order_no │ orderNo │ ? 一致(自動(dòng)轉(zhuǎn)換)│
│ created_at │ createdAt │ ? 一致(自動(dòng)轉(zhuǎn)換)│
│ user_id │ userId │ ? 一致(自動(dòng)轉(zhuǎn)換)│
│ gmt_create │ createdAt │ ? 不一致(語義不同,需手動(dòng)映射)│
└─────────────────┴──────────────┴────────┘
結(jié)論:下劃線命名 ? 駝峰命名 自動(dòng)轉(zhuǎn)換,也算一致
**轉(zhuǎn)換規(guī)則詳解:**
order_no → orderNo (o_n → On)
user_name → userName (u_n → Un)
created_at → createdAt (c_a → Ca)
is_deleted → isDeleted (i_d → Id)
**實(shí)際示例對(duì)比:**
// 未開啟駝峰轉(zhuǎn)換時(shí)
<resultMap id="UserMap" type="User">
<result property="userName" column="user_name"/> <!-- 必須手動(dòng)映射 -->
<result property="createdAt" column="created_at"/> <!-- 必須手動(dòng)映射 -->
</resultMap>
// 開啟駝峰轉(zhuǎn)換后(推薦!)
<select id="findById" resultType="User">
SELECT id, user_name, created_at FROM user WHERE id = #{id}
<!-- user_name 自動(dòng)映射到 userName -->
<!-- created_at 自動(dòng)映射到 createdAt -->
</select>
**配置駝峰轉(zhuǎn)換:**
# application.yml
mybatis:
configuration:
map-underscore-to-camel-case: true ← 開啟此配置
開啟后的效果:
? 數(shù)據(jù)庫(kù)用下劃線命名(user_name, order_no)
? Java用駝峰命名(userName, orderNo)
? MyBatis自動(dòng)轉(zhuǎn)換,無需手動(dòng)映射
**最佳實(shí)踐:**
1. 如果所有字段都需要自定義映射 → 使用 <resultMap> 并映射所有字段
2. 如果只有部分字段需要映射 → 使用 <resultMap autoMapping="true">
3. 如果字段名一致(或已開啟駝峰轉(zhuǎn)換)→ 直接使用 resultType
4. 配置文件已開啟駝峰轉(zhuǎn)換時(shí)(map-underscore-to-camel-case: true):
- 數(shù)據(jù)庫(kù)字段 order_no → Java屬性 orderNo(自動(dòng)轉(zhuǎn)換)
- 數(shù)據(jù)庫(kù)字段 created_at → Java屬性 createdAt(自動(dòng)轉(zhuǎn)換)
**對(duì)比表:**
| 場(chǎng)景 | 使用方式 | 是否自動(dòng)映射 | 示例 |
|------|---------|-------------|------|
| 字段名完全一致 | resultType | ? 自動(dòng) | 數(shù)據(jù)庫(kù) id → Java id |
| 開啟駝峰轉(zhuǎn)換 | resultType | ? 自動(dòng) | 數(shù)據(jù)庫(kù) user_name → Java userName |
| 字段名不一致 | resultMap | ? 手動(dòng) | 數(shù)據(jù)庫(kù) gmt_create → Java createdAt |
| 復(fù)雜關(guān)聯(lián)查詢 | resultMap | ? 手動(dòng) | 一對(duì)多、多對(duì)多關(guān)聯(lián) |
| resultMap + autoMapping | resultMap autoMapping="true" | ?? 部分自動(dòng) | 特殊字段手動(dòng),其他自動(dòng) |
-->
<!-- ========== SQL 片段(可復(fù)用) ========== -->
<sql id="baseColumns">
id, order_no, user_id, total_amount, status, created_at, updated_at
</sql>
<sql id="whereCondition">
<where>
<if test="status != null and status != ''">
AND status = #{status}
</if>
<if test="minAmount != null">
AND total_amount >= #{minAmount}
</if>
</where>
</sql>
<!-- ========== 查詢操作 ========== -->
<!-- 根據(jù)ID查詢 -->
<select id="findById" parameterType="long" resultMap="BaseResultMap">
SELECT <include refid="baseColumns"/>
FROM `order`
WHERE id = #{id}
</select>
<!-- 根據(jù)用戶ID查詢 -->
<select id="findByUserId" parameterType="long" resultMap="BaseResultMap">
SELECT <include refid="baseColumns"/>
FROM `order`
WHERE user_id = #{userId}
ORDER BY created_at DESC
</select>
<!-- 條件查詢 -->
<select id="findByCondition" resultMap="BaseResultMap">
SELECT <include refid="baseColumns"/>
FROM `order`
<include refid="whereCondition"/>
ORDER BY created_at DESC
</select>
<!-- 關(guān)聯(lián)查詢:訂單 + 訂單項(xiàng) -->
<select id="findByIdWithItems" parameterType="long" resultMap="OrderWithItemsMap">
SELECT
o.id, o.order_no, o.user_id, o.total_amount, o.status,
o.created_at, o.updated_at,
oi.id AS item_id, oi.order_id, oi.product_name,
oi.quantity, oi.price
FROM `order` o
LEFT JOIN order_item oi ON o.id = oi.order_id
WHERE o.id = #{id}
</select>
<!-- ========== 插入操作 ========== -->
<insert id="insert" parameterType="com.example.demo.domain.Order"
useGeneratedKeys="true" keyProperty="id">
INSERT INTO `order` (order_no, user_id, total_amount, status)
VALUES (#{orderNo}, #{userId}, #{totalAmount}, #{status})
</insert>
<!-- ========== 更新操作 ========== -->
<update id="update" parameterType="com.example.demo.domain.Order">
UPDATE `order`
<set>
<if test="orderNo != null">order_no = #{orderNo},</if>
<if test="totalAmount != null">total_amount = #{totalAmount},</if>
<if test="status != null">status = #{status},</if>
</set>
WHERE id = #{id}
</update>
<!-- ========== 刪除操作 ========== -->
<delete id="deleteById" parameterType="long">
DELETE FROM `order` WHERE id = #{id}
</delete>
</mapper>
6. 動(dòng)態(tài)SQL
動(dòng)態(tài)SQL是MyBatis的強(qiáng)大特性之一,可以根據(jù)不同條件動(dòng)態(tài)拼接SQL語句,避免編寫大量的if-else邏輯。
6.1 if 標(biāo)簽
作用: 根據(jù)條件判斷是否包含某段SQL
基本語法:
<if test="條件表達(dá)式">
SQL片段
</if>
完整示例:
Mapper 接口定義:
// 方式1:使用 @Param 注解(推薦)
List<User> findUsers(@Param("username") String username,
@Param("minAge") Integer minAge,
@Param("maxAge") Integer maxAge);
XML 配置:
<select id="findUsers" resultType="User">
SELECT * 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>
</where>
</select>
?? 參數(shù)是如何傳入的?
MyBatis 通過以下方式獲取參數(shù):
1. 使用 @Param 注解指定參數(shù)名(推薦)
// Mapper 接口
List<User> findUsers(@Param("username") String username,
@Param("minAge") Integer minAge,
@Param("maxAge") Integer maxAge);
// Service 層調(diào)用
List<User> users = userMapper.findUsers("張三", 20, 30);
工作原理:
@Param("username")告訴 MyBatis:這個(gè)參數(shù)在 XML 中叫username- XML 中的
#{username}就能獲取到傳入的值 "張三" test="username != null"也是通過參數(shù)名username來判斷
2. 不使用 @Param 的默認(rèn)規(guī)則
// ? 不推薦:不加 @Param
List<User> findUsers(String username, Integer minAge, Integer maxAge);
// XML 中需要使用默認(rèn)參數(shù)名(arg0, arg1 或 param1, param2)
<if test="arg0 != null"> <!-- 或者 param1 -->
AND username = #{arg0} <!-- 或者 #{param1} -->
</if>
3. parameterType 屬性(可選)
<!-- parameterType 是可選的,MyBatis 可以自動(dòng)推斷 -->
<select id="findUsers" parameterType="map" resultType="User">
SELECT * FROM user WHERE username = #{username}
</select>
parameterType 說明:
- ? 可以省略:MyBatis 會(huì)自動(dòng)推斷參數(shù)類型
- ?? 一般不寫:現(xiàn)代項(xiàng)目很少使用,容易造成混淆
- ?? 如果要寫:只在參數(shù)是復(fù)雜對(duì)象時(shí)才可能用到
4. 參數(shù)傳遞的幾種方式對(duì)比
方式1:多個(gè)基本參數(shù)(使用 @Param)
// Mapper 接口
List<User> findUsers(@Param("username") String username,
@Param("minAge") Integer minAge);
// XML 配置
<select id="findUsers" resultType="User">
SELECT * FROM user
WHERE username = #{username} AND age >= #{minAge}
</select>
// 調(diào)用
List<User> users = userMapper.findUsers("張三", 20);
方式2:?jiǎn)蝹€(gè)對(duì)象參數(shù)
// 定義查詢條件對(duì)象
public class UserQuery {
private String username;
private Integer minAge;
private Integer maxAge;
// getters and setters
}
// Mapper 接口
List<User> findUsers(UserQuery query);
// XML 配置(直接使用對(duì)象屬性名)
<select id="findUsers" resultType="User">
SELECT * FROM user
<where>
<if test="username != null">
AND username = #{username} <!-- 自動(dòng)調(diào)用 query.getUsername() -->
</if>
<if test="minAge != null">
AND age >= #{minAge} <!-- 自動(dòng)調(diào)用 query.getMinAge() -->
</if>
</where>
</select>
// 調(diào)用
UserQuery query = new UserQuery();
query.setUsername("張三");
query.setMinAge(20);
List<User> users = userMapper.findUsers(query);
方式3:Map 參數(shù)
// Mapper 接口
List<User> findUsers(Map<String, Object> params);
// XML 配置(使用 Map 的 key)
<select id="findUsers" resultType="User">
SELECT * FROM user
WHERE username = #{username} AND age >= #{minAge}
</select>
// 調(diào)用
Map<String, Object> params = new HashMap<>();
params.put("username", "張三");
params.put("minAge", 20);
List<User> users = userMapper.findUsers(params);
參數(shù)獲取總結(jié)表:
| 參數(shù)方式 | Mapper 接口 | XML 中如何獲取 | 是否需要 @Param |
|---|---|---|---|
| 單個(gè)基本參數(shù) | findById(Long id) |
#{id} 或 #{任意名} |
? 不需要 |
| 多個(gè)基本參數(shù) | find(String name, int age) |
#{param1}, #{param2} |
? 建議加 |
| 多個(gè) @Param | find(@Param("name") String name) |
#{name} |
? 推薦 |
| 對(duì)象參數(shù) | findUsers(UserQuery query) |
#{username} (屬性名) |
? 不需要 |
| Map 參數(shù) | findUsers(Map params) |
#{username} (key名) |
? 不需要 |
最佳實(shí)踐:
- ? 多個(gè)參數(shù)時(shí),總是使用 @Param(清晰明了)
- ? 復(fù)雜查詢條件,使用對(duì)象封裝(可維護(hù)性好)
- ? 不要使用 Map 傳參(類型不安全,難以維護(hù))
- ? parameterType 一般省略(MyBatis 自動(dòng)推斷)
詳細(xì)解釋:
-
<where>標(biāo)簽的作用:- 自動(dòng)處理 WHERE 關(guān)鍵字
- 自動(dòng)去除第一個(gè)條件前多余的 AND 或 OR
- 如果沒有任何條件成立,不會(huì)生成 WHERE 子句
-
<if test="條件">判斷規(guī)則:username != null:判斷參數(shù)是否為nullusername != '':判斷字符串是否為空串- 兩個(gè)條件用
and連接(注意是小寫) - 也可以用
or連接多個(gè)條件
-
XML 中的特殊字符轉(zhuǎn)義:
核心問題:為什么 >= 可以直接寫,而 <= 要轉(zhuǎn)義?
<if test="minAge != null">
AND age >= #{minAge} <!-- ? >= 可以直接寫 -->
</if>
<if test="maxAge != null">
AND age <= #{maxAge} <!-- ?? <= 必須轉(zhuǎn)義成 <= -->
</if>
原因:
<是 XML 標(biāo)簽的開始符號(hào)(如<if>、<select>),必須轉(zhuǎn)義>是 XML 標(biāo)簽的結(jié)束符號(hào),可以直接使用(但建議也轉(zhuǎn)義)
XML 特殊字符對(duì)比:
| 符號(hào) | 含義 | XML轉(zhuǎn)義 | 是否必須轉(zhuǎn)義 | 示例 |
|---|---|---|---|---|
< |
小于 | < |
? 必須 | age < 18 |
<= |
小于等于 | <= |
? 必須 | age <= 18 |
> |
大于 | > |
?? 建議(可不轉(zhuǎn)義) | age > 18 或 age > 18 |
>= |
大于等于 | >= |
?? 建議(可不轉(zhuǎn)義) | age >= 18 或 age >= 18 |
& |
與符號(hào) | & |
? 必須 | if (a && b) |
" |
雙引號(hào) | " |
?? 屬性值中建議 | name="value" |
' |
單引號(hào) | ' |
?? 屬性值中建議 | name='value' |
為什么 < 必須轉(zhuǎn)義?
<!-- ? 錯(cuò)誤示例:XML解析器會(huì)認(rèn)為 "age < 18" 中的 < 是一個(gè)標(biāo)簽開始 -->
<if test="age != null">
AND age < #{age} <!-- 報(bào)錯(cuò)!XML解析器混亂了 -->
</if>
<!-- ? 正確示例1:轉(zhuǎn)義 -->
<if test="age != null">
AND age < #{age} <!-- 正確 -->
</if>
<!-- ? 正確示例2:使用 CDATA -->
<if test="age != null">
<![CDATA[ AND age < #{age} ]]> <!-- 正確 -->
</if>
為什么 > 可以不轉(zhuǎn)義?
<!-- ? 這樣寫不會(huì)報(bào)錯(cuò)(雖然不推薦)-->
<if test="age != null">
AND age > #{age} <!-- 可以,但不規(guī)范 -->
</if>
<!-- ? 更規(guī)范的寫法 -->
<if test="age != null">
AND age > #{age} <!-- 推薦 -->
</if>
最佳實(shí)踐:
方式1:使用轉(zhuǎn)義字符(推薦,清晰明了)
<if test="minAge != null">AND age >= #{minAge}</if>
<if test="maxAge != null">AND age <= #{maxAge}</if>
<if test="age != null">AND age < #{age}</if>
<if test="age != null">AND age > #{age}</if>
方式2:使用 CDATA(推薦,復(fù)雜SQL時(shí)使用)
<if test="minAge != null and maxAge != null">
<![CDATA[
AND age >= #{minAge} AND age <= #{maxAge}
]]>
</if>
<!-- CDATA 區(qū)域內(nèi)的所有內(nèi)容都被視為純文本,不會(huì)被XML解析 -->
<if test="formula != null">
<![CDATA[
AND (price * quantity < 1000 OR discount > 0.5)
]]>
</if>
CDATA 說明:
<![CDATA[開始,]]>結(jié)束- CDATA 區(qū)域內(nèi)可以隨意使用
<、>、&等特殊字符 - 適合包含多個(gè)比較運(yùn)算符的復(fù)雜SQL
實(shí)際對(duì)比:
<!-- 方式1:全部轉(zhuǎn)義(啰嗦但安全)-->
<select id="findUsers1" resultType="User">
SELECT * FROM user
WHERE age >= 18 AND age <= 60 AND score > 80
</select>
<!-- 方式2:使用 CDATA(簡(jiǎn)潔清晰)-->
<select id="findUsers2" resultType="User">
<![CDATA[
SELECT * FROM user
WHERE age >= 18 AND age <= 60 AND score > 80
]]>
</select>
<!-- 方式3:混合使用(不推薦,容易出錯(cuò))-->
<select id="findUsers3" resultType="User">
SELECT * FROM user
WHERE age >= 18 AND age <= 60 AND score > 80 <!-- 不一致,容易混淆 -->
</select>
總結(jié):
<必須轉(zhuǎn)義成<,否則XML解析報(bào)錯(cuò)>可以不轉(zhuǎn)義,但建議轉(zhuǎn)義成>(統(tǒng)一規(guī)范)- 簡(jiǎn)單SQL用轉(zhuǎn)義,復(fù)雜SQL用CDATA
- 團(tuán)隊(duì)開發(fā)要統(tǒng)一規(guī)范(要么全轉(zhuǎn)義,要么全用CDATA)
補(bǔ)充:屬性值中的轉(zhuǎn)義規(guī)則
在 MyBatis XML 中,屬性值(如 test="...")也需要注意轉(zhuǎn)義規(guī)則。
場(chǎng)景1:test 屬性中的比較運(yùn)算符
<!-- ? 正確:test 屬性值中可以直接用 < 和 > -->
<if test="age != null and age > 18">
AND age > 18
</if>
<if test="age != null and age < 60">
AND age < 60
</if>
<if test="minAge != null and maxAge != null and minAge < maxAge">
AND age BETWEEN #{minAge} AND #{maxAge}
</if>
重要:test 屬性值中的 < 和 > 不需要轉(zhuǎn)義!
原因:
- test 屬性的值已經(jīng)被引號(hào)包裹(
test="...") - XML 解析器知道引號(hào)內(nèi)的內(nèi)容是屬性值,不會(huì)當(dāng)作標(biāo)簽解析
- MyBatis 會(huì)正確處理屬性值中的比較運(yùn)算符
場(chǎng)景2:屬性值中包含引號(hào)
<!-- ? 錯(cuò)誤:屬性值用雙引號(hào)包裹,內(nèi)部又有雙引號(hào) -->
<if test="username != null and username == "admin"">
AND role = 'admin'
</if>
<!-- ? 方式1:外層用雙引號(hào),內(nèi)層用單引號(hào) -->
<if test="username != null and username == 'admin'">
AND role = 'admin'
</if>
<!-- ? 方式2:使用轉(zhuǎn)義 -->
<if test="username != null and username == "admin"">
AND role = 'admin'
</if>
<!-- ? 方式3:外層用單引號(hào),內(nèi)層用雙引號(hào) -->
<if test='username != null and username == "admin"'>
AND role = 'admin'
</if>
場(chǎng)景3:屬性值中包含 & 符號(hào)
<!-- ? 錯(cuò)誤:& 符號(hào)沒有轉(zhuǎn)義 -->
<if test="status != null && isActive">
AND status = #{status}
</if>
<!-- ? 方式1:使用 and 代替 && -->
<if test="status != null and isActive">
AND status = #{status}
</if>
<!-- ? 方式2:轉(zhuǎn)義 & 符號(hào) -->
<if test="status != null && isActive">
AND status = #{status}
</if>
屬性值中的轉(zhuǎn)義規(guī)則總結(jié):
| 位置 | 符號(hào) | 是否需要轉(zhuǎn)義 | 說明 |
|---|---|---|---|
| test 屬性值中 | < |
? 不需要 | 引號(hào)內(nèi)可以直接使用 |
| test 屬性值中 | > |
? 不需要 | 引號(hào)內(nèi)可以直接使用 |
| test 屬性值中 | & |
? 需要(或用 and) | 用 & 或 and |
| test 屬性值中 | " |
? 需要(或換引號(hào)) | 外雙內(nèi)單,或轉(zhuǎn)義 " |
| test 屬性值中 | ' |
? 需要(或換引號(hào)) | 外單內(nèi)雙,或轉(zhuǎn)義 ' |
| SQL 內(nèi)容中 | < |
? 必須 | 用 < 或 CDATA |
| SQL 內(nèi)容中 | > |
?? 建議 | 用 > 或 CDATA |
完整示例對(duì)比:
<!-- ========== test 屬性值中的比較運(yùn)算符 ========== -->
<!-- ? 正確:test 屬性中的 < 和 > 不需要轉(zhuǎn)義 -->
<select id="findUsers" resultType="User">
SELECT * FROM user
<where>
<if test="minAge != null and minAge > 0">
AND age >= #{minAge}
</if>
<if test="maxAge != null and maxAge < 150">
AND age <= #{maxAge} <!-- SQL內(nèi)容中的 < 需要轉(zhuǎn)義 -->
</if>
</where>
</select>
<!-- ========== 復(fù)雜條件示例 ========== -->
<!-- ? 推薦:使用 and/or 代替 &&/|| -->
<if test="username != null and username != '' and age > 18">
AND username = #{username} AND age > 18
</if>
<!-- ? 也可以:使用轉(zhuǎn)義的 && -->
<if test="username != null && username != '' && age > 18">
AND username = #{username} AND age > 18
</if>
<!-- ========== 字符串比較示例 ========== -->
<!-- ? 推薦:外雙內(nèi)單 -->
<if test="status != null and status == 'active'">
AND status = 'active'
</if>
<!-- ? 也可以:外單內(nèi)雙 -->
<if test='status != null and status == "active"'>
AND status = 'active'
</if>
<!-- ? 也可以:使用轉(zhuǎn)義 -->
<if test="status != null and status == "active"">
AND status = 'active'
</if>
最佳實(shí)踐建議:
-
test 屬性中的比較運(yùn)算符:直接使用
<、><if test="age > 18 and age < 60">...</if> -
test 屬性中的邏輯運(yùn)算符:使用
and、or,不用&&、||<!-- ? 推薦 --> <if test="a != null and b != null or c > 0">...</if> <!-- ? 不推薦(需要轉(zhuǎn)義,麻煩)--> <if test="a != null && b != null || c > 0">...</if> -
test 屬性中的字符串比較:外雙內(nèi)單
<if test="status == 'active'">...</if> -
SQL 內(nèi)容中的比較運(yùn)算符:
<必須轉(zhuǎn)義,>建議轉(zhuǎn)義AND age < 60 AND score > 80 -
復(fù)雜SQL:使用 CDATA
<![CDATA[ AND age < 60 AND score > 80 ]]>
執(zhí)行場(chǎng)景演示:
場(chǎng)景1:傳入所有參數(shù)
// Mapper調(diào)用
findUsers("張三", 20, 30);
// 生成的SQL
SELECT * FROM user
WHERE username LIKE CONCAT('%', '張三', '%')
AND age >= 20
AND age <= 30
場(chǎng)景2:只傳入 username
// Mapper調(diào)用
findUsers("李四", null, null);
// 生成的SQL
SELECT * FROM user
WHERE username LIKE CONCAT('%', '李四', '%')
場(chǎng)景3:只傳入年齡范圍
// Mapper調(diào)用
findUsers(null, 18, 60);
// 生成的SQL
SELECT * FROM user
WHERE age >= 18
AND age <= 60
場(chǎng)景4:不傳任何參數(shù)
// Mapper調(diào)用
findUsers(null, null, null);
// 生成的SQL
SELECT * FROM user
-- 注意:<where>標(biāo)簽智能處理,沒有WHERE子句
常見判斷條件:
<!-- 判斷是否為null -->
<if test="id != null">
AND id = #{id}
</if>
<!-- 判斷字符串非空 -->
<if test="username != null and username != ''">
AND username = #{username}
</if>
<!-- 判斷數(shù)字大于0 -->
<if test="age != null and age > 0">
AND age = #{age}
</if>
<!-- 判斷集合非空 -->
<if test="ids != null and ids.size() > 0">
AND id IN
<foreach collection="ids" item="id" open="(" close=")" separator=",">
#{id}
</foreach>
</if>
<!-- 判斷布爾值 -->
<if test="isDeleted != null and isDeleted">
AND is_deleted = 1
</if>
6.2 choose-when-otherwise(類似switch)
作用: 多個(gè)條件中只選擇一個(gè)執(zhí)行(類似Java的switch-case)
基本語法:
<choose>
<when test="條件1">SQL片段1</when>
<when test="條件2">SQL片段2</when>
<when test="條件3">SQL片段3</when>
<otherwise>默認(rèn)SQL片段</otherwise>
</choose>
完整示例:
<select id="findUsersByCondition" resultType="User">
SELECT * FROM user
<where>
<choose>
<when test="id != null">
AND id = #{id}
</when>
<when test="username != null">
AND username = #{username}
</when>
<when test="email != null">
AND email = #{email}
</when>
<otherwise>
AND status = 'active'
</otherwise>
</choose>
</where>
</select>
詳細(xì)解釋:
-
執(zhí)行規(guī)則:
- 從上到下依次判斷
<when>條件 - 只要有一個(gè)條件成立,執(zhí)行對(duì)應(yīng)的SQL,然后跳出(不再判斷后續(xù)條件)
- 如果所有
<when>都不成立,執(zhí)行<otherwise> <otherwise>可以省略(相當(dāng)于沒有default分支)
- 從上到下依次判斷
-
與
<if>的區(qū)別:<if>:可以同時(shí)滿足多個(gè)條件,全部執(zhí)行<choose>:只執(zhí)行第一個(gè)滿足的條件,其他忽略
執(zhí)行場(chǎng)景演示:
場(chǎng)景1:傳入 id(優(yōu)先級(jí)最高)
// Mapper調(diào)用
findUsersByCondition(1L, "張三", "test@example.com");
// 生成的SQL(只匹配id,忽略u(píng)sername和email)
SELECT * FROM user
WHERE id = 1
場(chǎng)景2:不傳id,傳入 username
// Mapper調(diào)用
findUsersByCondition(null, "張三", "test@example.com");
// 生成的SQL(匹配username,忽略email)
SELECT * FROM user
WHERE username = '張三'
場(chǎng)景3:只傳入 email
// Mapper調(diào)用
findUsersByCondition(null, null, "test@example.com");
// 生成的SQL
SELECT * FROM user
WHERE email = 'test@example.com'
場(chǎng)景4:什么都不傳(執(zhí)行 otherwise)
// Mapper調(diào)用
findUsersByCondition(null, null, null);
// 生成的SQL
SELECT * FROM user
WHERE status = 'active'
實(shí)際應(yīng)用場(chǎng)景:
示例1:按優(yōu)先級(jí)排序
<!-- 優(yōu)先級(jí):精確ID > 模糊用戶名 > 模糊郵箱 > 查詢所有活躍用戶 -->
<select id="searchUsers" resultType="User">
SELECT * FROM user
<where>
<choose>
<when test="id != null">
id = #{id} <!-- 最精確 -->
</when>
<when test="username != null and username != ''">
username LIKE CONCAT('%', #{username}, '%')
</when>
<when test="email != null and email != ''">
email LIKE CONCAT('%', #{email}, '%')
</when>
<otherwise>
status = 'active' <!-- 默認(rèn)查詢 -->
</otherwise>
</choose>
</where>
</select>
示例2:不同排序策略
<select id="findUsersWithSort" resultType="User">
SELECT * FROM user
ORDER BY
<choose>
<when test="sortBy == 'age'">
age DESC
</when>
<when test="sortBy == 'name'">
username ASC
</when>
<when test="sortBy == 'createTime'">
created_at DESC
</when>
<otherwise>
id ASC <!-- 默認(rèn)按ID排序 -->
</otherwise>
</choose>
</select>
對(duì)比 if 和 choose:
使用 <if>(多個(gè)條件可同時(shí)生效):
<where>
<if test="id != null">AND id = #{id}</if>
<if test="username != null">AND username = #{username}</if>
</where>
<!-- 如果兩個(gè)都傳,生成:WHERE id = 1 AND username = '張三' -->
使用 <choose>(只生效一個(gè)):
<where>
<choose>
<when test="id != null">AND id = #{id}</when>
<when test="username != null">AND username = #{username}</when>
</choose>
</where>
<!-- 如果兩個(gè)都傳,生成:WHERE id = 1(只用id,忽略u(píng)sername)-->
6.3 set 標(biāo)簽(動(dòng)態(tài)更新)
作用: 動(dòng)態(tài)生成UPDATE語句的SET子句,自動(dòng)處理逗號(hào)
基本語法:
<update id="updateXxx">
UPDATE 表名
<set>
<if test="條件1">字段1 = #{值1},</if>
<if test="條件2">字段2 = #{值2},</if>
</set>
WHERE id = #{id}
</update>
完整示例:
<update id="updateUser" 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>
詳細(xì)解釋:
-
<set>標(biāo)簽的作用:- 自動(dòng)添加
SET關(guān)鍵字 - 自動(dòng)去除最后一個(gè)多余的逗號(hào)(這是核心功能)
- 如果所有條件都不成立,不會(huì)生成SET子句(避免SQL錯(cuò)誤)
- 自動(dòng)添加
-
為什么需要
<set>標(biāo)簽?
不使用 <set> 的問題:
<!-- ? 錯(cuò)誤示例 -->
<update id="updateUser">
UPDATE user SET
<if test="username != null">username = #{username},</if>
<if test="email != null">email = #{email},</if>
<if test="age != null">age = #{age},</if> <!-- 最后一個(gè)逗號(hào)怎么處理? -->
WHERE id = #{id}
</update>
<!-- 如果只傳age,生成的SQL:-->
UPDATE user SET age = 25, WHERE id = 1
↑ 這個(gè)逗號(hào)會(huì)導(dǎo)致SQL語法錯(cuò)誤!
使用 <set> 后:
<!-- ? 正確示例 -->
<update id="updateUser">
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>
<!-- 生成的SQL(<set>自動(dòng)去除最后的逗號(hào)):-->
UPDATE user SET age = 25 WHERE id = 1
↑ 逗號(hào)被自動(dòng)去除了!
執(zhí)行場(chǎng)景演示:
場(chǎng)景1:更新所有字段
// Mapper調(diào)用
User user = new User();
user.setId(1L);
user.setUsername("張三");
user.setEmail("zhangsan@example.com");
user.setAge(25);
updateUser(user);
// 生成的SQL
UPDATE user
SET username = '張三',
email = 'zhangsan@example.com',
age = 25
WHERE id = 1
場(chǎng)景2:只更新部分字段
// Mapper調(diào)用(只更新email)
User user = new User();
user.setId(1L);
user.setEmail("newemail@example.com"); // 只設(shè)置email
updateUser(user);
// 生成的SQL
UPDATE user
SET email = 'newemail@example.com'
WHERE id = 1
場(chǎng)景3:只更新一個(gè)字段
// Mapper調(diào)用(只更新age)
User user = new User();
user.setId(1L);
user.setAge(30);
updateUser(user);
// 生成的SQL(注意:最后沒有逗號(hào))
UPDATE user
SET age = 30
WHERE id = 1
實(shí)際應(yīng)用場(chǎng)景:
示例1:復(fù)雜的動(dòng)態(tài)更新
<update id="updateUserSelective">
UPDATE user
<set>
<!-- 字符串字段:判斷非空 -->
<if test="username != null and username != ''">
username = #{username},
</if>
<if test="email != null and email != ''">
email = #{email},
</if>
<!-- 數(shù)字字段:判斷不為null -->
<if test="age != null">
age = #{age},
</if>
<!-- 布爾字段 -->
<if test="isVip != null">
is_vip = #{isVip},
</if>
<!-- 更新時(shí)間:總是更新 -->
updated_at = NOW(),
</set>
WHERE id = #{id}
</update>
示例2:批量更新某些字段
<update id="batchUpdateStatus">
UPDATE user
<set>
status = #{status},
updated_at = NOW()
</set>
WHERE id IN
<foreach collection="ids" item="id" open="(" close=")" separator=",">
#{id}
</foreach>
</update>
示例3:根據(jù)條件決定更新哪些字段
<update id="updateUserByRole">
UPDATE user
<set>
<!-- 管理員可以更新所有字段 -->
<if test="role == 'admin'">
<if test="username != null">username = #{username},</if>
<if test="email != null">email = #{email},</if>
<if test="status != null">status = #{status},</if>
</if>
<!-- 普通用戶只能更新部分字段 -->
<if test="role == 'user'">
<if test="email != null">email = #{email},</if>
</if>
updated_at = NOW()
</set>
WHERE id = #{id}
</update>
與 trim 標(biāo)簽的對(duì)比:
使用 <set> 標(biāo)簽(推薦,簡(jiǎn)潔):
<update id="updateUser">
UPDATE user
<set>
<if test="username != null">username = #{username},</if>
<if test="email != null">email = #{email},</if>
</set>
WHERE id = #{id}
</update>
使用 <trim> 標(biāo)簽實(shí)現(xiàn)相同效果(更靈活但復(fù)雜):
<update id="updateUser">
UPDATE user
<trim prefix="SET" suffixOverrides=",">
<if test="username != null">username = #{username},</if>
<if test="email != null">email = #{email},</if>
</trim>
WHERE id = #{id}
</update>
注意事項(xiàng):
- 每個(gè)字段賦值后都要加逗號(hào),
<set>會(huì)自動(dòng)去除最后的逗號(hào) - 如果所有
<if>條件都不滿足,UPDATE語句可能無效,建議至少保留一個(gè)必更新字段(如updated_at) - WHERE條件必須寫在
<set>標(biāo)簽外面
6.4 foreach 標(biāo)簽(批量操作)
作用: 遍歷集合或數(shù)組,生成重復(fù)的SQL片段(如批量插入、IN查詢)
基本語法:
<foreach collection="集合名稱" item="每個(gè)元素的變量名"
separator="分隔符" open="開始符號(hào)" close="結(jié)束符號(hào)" index="索引變量">
SQL片段(可使用 #{item} 訪問當(dāng)前元素)
</foreach>
屬性說明:
| 屬性 | 必填 | 說明 | 示例 |
|---|---|---|---|
collection |
? 是 | 要遍歷的集合名稱 | list、array、ids(參數(shù)名) |
item |
? 是 | 當(dāng)前元素的變量名 | user、id、item |
separator |
? 否 | 每個(gè)元素之間的分隔符 | ,、OR、AND |
open |
? 否 | 整個(gè)循環(huán)開始前添加的字符 | (、VALUES |
close |
? 否 | 整個(gè)循環(huán)結(jié)束后添加的字符 | )、; |
index |
? 否 | 當(dāng)前元素的索引(從0開始) | i、idx |
示例1:批量插入
<!-- 批量插入 -->
<insert id="batchInsert" parameterType="list">
INSERT INTO user (username, email, age)
VALUES
<foreach collection="list" item="user" separator=",">
(#{user.username}, #{user.email}, #{user.age})
</foreach>
</insert>
詳細(xì)解釋:
collection="list":遍歷參數(shù)中的 list 集合item="user":每次遍歷的元素命名為 userseparator=",":每個(gè)元素之間用逗號(hào)分隔#{user.username}:訪問當(dāng)前 user 對(duì)象的 username 屬性
執(zhí)行場(chǎng)景:
// Mapper調(diào)用
List<User> users = Arrays.asList(
new User("張三", "zhangsan@example.com", 25),
new User("李四", "lisi@example.com", 30),
new User("王五", "wangwu@example.com", 28)
);
batchInsert(users);
// 生成的SQL
INSERT INTO user (username, email, age)
VALUES
('張三', 'zhangsan@example.com', 25),
('李四', 'lisi@example.com', 30),
('王五', 'wangwu@example.com', 28)
示例2:IN 查詢
<!-- IN 查詢 -->
<select id="findByIds" resultType="User">
SELECT * FROM user
WHERE id IN
<foreach collection="ids" item="id" open="(" separator="," close=")">
#{id}
</foreach>
</select>
詳細(xì)解釋:
collection="ids":遍歷參數(shù)名為 ids 的集合item="id":每個(gè)元素命名為 idopen="(":循環(huán)開始前加左括號(hào)close=")":循環(huán)結(jié)束后加右括號(hào)separator=",":元素之間用逗號(hào)分隔
執(zhí)行場(chǎng)景:
// Mapper接口定義
List<User> findByIds(@Param("ids") List<Long> ids);
// 調(diào)用
List<Long> ids = Arrays.asList(1L, 3L, 5L, 7L);
List<User> users = findByIds(ids);
// 生成的SQL
SELECT * FROM user
WHERE id IN (1, 3, 5, 7)
↑ ↑
open close
示例3:批量刪除
<!-- 批量刪除 -->
<delete id="batchDelete">
DELETE FROM user WHERE id IN
<foreach collection="array" item="id" open="(" separator="," close=")">
#{id}
</foreach>
</delete>
詳細(xì)解釋:
collection="array":遍歷數(shù)組類型的參數(shù)
執(zhí)行場(chǎng)景:
// Mapper接口定義(參數(shù)是數(shù)組)
int batchDelete(Long[] ids);
// 調(diào)用
Long[] ids = {1L, 2L, 3L};
batchDelete(ids);
// 生成的SQL
DELETE FROM user WHERE id IN (1, 2, 3)
示例4:動(dòng)態(tài)拼接多個(gè)OR條件
<select id="searchUsers" resultType="User">
SELECT * FROM user
<where>
<foreach collection="keywords" item="keyword" separator="OR">
username LIKE CONCAT('%', #{keyword}, '%')
</foreach>
</where>
</select>
執(zhí)行場(chǎng)景:
// Mapper調(diào)用
List<String> keywords = Arrays.asList("張", "李", "王");
List<User> users = searchUsers(keywords);
// 生成的SQL
SELECT * FROM user
WHERE username LIKE CONCAT('%', '張', '%')
OR username LIKE CONCAT('%', '李', '%')
OR username LIKE CONCAT('%', '王', '%')
示例5:使用 index 屬性
Mapper 接口:
// 方式1:不使用 @Param(單個(gè)參數(shù))
int batchInsertWithIndex(List<User> users);
// 方式2:使用 @Param(推薦)
int batchInsertWithIndex(@Param("userList") List<User> users);
XML 配置:
<!-- 方式1:不使用 @Param 時(shí),collection 必須寫 "list" -->
<insert id="batchInsertWithIndex">
INSERT INTO user (username, email, age, sort_order)
VALUES
<foreach collection="list" item="user" index="i" separator=",">
(#{user.username}, #{user.email}, #{user.age}, #{i})
</foreach>
</insert>
<!-- 方式2:使用 @Param 時(shí),collection 寫 @Param 指定的名稱 -->
<insert id="batchInsertWithIndex">
INSERT INTO user (username, email, age, sort_order)
VALUES
<foreach collection="userList" item="user" index="i" separator=",">
(#{user.username}, #{user.email}, #{user.age}, #{i})
</foreach>
</insert>
?? 重要區(qū)別:Java 的 List 和 collection="list"
這兩個(gè) list 不是同一個(gè)含義!
1. Java 中的 List<User>
List<User> users = Arrays.asList(user1, user2, user3);
// ↑
// 這是 Java 的集合類型,表示一個(gè) User 對(duì)象的列表
- 這是 Java 的類型聲明
List是java.util.List接口<User>是泛型,表示列表中元素的類型
2. XML 中的 collection="list"
<foreach collection="list" item="user">
<!-- ↑
這是 MyBatis 的默認(rèn)參數(shù)名,不是 Java 類型
-->
- 這是 MyBatis 的參數(shù)名
- 當(dāng)方法參數(shù)是單個(gè)
List類型且沒有@Param時(shí),MyBatis 默認(rèn)把它命名為"list" - 這是一個(gè)字符串,用來引用參數(shù)
詳細(xì)對(duì)比:
| Java 接口 | 參數(shù)類型 | XML collection 屬性 | 說明 |
|---|---|---|---|
batchInsert(List<User> users) |
List<User> |
collection="list" |
單個(gè)List參數(shù),默認(rèn)名 "list" |
batchInsert(@Param("users") List<User> users) |
List<User> |
collection="users" |
用 @Param 指定名稱 |
batchInsert(User[] users) |
User[] |
collection="array" |
數(shù)組參數(shù),默認(rèn)名 "array" |
batchInsert(@Param("ids") List<Long> ids) |
List<Long> |
collection="ids" |
用 @Param 指定名稱 |
工作流程圖解:
Java 代碼調(diào)用:
userMapper.batchInsert(Arrays.asList(user1, user2));
↓
傳入一個(gè) List<User> 類型的參數(shù)
↓
MyBatis 內(nèi)部處理:
- 檢測(cè)到參數(shù)類型是 List
- 沒有 @Param 注解
- 自動(dòng)給這個(gè)參數(shù)命名為 "list"
↓
XML 中通過名稱引用:
<foreach collection="list"> ← 這里的 "list" 是參數(shù)的名字
↑
通過名字 "list" 找到傳入的 List<User> 對(duì)象
常見錯(cuò)誤示例:
// Mapper 接口(使用了 @Param)
int batchInsert(@Param("users") List<User> users);
<!-- ? 錯(cuò)誤:使用了 @Param("users"),但 collection 還寫 "list" -->
<foreach collection="list" item="user">
<!-- 報(bào)錯(cuò):Parameter 'list' not found -->
</foreach>
<!-- ? 正確:collection 要和 @Param 的值一致 -->
<foreach collection="users" item="user">
(#{user.username}, #{user.email})
</foreach>
執(zhí)行場(chǎng)景:
// Mapper調(diào)用
List<User> users = Arrays.asList(user1, user2, user3);
// ↑ 這是 Java 的 List 類型
batchInsertWithIndex(users);
// 在 MyBatis 內(nèi)部,這個(gè) List 被命名為 "list"(如果沒有 @Param)
// 所以 XML 中 collection="list" 才能找到這個(gè)參數(shù)
// 生成的SQL(index從0開始)
INSERT INTO user (username, email, age, sort_order)
VALUES
('張三', 'zhangsan@example.com', 25, 0),
('李四', 'lisi@example.com', 30, 1),
('王五', 'wangwu@example.com', 28, 2)
MyBatis 默認(rèn)參數(shù)名規(guī)則:
| 參數(shù)類型 | 沒有 @Param 時(shí)的默認(rèn)名 | 示例 |
|---|---|---|
List |
"list" |
collection="list" |
數(shù)組 |
"array" |
collection="array" |
Map |
"map" |
鍵名直接訪問 |
其他單個(gè)對(duì)象 |
對(duì)象的屬性名 | #{username} |
多個(gè)參數(shù)(無@Param) |
param1, param2, arg0, arg1 |
#{param1}, #{arg0} |
最佳實(shí)踐建議:
// ? 推薦:總是使用 @Param,清晰明了
int batchInsert(@Param("users") List<User> users);
int batchInsert(@Param("ids") List<Long> ids);
<!-- ? 推薦:collection 使用有意義的名稱 -->
<foreach collection="users" item="user">
...
</foreach>
<foreach collection="ids" item="id">
...
</foreach>
// ? 不推薦:依賴默認(rèn)名稱 "list",不夠清晰
int batchInsert(List<User> users);
<!-- ? 不推薦:collection="list" 不夠語義化 -->
<foreach collection="list" item="user">
...
</foreach>
總結(jié):
- Java 的
List<User>= 類型聲明 - XML 的
collection="list"= 參數(shù)名(MyBatis 的默認(rèn)命名) - 它們是兩個(gè)不同層面的概念
- 建議使用
@Param明確指定參數(shù)名,避免混淆
示例6:遍歷Map
<select id="findByMap" resultType="User">
SELECT * FROM user
<where>
<foreach collection="params" item="value" index="key" separator="AND">
${key} = #{value}
</foreach>
</where>
</select>
?? 為什么這里使用 ${key} 而不是 #{key}?
這是一個(gè)關(guān)鍵問題,涉及 MyBatis 中 #{} 和 ${} 的核心區(qū)別!
答案:key 是字段名,必須直接拼接到 SQL 中,不能用預(yù)編譯參數(shù)。
#{} vs ${} 的本質(zhì)區(qū)別:
| 特性 | #{}(推薦) |
${}(謹(jǐn)慎使用) |
|---|---|---|
| 實(shí)現(xiàn)方式 | 預(yù)編譯(PreparedStatement) | 字符串拼接 |
| SQL注入 | ? 安全(自動(dòng)轉(zhuǎn)義) | ? 不安全(可能注入) |
| 使用場(chǎng)景 | 參數(shù)值(WHERE條件、INSERT值等) | 表名、字段名、ORDER BY |
| 生成SQL | 使用 ? 占位符 |
直接替換為實(shí)際值 |
| 類型處理 | 自動(dòng)處理類型轉(zhuǎn)換 | 原樣拼接 |
詳細(xì)對(duì)比:
1. #{} - 預(yù)編譯參數(shù)(安全,推薦)
<!-- 使用 #{} -->
<select id="findByUsername" resultType="User">
SELECT * FROM user WHERE username = #{username}
</select>
// Java 調(diào)用
findByUsername("張三");
// 生成的預(yù)編譯SQL(使用 ? 占位符)
SELECT * FROM user WHERE username = ?
// 執(zhí)行時(shí) MyBatis 會(huì):
// 1. 準(zhǔn)備 PreparedStatement
// 2. 設(shè)置參數(shù):setString(1, "張三")
// 3. 執(zhí)行SQL
// 結(jié)果:安全,防止SQL注入
2. ${} - 字符串替換(不安全,慎用)
<!-- 使用 ${} -->
<select id="findByUsername" resultType="User">
SELECT * FROM user WHERE username = '${username}'
</select>
// Java 調(diào)用
findByUsername("張三");
// 生成的SQL(直接字符串替換)
SELECT * FROM user WHERE username = '張三'
// ?? 如果傳入惡意參數(shù)
findByUsername("' OR '1'='1");
// 生成的SQL(SQL注入!)
SELECT * FROM user WHERE username = '' OR '1'='1'
-- 這會(huì)返回所有用戶!
為什么示例6必須用 ${key}?
<foreach collection="params" item="value" index="key">
${key} = #{value}
<!-- ↑字段名 ↑參數(shù)值 -->
</foreach>
原因分析:
// Map 的內(nèi)容
params.put("username", "張三");
params.put("age", 25);
期望生成的SQL:
WHERE username = '張三' AND age = 25
↑字段名 ↑值 ↑字段名 ↑值
如果使用 #{key}(錯(cuò)誤):
-- 錯(cuò)誤的SQL
WHERE ? = '張三' AND ? = 25
-- ↑ 字段名不能是占位符!
-- 這是無效的SQL,數(shù)據(jù)庫(kù)會(huì)報(bào)錯(cuò)
-- 字段名必須是明確的標(biāo)識(shí)符,不能是參數(shù)
如果使用 ${key}(正確):
-- 正確的SQL
WHERE username = '張三' AND age = 25
-- ↑ 字段名直接拼接進(jìn)SQL
完整對(duì)比示例:
<!-- 示例1:字段名用 ${},值用 #{} -->
<foreach collection="params" item="value" index="key">
${key} = #{value} <!-- ? 正確 -->
</foreach>
<!-- 示例2:都用 #{} -->
<foreach collection="params" item="value" index="key">
#{key} = #{value} <!-- ? 錯(cuò)誤:字段名不能是 ? -->
</foreach>
<!-- 示例3:都用 ${} -->
<foreach collection="params" item="value" index="key">
${key} = '${value}' <!-- ?? 不安全:值有SQL注入風(fēng)險(xiǎn) -->
</foreach>
執(zhí)行場(chǎng)景:
// Mapper調(diào)用
Map<String, Object> params = new HashMap<>();
params.put("username", "張三");
params.put("age", 25);
params.put("status", "active");
findByMap(params);
// 生成的SQL(${key} 被替換為字段名,#{value} 被替換為 ?)
SELECT * FROM user
WHERE username = ? -- 參數(shù)1: "張三"
AND age = ? -- 參數(shù)2: 25
AND status = ? -- 參數(shù)3: "active"
什么時(shí)候必須用 ${}?
| 場(chǎng)景 | 示例 | 原因 |
|---|---|---|
| 動(dòng)態(tài)表名 | SELECT * FROM ${tableName} |
表名不能是參數(shù) |
| 動(dòng)態(tài)字段名 | ORDER BY ${orderColumn} |
字段名不能是參數(shù) |
| 動(dòng)態(tài)排序 | ORDER BY id ${sortOrder} |
ASC/DESC不能是參數(shù) |
| IN 子句(舊版) | WHERE id IN (${ids}) |
現(xiàn)代應(yīng)用用 <foreach> 代替 |
?? 使用 ${} 的安全建議:
- 嚴(yán)格驗(yàn)證輸入
// ? 推薦:白名單驗(yàn)證
public List<User> findByColumn(String column, Object value) {
// 驗(yàn)證字段名是否在允許的列表中
if (!Arrays.asList("username", "age", "status").contains(column)) {
throw new IllegalArgumentException("Invalid column: " + column);
}
return userMapper.findByColumn(column, value);
}
- 避免用戶直接輸入
// ? 危險(xiǎn):直接使用用戶輸入
String userInput = request.getParameter("column");
findByColumn(userInput, value); // SQL注入風(fēng)險(xiǎn)!
// ? 安全:使用枚舉或映射
Map<String, String> columnMap = Map.of(
"name", "username",
"userAge", "age"
);
String column = columnMap.get(request.getParameter("field"));
if (column != null) {
findByColumn(column, value);
}
- 優(yōu)先使用其他方案
<!-- ? 不推薦:動(dòng)態(tài)字段名 -->
<select id="findByColumn" resultType="User">
SELECT * FROM user WHERE ${column} = #{value}
</select>
<!-- ? 推薦:使用 <choose> 明確指定 -->
<select id="findByColumn" resultType="User">
SELECT * FROM user
<where>
<choose>
<when test="column == 'username'">
AND username = #{value}
</when>
<when test="column == 'age'">
AND age = #{value}
</when>
<when test="column == 'status'">
AND status = #{value}
</when>
</choose>
</where>
</select>
總結(jié):
${key}用于字段名:因?yàn)樽侄蚊仨毷荢QL標(biāo)識(shí)符,不能是參數(shù)#{value}用于參數(shù)值:安全的預(yù)編譯方式,防止SQL注入- 原則:能用
#{}就不用${} - 必須用
${}時(shí),要嚴(yán)格驗(yàn)證輸入,防止SQL注入
collection 參數(shù)的不同取值:
| 參數(shù)類型 | collection 取值 | 說明 |
|---|---|---|
List<User> list |
list |
單參數(shù)List,默認(rèn)名稱是 list |
User[] array |
array |
單參數(shù)數(shù)組,默認(rèn)名稱是 array |
@Param("ids") List<Long> ids |
ids |
使用 @Param 指定的名稱 |
Map<String, Object> map |
map 或 map 的key |
遍歷Map |
完整示例對(duì)比:
不使用 @Param:
// Mapper接口
List<User> findByIds(List<Long> ids);
// XML配置
<foreach collection="list" item="id"> <!-- 必須用 list -->
#{id}
</foreach>
使用 @Param(推薦):
// Mapper接口
List<User> findByIds(@Param("ids") List<Long> ids);
// XML配置
<foreach collection="ids" item="id"> <!-- 使用參數(shù)名 ids -->
#{id}
</foreach>
性能注意事項(xiàng):
- 批量插入優(yōu)化:
<!-- ? 推薦:一次性插入多條(高效)-->
INSERT INTO user (username, email) VALUES
('張三', 'a@example.com'),
('李四', 'b@example.com')
<!-- ? 不推薦:多次單條插入(效率低)-->
INSERT INTO user (username, email) VALUES ('張三', 'a@example.com');
INSERT INTO user (username, email) VALUES ('李四', 'b@example.com');
- IN 查詢數(shù)量限制:
// ?? 警告:IN 的參數(shù)不宜過多(建議 < 1000)
// WHERE id IN (1, 2, 3, ..., 10000) ← 可能導(dǎo)致SQL過長(zhǎng)或性能問題
// ? 建議:分批查詢
List<Long> ids = ...; // 10000個(gè)ID
List<User> allUsers = new ArrayList<>();
for (int i = 0; i < ids.size(); i += 500) {
List<Long> batch = ids.subList(i, Math.min(i + 500, ids.size()));
allUsers.addAll(findByIds(batch));
}
- 批量插入數(shù)量限制:
// ?? 批量插入建議每次 < 500 條,避免SQL過長(zhǎng)
if (users.size() > 500) {
// 分批插入
for (int i = 0; i < users.size(); i += 500) {
List<User> batch = users.subList(i, Math.min(i + 500, users.size()));
batchInsert(batch);
}
}
6.5 trim 標(biāo)簽(更靈活的拼接)
作用: 靈活處理SQL片段的前綴、后綴,以及去除多余的分隔符(最強(qiáng)大的拼接標(biāo)簽)
基本語法:
<trim prefix="前綴" suffix="后綴"
prefixOverrides="要去除的前綴" suffixOverrides="要去除的后綴">
SQL片段
</trim>
屬性說明:
| 屬性 | 說明 | 示例 |
|---|---|---|
prefix |
在整個(gè)SQL片段前添加的內(nèi)容 | WHERE、SET、( |
suffix |
在整個(gè)SQL片段后添加的內(nèi)容 | )、; |
prefixOverrides |
去除SQL片段開頭的指定字符 | AND 、OR 、, |
suffixOverrides |
去除SQL片段結(jié)尾的指定字符 | ,、AND 、OR |
注意: prefixOverrides 和 suffixOverrides 中的空格和豎線 | 都有意義!
完整示例:
<select id="findUsers" resultType="User">
SELECT * FROM user
<trim prefix="WHERE" prefixOverrides="AND |OR ">
<if test="username != null">
AND username = #{username}
</if>
<if test="email != null">
AND email = #{email}
</if>
</trim>
</select>
詳細(xì)解釋:
prefix="WHERE":如果 trim 內(nèi)容不為空,在前面加 WHEREprefixOverrides="AND |OR ":去除開頭的AND或OR(注意有空格)AND |OR表示:AND或OR(豎線|是"或"的意思)- 空格很重要:
AND會(huì)匹配 "AND " 但不會(huì)匹配 "AND"
執(zhí)行場(chǎng)景演示:
場(chǎng)景1:傳入兩個(gè)條件
// Mapper調(diào)用
findUsers("張三", "test@example.com");
// 生成的SQL
SELECT * FROM user
WHERE username = '張三' -- 開頭的 "AND " 被去除了
AND email = 'test@example.com'
場(chǎng)景2:只傳入第二個(gè)條件
// Mapper調(diào)用
findUsers(null, "test@example.com");
// 生成的SQL
SELECT * FROM user
WHERE email = 'test@example.com' -- 開頭的 "AND " 被去除
場(chǎng)景3:不傳任何條件
// Mapper調(diào)用
findUsers(null, null);
// 生成的SQL
SELECT * FROM user
-- 沒有 WHERE 子句(trim內(nèi)容為空)
實(shí)際應(yīng)用場(chǎng)景:
示例1:模擬 <where> 標(biāo)簽
<!-- 使用 <where> 標(biāo)簽(簡(jiǎn)潔)-->
<select id="findUsers1" resultType="User">
SELECT * FROM user
<where>
<if test="username != null">AND username = #{username}</if>
<if test="email != null">AND email = #{email}</if>
</where>
</select>
<!-- 使用 <trim> 實(shí)現(xiàn)相同效果(更靈活)-->
<select id="findUsers2" resultType="User">
SELECT * FROM user
<trim prefix="WHERE" prefixOverrides="AND |OR ">
<if test="username != null">AND username = #{username}</if>
<if test="email != null">AND email = #{email}</if>
</trim>
</select>
示例2:模擬 <set> 標(biāo)簽
<!-- 使用 <set> 標(biāo)簽(簡(jiǎn)潔)-->
<update id="updateUser1">
UPDATE user
<set>
<if test="username != null">username = #{username},</if>
<if test="email != null">email = #{email},</if>
</set>
WHERE id = #{id}
</update>
<!-- 使用 <trim> 實(shí)現(xiàn)相同效果(更靈活)-->
<update id="updateUser2">
UPDATE user
<trim prefix="SET" suffixOverrides=",">
<if test="username != null">username = #{username},</if>
<if test="email != null">email = #{email},</if>
</trim>
WHERE id = #{id}
</update>
示例3:動(dòng)態(tài)生成 IN 子句
<select id="findByConditions" resultType="User">
SELECT * FROM user
WHERE status = 'active'
<trim prefix="AND id IN" prefixOverrides="," suffix=")">
<if test="ids != null and ids.size() > 0">
(
<foreach collection="ids" item="id" separator=",">
#{id}
</foreach>
</if>
</trim>
</select>
執(zhí)行場(chǎng)景:
// 傳入ID列表
findByConditions(Arrays.asList(1L, 2L, 3L));
// 生成的SQL
SELECT * FROM user
WHERE status = 'active'
AND id IN (1, 2, 3)
// 不傳ID
findByConditions(null);
// 生成的SQL
SELECT * FROM user
WHERE status = 'active'
-- 沒有 AND id IN 部分
示例4:復(fù)雜的 OR 條件
<select id="complexSearch" resultType="User">
SELECT * FROM user
<trim prefix="WHERE" prefixOverrides="AND |OR ">
<if test="id != null">
AND id = #{id}
</if>
<trim prefix="AND (" suffix=")" prefixOverrides="OR ">
<if test="username != null">
OR username LIKE CONCAT('%', #{username}, '%')
</if>
<if test="email != null">
OR email LIKE CONCAT('%', #{email}, '%')
</if>
</trim>
</trim>
</select>
執(zhí)行場(chǎng)景:
// Mapper調(diào)用
complexSearch(null, "張三", "test@example.com");
// 生成的SQL
SELECT * FROM user
WHERE (
username LIKE CONCAT('%', '張三', '%') -- 開頭的 OR 被去除
OR email LIKE CONCAT('%', 'test@example.com', '%')
)
prefixOverrides 和 suffixOverrides 的匹配規(guī)則:
<!-- 示例1:去除 "AND " 和 "OR "(注意空格)-->
<trim prefixOverrides="AND |OR ">
AND username = 'test' <!-- 匹配成功,去除 "AND " -->
OR email = 'test' <!-- 匹配成功,去除 "OR " -->
ANDOR status = 'active' <!-- 不匹配(沒有空格)-->
</trim>
<!-- 示例2:去除逗號(hào) -->
<trim suffixOverrides=",">
username = 'test',
email = 'test', <!-- 最后的逗號(hào)會(huì)被去除 -->
</trim>
<!-- 示例3:去除多個(gè)可能的后綴 -->
<trim suffixOverrides=", |AND |OR ">
username = 'test', <!-- 去除逗號(hào)+空格 -->
email = 'test' AND <!-- 去除 " AND" -->
age = 25 OR <!-- 去除 " OR" -->
</trim>
標(biāo)簽對(duì)比總結(jié):
| 標(biāo)簽 | 適用場(chǎng)景 | 靈活性 | 推薦度 |
|---|---|---|---|
<where> |
動(dòng)態(tài)WHERE條件 | 低(固定功能) | ?????(最常用) |
<set> |
動(dòng)態(tài)UPDATE | 低(固定功能) | ?????(最常用) |
<trim> |
任意動(dòng)態(tài)拼接 | 高(完全自定義) | ???(復(fù)雜場(chǎng)景) |
選擇建議:
- ? 優(yōu)先使用
<where>和<set>(簡(jiǎn)潔明了) - ? 需要自定義前綴/后綴時(shí)才用
<trim>(更靈活但復(fù)雜) - ?
<trim>可以實(shí)現(xiàn)<where>和<set>的所有功能,但代碼可讀性較差
完整示例:綜合使用
<select id="advancedSearch" resultType="User">
SELECT * FROM user
<trim prefix="WHERE" prefixOverrides="AND |OR ">
<!-- 基礎(chǔ)條件 -->
<if test="status != null">
AND status = #{status}
</if>
<!-- 復(fù)雜OR條件組 -->
<if test="keyword != null and keyword != ''">
AND (
username LIKE CONCAT('%', #{keyword}, '%')
OR email LIKE CONCAT('%', #{keyword}, '%')
)
</if>
<!-- 時(shí)間范圍 -->
<if test="startDate != null">
AND created_at >= #{startDate}
</if>
<if test="endDate != null">
AND created_at <= #{endDate}
</if>
<!-- 年齡范圍 -->
<trim prefix="AND age" prefixOverrides="IN ">
<if test="ages != null and ages.size() > 0">
IN
<foreach collection="ages" item="age" open="(" close=")" separator=",">
#{age}
</foreach>
</if>
</trim>
</trim>
ORDER BY created_at DESC
</select>
總結(jié)要點(diǎn):
<trim>是最靈活的動(dòng)態(tài)SQL標(biāo)簽,可以替代<where>和<set>prefixOverrides去除開頭多余內(nèi)容,suffixOverrides去除結(jié)尾多余內(nèi)容- 豎線
|表示"或"關(guān)系,可以匹配多個(gè)可能的字符 - 空格很重要:
"AND "只匹配 "AND加空格" - 優(yōu)先使用專用標(biāo)簽(
<where>、<set>),復(fù)雜場(chǎng)景再考慮<trim>
7. 結(jié)果映射
?? 如果不加 resultType 或 resultMap 會(huì)怎么樣?
核心規(guī)則:
<select>語句:必須指定 resultType 或 resultMap(否則報(bào)錯(cuò))<insert>、<update>、<delete>語句:不需要 resultType/resultMap(返回受影響行數(shù))
情況1:<select> 不加 resultType/resultMap ?
<!-- ? 錯(cuò)誤示例 -->
<select id="findById">
SELECT * FROM user WHERE id = #{id}
</select>
結(jié)果:編譯或運(yùn)行時(shí)報(bào)錯(cuò)!
org.apache.ibatis.executor.ExecutorException:
A query was run and no Result Maps were found for the Mapped Statement
'com.example.demo.mapper.UserMapper.findById'.
原因:
- MyBatis 不知道如何將查詢結(jié)果映射到 Java 對(duì)象
- 必須明確指定返回類型
正確做法:
<!-- ? 方式1:使用 resultType -->
<select id="findById" resultType="User">
SELECT * FROM user WHERE id = #{id}
</select>
<!-- ? 方式2:使用 resultMap -->
<select id="findById" resultMap="UserResultMap">
SELECT * FROM user WHERE id = #{id}
</select>
情況2:<insert>、<update>、<delete> 不加 resultType/resultMap ?
<!-- ? 正確:增刪改操作不需要 resultType/resultMap -->
<insert id="insert">
INSERT INTO user (username, email, age)
VALUES (#{username}, #{email}, #{age})
</insert>
<update id="update">
UPDATE user SET username = #{username} WHERE id = #{id}
</update>
<delete id="deleteById">
DELETE FROM user WHERE id = #{id}
</delete>
返回值:
- 默認(rèn)返回
int或long(受影響的行數(shù)) - Mapper 接口方法的返回類型通常是
int
// Mapper 接口
public interface UserMapper {
int insert(User user); // 返回插入的行數(shù)(通常是1)
int update(User user); // 返回更新的行數(shù)
int deleteById(Long id); // 返回刪除的行數(shù)
}
執(zhí)行示例:
// 插入1條記錄
int rows = userMapper.insert(user);
System.out.println(rows); // 輸出:1
// 批量刪除3條記錄
int deletedRows = userMapper.deleteByStatus("inactive");
System.out.println(deletedRows); // 輸出:3
// 更新操作,但沒有記錄被更新(WHERE條件不匹配)
int updatedRows = userMapper.updateById(999L);
System.out.println(updatedRows); // 輸出:0
情況3:<select> 返回值類型與 resultType 不匹配
<!-- Mapper 接口定義 -->
List<User> findAll();
<!-- ? 錯(cuò)誤:返回類型應(yīng)該是集合,但 resultType 寫的是 List -->
<select id="findAll" resultType="List">
SELECT * FROM user
</select>
<!-- ? 正確:resultType 指定集合的元素類型,不是集合本身 -->
<select id="findAll" resultType="User">
SELECT * FROM user
</select>
重要規(guī)則:
- Mapper 接口方法返回
List<User> - XML 中 resultType 寫
User(元素類型),不是List - MyBatis 會(huì)自動(dòng)根據(jù)方法簽名判斷是返回單個(gè)對(duì)象還是集合
對(duì)應(yīng)關(guān)系:
| Mapper 方法返回類型 | XML resultType | 說明 |
|---|---|---|
User findById(Long id) |
User |
返回單個(gè)對(duì)象 |
List<User> findAll() |
User |
返回集合,resultType寫元素類型 |
Map<String, Object> findAsMap() |
map 或 hashmap |
返回Map |
List<Map<String, Object>> findAllAsMap() |
map |
返回Map集合 |
int count() |
int 或不寫 |
返回基本類型 |
Long countUsers() |
long 或不寫 |
返回基本類型 |
情況4:特殊的返回類型
返回 Map:
<!-- Mapper 接口 -->
Map<String, Object> findByIdAsMap(@Param("id") Long id);
<!-- XML 配置 -->
<select id="findByIdAsMap" resultType="map">
SELECT id, username, email, age FROM user WHERE id = #{id}
</select>
返回基本類型:
<!-- Mapper 接口 -->
int count();
Long maxId();
String findUsernameById(@Param("id") Long id);
<!-- XML 配置 -->
<select id="count" resultType="int">
SELECT COUNT(*) FROM user
</select>
<select id="maxId" resultType="long">
SELECT MAX(id) FROM user
</select>
<select id="findUsernameById" resultType="string">
SELECT username FROM user WHERE id = #{id}
</select>
MyBatis 內(nèi)置的類型別名:
| Java 類型 | MyBatis 別名 |
|---|---|
int / Integer |
int, integer |
long / Long |
long |
String |
string |
boolean / Boolean |
boolean |
float / Float |
float |
double / Double |
double |
BigDecimal |
bigdecimal |
Date |
date |
Map |
map, hashmap |
List |
list, arraylist |
情況5:同時(shí)指定 resultType 和 resultMap ?
<!-- ? 錯(cuò)誤:不能同時(shí)使用 -->
<select id="findById" resultType="User" resultMap="UserResultMap">
SELECT * FROM user WHERE id = #{id}
</select>
結(jié)果:報(bào)錯(cuò)!
Cannot specify both resultType and resultMap in the same statement
正確做法:只能二選一
<!-- ? 方式1:簡(jiǎn)單映射用 resultType -->
<select id="findById" resultType="User">
SELECT * FROM user WHERE id = #{id}
</select>
<!-- ? 方式2:復(fù)雜映射用 resultMap -->
<select id="findById" resultMap="UserResultMap">
SELECT * FROM user WHERE id = #{id}
</select>
總結(jié)對(duì)比表:
| 操作類型 | 是否需要 resultType/resultMap | 返回值 | 示例 |
|---|---|---|---|
<select> |
? 必須 | 對(duì)象、集合、基本類型、Map | resultType="User" |
<insert> |
? 不需要 | 受影響行數(shù)(int) | 插入1條返回1 |
<update> |
? 不需要 | 受影響行數(shù)(int) | 更新3條返回3 |
<delete> |
? 不需要 | 受影響行數(shù)(int) | 刪除5條返回5 |
選擇 resultType 還是 resultMap?
| 場(chǎng)景 | 推薦使用 | 原因 |
|---|---|---|
| 字段名一致(或開啟駝峰轉(zhuǎn)換) | resultType |
簡(jiǎn)單自動(dòng)映射 |
| 字段名不一致 | resultMap |
手動(dòng)指定映射關(guān)系 |
| 一對(duì)一關(guān)聯(lián) | resultMap + <association> |
需要嵌套映射 |
| 一對(duì)多關(guān)聯(lián) | resultMap + <collection> |
需要集合映射 |
| 多對(duì)多關(guān)聯(lián) | resultMap + <collection> |
需要復(fù)雜映射 |
| 返回 Map | resultType="map" |
動(dòng)態(tài)列 |
7.1 自動(dòng)映射(字段名一致)
<!-- 當(dāng)數(shù)據(jù)庫(kù)字段和實(shí)體類屬性完全一致時(shí) -->
<select id="findById" resultType="User">
SELECT id, username, email, age FROM user WHERE id = #{id}
</select>
7.2 ResultMap(自定義映射)
<resultMap id="UserResultMap" type="com.example.demo.domain.User">
<!-- id: 主鍵映射 -->
<id property="id" column="user_id"/>
<!-- result: 普通字段映射 -->
<result property="username" column="user_name"/>
<result property="email" column="user_email"/>
<result property="age" column="user_age"/>
<result property="createdAt" column="gmt_create"/>
</resultMap>
<select id="findById" resultMap="UserResultMap">
SELECT user_id, user_name, user_email, user_age, gmt_create
FROM t_user
WHERE user_id = #{id}
</select>
7.3 一對(duì)一映射(association)
<!-- 用戶信息表 -->
<resultMap id="UserDetailMap" type="com.example.demo.domain.User">
<id property="id" column="id"/>
<result property="username" column="username"/>
<!-- association: 一對(duì)一關(guān)聯(lián) -->
<association property="profile" javaType="com.example.demo.domain.UserProfile">
<id property="id" column="profile_id"/>
<result property="realName" column="real_name"/>
<result property="phone" column="phone"/>
<result property="address" column="address"/>
</association>
</resultMap>
<select id="findUserWithProfile" resultMap="UserDetailMap">
SELECT
u.id, u.username,
p.id AS profile_id, p.real_name, p.phone, p.address
FROM user u
LEFT JOIN user_profile p ON u.id = p.user_id
WHERE u.id = #{id}
</select>
7.4 一對(duì)多映射(collection)
<resultMap id="UserWithOrdersMap" type="com.example.demo.domain.User">
<id property="id" column="id"/>
<result property="username" column="username"/>
<!-- collection: 一對(duì)多關(guān)聯(lián) -->
<collection property="orders" ofType="com.example.demo.domain.Order">
<id property="id" column="order_id"/>
<result property="orderNo" column="order_no"/>
<result property="totalAmount" column="total_amount"/>
<result property="status" column="status"/>
</collection>
</resultMap>
<select id="findUserWithOrders" resultMap="UserWithOrdersMap">
SELECT
u.id, u.username,
o.id AS order_id, o.order_no, o.total_amount, o.status
FROM user u
LEFT JOIN `order` o ON u.id = o.user_id
WHERE u.id = #{id}
</select>
7.5 多對(duì)多映射(collection + 中間表)
場(chǎng)景:學(xué)生和課程的關(guān)系
- 一個(gè)學(xué)生可以選多門課程
- 一門課程可以被多個(gè)學(xué)生選擇
數(shù)據(jù)庫(kù)設(shè)計(jì)(三張表):
-- 學(xué)生表
CREATE TABLE student (
id BIGINT PRIMARY KEY,
name VARCHAR(50)
);
-- 課程表
CREATE TABLE course (
id BIGINT PRIMARY KEY,
name VARCHAR(50)
);
-- 中間表(關(guān)聯(lián)表)
CREATE TABLE student_course (
student_id BIGINT,
course_id BIGINT,
PRIMARY KEY (student_id, course_id)
);
Java 實(shí)體類:
// 學(xué)生類
public class Student {
private Long id;
private String name;
private List<Course> courses; // ← 多對(duì)多:集合
}
// 課程類
public class Course {
private Long id;
private String name;
private List<Student> students; // ← 反向關(guān)系(可選)
}
MyBatis 配置:
<resultMap id="StudentWithCoursesMap" type="com.example.demo.domain.Student">
<id property="id" column="id"/>
<result property="name" column="name"/>
<!-- collection: 多對(duì)多也用 collection -->
<collection property="courses" ofType="com.example.demo.domain.Course">
<id property="id" column="course_id"/>
<result property="name" column="course_name"/>
</collection>
</resultMap>
<select id="findStudentWithCourses" resultMap="StudentWithCoursesMap">
SELECT
s.id, s.name,
c.id AS course_id, c.name AS course_name
FROM student s
LEFT JOIN student_course sc ON s.id = sc.student_id -- ← 第一次 JOIN:連接中間表
LEFT JOIN course c ON sc.course_id = c.id -- ← 第二次 JOIN:連接目標(biāo)表
WHERE s.id = #{id}
</select>
?? 關(guān)系映射完整規(guī)律總結(jié)
?? 一、從數(shù)據(jù)庫(kù)角度理解
| 關(guān)系類型 | 外鍵位置 | 表的數(shù)量 | 示例 |
|---|---|---|---|
| 一對(duì)一 | A表或B表(任一方) | 2張表 | 用戶 ? 用戶檔案 |
| 一對(duì)多 | 多的一方(B表) | 2張表 | 用戶 ? 訂單 |
| 多對(duì)多 | 獨(dú)立的中間表 | 3張表 | 學(xué)生 ? 課程 |
規(guī)律:
- 一對(duì)一/一對(duì)多:2張表,直接 JOIN
- 多對(duì)多:3張表(需要中間表),JOIN 兩次
?? 二、從 Java 實(shí)體類角度理解
| 關(guān)系類型 | 屬性類型 | 記憶技巧 |
|---|---|---|
| 一對(duì)一 | Profile profile; |
單個(gè)對(duì)象 - 一個(gè)人一張身份證 |
| 一對(duì)多 | List<Order> orders; |
集合 - 一個(gè)人多個(gè)訂單 |
| 多對(duì)多 | List<Course> courses; |
集合 - 一個(gè)學(xué)生多門課 |
規(guī)律:
// 看屬性類型就知道用什么標(biāo)簽
private Profile profile; // ← 單個(gè)對(duì)象 → association
private List<Order> orders; // ← 集合 → collection
private List<Course> courses; // ← 集合 → collection
?? 三、從 MyBatis 配置角度理解
| 關(guān)系類型 | 標(biāo)簽 | 類型屬性 | 完整寫法 |
|---|---|---|---|
| 一對(duì)一 | <association> |
javaType="對(duì)象類" |
<association property="profile" javaType="Profile"> |
| 一對(duì)多 | <collection> |
ofType="元素類" |
<collection property="orders" ofType="Order"> |
| 多對(duì)多 | <collection> |
ofType="元素類" |
<collection property="courses" ofType="Course"> |
規(guī)律:
- association = 單個(gè)關(guān)聯(lián) → 用
javaType(Java類型) - collection = 集合 → 用
ofType(集合里元素的類型)
記憶口訣:
一對(duì)一 → association → javaType (單個(gè)Java對(duì)象)
一對(duì)多 → collection → ofType (集合Of某類型)
多對(duì)多 → collection → ofType (集合Of某類型)
?? 四、從 SQL 角度理解
| 關(guān)系類型 | JOIN 次數(shù) | SQL 模式 |
|---|---|---|
| 一對(duì)一 | 1次 JOIN | FROM A LEFT JOIN B ON A.id = B.a_id |
| 一對(duì)多 | 1次 JOIN | FROM A LEFT JOIN B ON A.id = B.a_id |
| 多對(duì)多 | 2次 JOIN | FROM A LEFT JOIN AB ON A.id = AB.a_idLEFT JOIN B ON AB.b_id = B.id |
規(guī)律:
- 一對(duì)一/一對(duì)多:只連接目標(biāo)表(1次 JOIN)
- 多對(duì)多:先連中間表,再連目標(biāo)表(2次 JOIN)
?? 五、完整對(duì)比示例
1?? 一對(duì)一(User → Profile)
// 實(shí)體類
public class User {
private Long id;
private String username;
private Profile profile; // ← 單個(gè)對(duì)象
}
<!-- MyBatis 配置 -->
<resultMap id="UserDetailMap" type="User">
<id property="id" column="id"/>
<result property="username" column="username"/>
<association property="profile" javaType="Profile"> <!-- ← association + javaType -->
<id property="id" column="profile_id"/>
<result property="phone" column="phone"/>
</association>
</resultMap>
<!-- SQL:1次 JOIN -->
<select id="findUserWithProfile" resultMap="UserDetailMap">
SELECT u.id, u.username, p.id AS profile_id, p.phone
FROM user u
LEFT JOIN user_profile p ON u.id = p.user_id -- ← 直接連目標(biāo)表
WHERE u.id = #{id}
</select>
2?? 一對(duì)多(User → Orders)
// 實(shí)體類
public class User {
private Long id;
private String username;
private List<Order> orders; // ← 集合
}
<!-- MyBatis 配置 -->
<resultMap id="UserWithOrdersMap" type="User">
<id property="id" column="id"/>
<result property="username" column="username"/>
<collection property="orders" ofType="Order"> <!-- ← collection + ofType -->
<id property="id" column="order_id"/>
<result property="orderNo" column="order_no"/>
</collection>
</resultMap>
<!-- SQL:1次 JOIN -->
<select id="findUserWithOrders" resultMap="UserWithOrdersMap">
SELECT u.id, u.username, o.id AS order_id, o.order_no
FROM user u
LEFT JOIN `order` o ON u.id = o.user_id -- ← 直接連目標(biāo)表
WHERE u.id = #{id}
</select>
3?? 多對(duì)多(Student → Courses)
// 實(shí)體類
public class Student {
private Long id;
private String name;
private List<Course> courses; // ← 集合
}
<!-- MyBatis 配置 -->
<resultMap id="StudentWithCoursesMap" type="Student">
<id property="id" column="id"/>
<result property="name" column="name"/>
<collection property="courses" ofType="Course"> <!-- ← collection + ofType -->
<id property="id" column="course_id"/>
<result property="name" column="course_name"/>
</collection>
</resultMap>
<!-- SQL:2次 JOIN(關(guān)鍵區(qū)別!)-->
<select id="findStudentWithCourses" resultMap="StudentWithCoursesMap">
SELECT s.id, s.name, c.id AS course_id, c.name AS course_name
FROM student s
LEFT JOIN student_course sc ON s.id = sc.student_id -- ← 第1次:連中間表
LEFT JOIN course c ON sc.course_id = c.id -- ← 第2次:連目標(biāo)表
WHERE s.id = #{id}
</select>
?? 六、終極記憶法
?? 方法1:看Java屬性類型
private Profile profile; → association + javaType
private List<Order> orders; → collection + ofType
private List<Course> courses; → collection + ofType
?? 方法2:看表的數(shù)量
2張表(一對(duì)一/一對(duì)多)→ 1次 JOIN
3張表(多對(duì)多) → 2次 JOIN(中間表是核心)
?? 方法3:看外鍵位置
一對(duì)一 → 外鍵在任一表 → 直接 JOIN
一對(duì)多 → 外鍵在"多"的表 → 直接 JOIN
多對(duì)多 → 外鍵在中間表 → 2次 JOIN
?? 方法4:口訣
單個(gè)對(duì)象 association,多個(gè)對(duì)象 collection
javaType 定整體,ofType 定元素
一二直連目標(biāo)表,多多要過中間表
?? 七、常見錯(cuò)誤對(duì)比
| 錯(cuò)誤寫法 | 正確寫法 | 說明 |
|---|---|---|
<association ofType="Order"> |
<collection ofType="Order"> |
集合不能用 association |
<collection javaType="List"> |
<collection ofType="Order"> |
collection 用 ofType |
<association javaType="List"> |
<association javaType="Profile"> |
association 指定具體類 |
?? 八、快速判斷流程圖
看 Java 實(shí)體類屬性
↓
是集合嗎?
/ \
是 否
↓ ↓
collection association
↓ ↓
ofType javaType
↓ ↓
看表數(shù)量 1次JOIN
/ \
2張 3張
↓ ↓
一對(duì)多 多對(duì)多
1次 2次
JOIN JOIN
?? 九、實(shí)戰(zhàn)對(duì)照表
| 業(yè)務(wù)場(chǎng)景 | 關(guān)系 | Java類型 | 標(biāo)簽 | JOIN次數(shù) |
|---|---|---|---|---|
| 用戶-檔案 | 一對(duì)一 | Profile |
association | 1次 |
| 用戶-訂單 | 一對(duì)多 | List<Order> |
collection | 1次 |
| 學(xué)生-課程 | 多對(duì)多 | List<Course> |
collection | 2次 |
| 訂單-訂單詳情 | 一對(duì)多 | List<OrderItem> |
collection | 1次 |
| 文章-標(biāo)簽 | 多對(duì)多 | List<Tag> |
collection | 2次 |
| 用戶-錢包 | 一對(duì)一 | Wallet |
association | 1次 |
?? 十、單向關(guān)聯(lián) vs 雙向關(guān)聯(lián)
什么是單向關(guān)聯(lián)和雙向關(guān)聯(lián)?
在關(guān)系映射中,可以選擇單向關(guān)聯(lián)(只能從一方訪問另一方)或雙向關(guān)聯(lián)(互相訪問)。
10.1 一對(duì)一關(guān)系的單向 vs 雙向
單向關(guān)聯(lián)(User → Profile)
// 用戶類
public class User {
private Long id;
private String username;
private UserProfile profile; // ← 可以訪問檔案
}
// 檔案類
public class UserProfile {
private Long id;
private Long userId; // ← 只有外鍵ID
private String realName;
// 沒有 User 對(duì)象
}
特點(diǎn):
- 只能從
User訪問Profile Profile不能直接訪問User(只有userId)
使用:
User user = userMapper.findUserWithProfile(1L);
System.out.println(user.getProfile().getPhone()); // ? 可以
UserProfile profile = profileMapper.findById(1L);
System.out.println(profile.getUser().getUsername()); // ? 不行!沒有 user 對(duì)象
雙向關(guān)聯(lián)(User ? Profile)
// 用戶類
public class User {
private Long id;
private String username;
private UserProfile profile; // ← 可以訪問檔案
}
// 檔案類
public class UserProfile {
private Long id;
private Long userId; // ← 保留外鍵ID
private String realName;
private User user; // ← 反向引用:可以訪問用戶
}
特點(diǎn):
User可以訪問ProfileProfile也可以訪問User(雙向)
MyBatis 配置(反向查詢):
<!-- 查詢檔案及其用戶 -->
<resultMap id="ProfileWithUserMap" type="UserProfile">
<id property="id" column="id"/>
<result property="userId" column="user_id"/>
<result property="realName" column="real_name"/>
<!-- 反向關(guān)聯(lián)用戶 -->
<association property="user" javaType="User">
<id property="id" column="user_id"/>
<result property="username" column="username"/>
</association>
</resultMap>
<select id="findProfileWithUser" resultMap="ProfileWithUserMap">
SELECT
p.id, p.user_id, p.real_name,
u.username
FROM user_profile p
LEFT JOIN user u ON p.user_id = u.id
WHERE p.id = #{id}
</select>
使用:
// 正向查詢
User user = userMapper.findUserWithProfile(1L);
System.out.println(user.getProfile().getPhone()); // ? 可以
// 反向查詢
UserProfile profile = profileMapper.findProfileWithUser(1L);
System.out.println(profile.getUser().getUsername()); // ? 可以!
10.2 一對(duì)多關(guān)系的單向 vs 雙向
單向關(guān)聯(lián)(User → Orders)
// 用戶類
public class User {
private Long id;
private String username;
private List<Order> orders; // ← 可以訪問訂單列表
}
// 訂單類
public class Order {
private Long id;
private Long userId; // ← 只有外鍵ID
private String orderNo;
// 沒有 User 對(duì)象
}
特點(diǎn):
- 只能從
User訪問Orders Order不能直接訪問User
雙向關(guān)聯(lián)(User ? Orders)
// 用戶類
public class User {
private Long id;
private String username;
private List<Order> orders; // ← 可以訪問訂單列表
}
// 訂單類
public class Order {
private Long id;
private Long userId; // ← 保留外鍵ID
private String orderNo;
private User user; // ← 反向引用:可以訪問用戶
}
MyBatis 配置(反向查詢):
<!-- 查詢訂單及其用戶 -->
<resultMap id="OrderWithUserMap" type="Order">
<id property="id" column="id"/>
<result property="userId" column="user_id"/>
<result property="orderNo" column="order_no"/>
<!-- 多對(duì)一:反向關(guān)聯(lián)用戶(注意用 association,不是 collection)-->
<association property="user" javaType="User">
<id property="id" column="user_id"/>
<result property="username" column="username"/>
</association>
</resultMap>
<select id="findOrderWithUser" resultMap="OrderWithUserMap">
SELECT
o.id, o.user_id, o.order_no,
u.username
FROM `order` o
LEFT JOIN user u ON o.user_id = u.id
WHERE o.id = #{id}
</select>
關(guān)鍵點(diǎn): 反向查詢(多 → 一)用的是 <association>,不是 <collection>!
使用場(chǎng)景:
// 訂單列表:顯示每個(gè)訂單的下單用戶
List<Order> orders = orderMapper.findAllOrdersWithUser();
for (Order order : orders) {
System.out.println(order.getOrderNo() + " - " + order.getUser().getUsername());
}
// 輸出:
// ORD001 - zhangsan
// ORD002 - lisi
10.3 多對(duì)多關(guān)系的單向 vs 雙向
單向關(guān)聯(lián)(Student → Courses)
// 學(xué)生類
public class Student {
private Long id;
private String name;
private List<Course> courses; // ← 可以訪問課程列表
}
// 課程類
public class Course {
private Long id;
private String name;
// 沒有 students 列表
}
雙向關(guān)聯(lián)(Student ? Courses)
// 學(xué)生類
public class Student {
private Long id;
private String name;
private List<Course> courses; // ← 學(xué)生的課程列表
}
// 課程類
public class Course {
private Long id;
private String name;
private List<Student> students; // ← 反向:選這門課的學(xué)生列表
}
MyBatis 配置(反向查詢):
<!-- 查詢課程及選課學(xué)生 -->
<resultMap id="CourseWithStudentsMap" type="Course">
<id property="id" column="id"/>
<result property="name" column="name"/>
<!-- 反向:課程的學(xué)生列表 -->
<collection property="students" ofType="Student">
<id property="id" column="student_id"/>
<result property="name" column="student_name"/>
</collection>
</resultMap>
<select id="findCourseWithStudents" resultMap="CourseWithStudentsMap">
SELECT
c.id, c.name,
s.id AS student_id, s.name AS student_name
FROM course c
LEFT JOIN student_course sc ON c.id = sc.course_id
LEFT JOIN student s ON sc.student_id = s.id
WHERE c.id = #{id}
</select>
10.4 雙向關(guān)聯(lián)的循環(huán)引用問題
問題:JSON 序列化死循環(huán)
// 雙向關(guān)聯(lián)
public class User {
private List<Order> orders;
}
public class Order {
private User user;
}
// 查詢并返回給前端
@GetMapping("/users/{id}")
public Result getUser(@PathVariable Long id) {
User user = userMapper.findUserWithOrders(id);
return Result.success(user); // ? 會(huì)死循環(huán)!
}
循環(huán)引用結(jié)構(gòu):
user {
orders: [
order1 {
user: {
orders: [
order1 { user: { ... } } // 無限循環(huán)
]
}
}
]
}
解決方案1:使用 @JsonIgnore
public class Order {
private Long id;
private String orderNo;
@JsonIgnore // ← 序列化時(shí)忽略 user 字段
private User user;
}
序列化結(jié)果:
{
"id": 1,
"username": "zhangsan",
"orders": [
{
"id": 101,
"orderNo": "ORD001"
}
]
}
解決方案2:使用 @JsonManagedReference 和 @JsonBackReference
public class User {
@JsonManagedReference // ← 主引用(會(huì)序列化)
private List<Order> orders;
}
public class Order {
@JsonBackReference // ← 反向引用(會(huì)被忽略)
private User user;
}
解決方案3:使用 DTO(最佳實(shí)踐)
// 實(shí)體類(內(nèi)部使用,可以雙向)
public class User {
private List<Order> orders;
}
public class Order {
private User user;
}
// DTO 類(返回給前端,單向)
public class UserDTO {
private Long id;
private String username;
private List<OrderDTO> orders;
}
public class OrderDTO {
private Long id;
private String orderNo;
// 沒有 user 字段,打斷循環(huán)
}
// Controller
@GetMapping("/users/{id}")
public Result getUser(@PathVariable Long id) {
User user = userMapper.findUserWithOrders(id);
UserDTO dto = convertToDTO(user); // 轉(zhuǎn)換
return Result.success(dto);
}
10.6 何時(shí)使用單向,何時(shí)使用雙向?
推薦使用單向關(guān)聯(lián):
? 大多數(shù)情況(默認(rèn)選擇)
? 訪問方向明確(總是從主表查從表)
? 反向查詢需求少
? 需要返回 JSON API(避免循環(huán)引用)
? 追求簡(jiǎn)單性
示例場(chǎng)景:
- 用戶 → 檔案(總是查用戶順便看檔案)
- 文章 → 作者(總是看文章是誰寫的)
推薦使用雙向關(guān)聯(lián):
? 雙向訪問需求頻繁
? 兩個(gè)方向同樣重要
? 內(nèi)部業(yè)務(wù)邏輯(不直接返回給前端)
? 有完善的循環(huán)引用處理機(jī)制
示例場(chǎng)景:
- 訂單 ? 用戶(既查用戶的訂單,也查訂單的用戶)
- 評(píng)論 ? 用戶(既查用戶的評(píng)論,也查評(píng)論的作者)
- 員工 ? 部門(既查員工的部門,也查部門的員工)
10.7 關(guān)系映射方向總結(jié)
一對(duì)一關(guān)系:
| 方向 | User 類 | UserProfile 類 | MyBatis 標(biāo)簽 |
|---|---|---|---|
| 單向 | UserProfile profile |
Long userId |
<association> |
| 雙向 | UserProfile profile |
Long userId + User user |
兩邊都配 <association> |
一對(duì)多關(guān)系:
| 方向 | User 類 | Order 類 | MyBatis 標(biāo)簽 |
|---|---|---|---|
| 單向 | List<Order> orders |
Long userId |
<collection> |
| 雙向 | List<Order> orders |
Long userId + User user |
User 用 <collection>Order 用 <association> |
關(guān)鍵: 反向(多 → 一)用 <association>,不是 <collection>!
多對(duì)多關(guān)系:
| 方向 | Student 類 | Course 類 | MyBatis 標(biāo)簽 |
|---|---|---|---|
| 單向 | List<Course> courses |
無 | <collection> |
| 雙向 | List<Course> courses |
List<Student> students |
兩邊都用 <collection> |
10.8 最佳實(shí)踐建議
-
默認(rèn)使用單向關(guān)聯(lián)
// 簡(jiǎn)單、安全、推薦 public class Order { private Long userId; // 只存外鍵ID } -
需要雙向時(shí)添加反向引用
// 按需添加 public class Order { private Long userId; private User user; // 反向引用 } -
雙向關(guān)聯(lián)必須處理循環(huán)引用
// 使用 @JsonIgnore 或 DTO @JsonIgnore private User user; -
按查詢需求定義 ResultMap
<!-- 查詢1:只查用戶和訂單號(hào)(不包含 user) --> <resultMap id="UserWithOrdersSimple" type="User"> <collection property="orders" ofType="Order"> <result property="orderNo" column="order_no"/> </collection> </resultMap> <!-- 查詢2:查訂單和用戶信息(包含 user) --> <resultMap id="OrderWithUser" type="Order"> <association property="user" javaType="User"> <result property="username" column="username"/> </association> </resultMap> -
記憶口訣
單向簡(jiǎn)單又安全,雙向功能更強(qiáng)大 默認(rèn)單向是首選,需要雙向再添加 雙向一定防循環(huán),@JsonIgnore 或 DTO 多對(duì)一用 association,一對(duì)多用 collection
8. 參數(shù)傳遞
8.1 單個(gè)參數(shù)
// Mapper 接口
User findById(Long id);
String findUsername(Long id);
// XML
<select id="findById" parameterType="long" resultType="User">
SELECT * FROM user WHERE id = #{id}
</select>
8.2 多個(gè)參數(shù)(@Param)
// Mapper 接口
List<User> findByCondition(@Param("username") String username,
@Param("minAge") Integer minAge);
// XML
<select id="findByCondition" resultType="User">
SELECT * FROM user
WHERE username = #{username} AND age >= #{minAge}
</select>
@Param 注解詳解:
@Param 用于指定參數(shù)在 SQL 中的名稱,讓 MyBatis 知道如何映射參數(shù)。
?? 什么時(shí)候必須使用 @Param?
- 多個(gè)參數(shù)時(shí)(推薦總是加)
// ? 不加 @Param - 會(huì)報(bào)錯(cuò)或無法識(shí)別
User findByUsernameAndAge(String username, Integer age);
// ? 加 @Param - 明確指定參數(shù)名
User findByUsernameAndAge(@Param("username") String username,
@Param("age") Integer age);
對(duì)應(yīng) XML:
<select id="findByUsernameAndAge" resultType="User">
SELECT * FROM user
WHERE username = #{username} AND age = #{age}
</select>
- 動(dòng)態(tài) SQL 中需要引用參數(shù)名
List<User> findUsers(@Param("username") String username,
@Param("minAge") Integer minAge);
<select id="findUsers" resultType="User">
SELECT * FROM user
<where>
<if test="username != null">
AND username = #{username}
</if>
<if test="minAge != null">
AND age >= #{minAge}
</if>
</where>
</select>
?? 什么時(shí)候可以省略 @Param?
- 單個(gè)參數(shù)(簡(jiǎn)單類型)
// 可以省略 @Param
User findById(Long id);
List<User> findByAge(Integer age);
// XML 中參數(shù)名可以隨意寫(建議保持一致)
<select id="findById" resultType="User">
SELECT * FROM user WHERE id = #{id}
<!-- 也可以寫 #{userId}、#{value}、#{_parameter} -->
</select>
- 單個(gè)參數(shù)(對(duì)象類型)
// 可以省略 @Param
int insert(User user);
<insert id="insert">
INSERT INTO user (username, email, age)
VALUES (#{username}, #{email}, #{age})
<!-- 直接使用對(duì)象的屬性名 -->
</insert>
?? @Param 使用對(duì)比表
| 場(chǎng)景 | 是否需要 @Param | 示例 |
|---|---|---|
| 單個(gè)簡(jiǎn)單參數(shù) | ? 不需要 | findById(Long id) |
| 單個(gè)對(duì)象參數(shù) | ? 不需要 | insert(User user) |
| 多個(gè)參數(shù) | ? 必須 | find(@Param("name") String name, @Param("age") int age) |
| 動(dòng)態(tài)SQL引用參數(shù) | ? 必須 | <if test="name != null"> |
| 集合參數(shù) | ?? 建議加 | findByIds(@Param("ids") List<Long> ids) |
?? 不加 @Param 的默認(rèn)規(guī)則
MyBatis 會(huì)使用以下默認(rèn)參數(shù)名:
// 方法定義
List<User> findByCondition(String username, Integer age);
// 不加 @Param 時(shí),MyBatis 使用默認(rèn)名稱
// param1, param2, param3... 或 arg0, arg1, arg2...
對(duì)應(yīng) XML(不推薦):
<select id="findByCondition" resultType="User">
SELECT * FROM user
WHERE username = #{param1} AND age = #{param2}
<!-- 或者 #{arg0} 和 #{arg1} -->
</select>
? 這種方式可讀性差,強(qiáng)烈不推薦!
?? 最佳實(shí)踐
建議:只要是 Mapper 方法,都加上 @Param
@Mapper
public interface UserMapper {
// ? 單個(gè)參數(shù)也加,更清晰
User findById(@Param("id") Long id);
// ? 多個(gè)參數(shù)必須加
List<User> findByCondition(@Param("username") String username,
@Param("age") Integer age);
// ? 對(duì)象參數(shù)可以不加,但加了更明確
int insert(@Param("user") User user);
// ? 集合參數(shù)建議加
int batchInsert(@Param("users") List<User> users);
List<User> findByIds(@Param("ids") List<Long> ids);
}
原因:
- ? 代碼可讀性更好
- ? XML 中參數(shù)名更明確
- ? 避免升級(jí) MyBatis 版本導(dǎo)致的兼容問題
- ? 團(tuán)隊(duì)開發(fā)統(tǒng)一規(guī)范
8.3 對(duì)象參數(shù)
// Mapper 接口
int insert(User user);
// XML
<insert id="insert" parameterType="User">
INSERT INTO user (username, email, age)
VALUES (#{username}, #{email}, #{age})
</insert>
8.4 Map參數(shù)
// Mapper 接口
List<User> findByMap(Map<String, Object> params);
// XML
<select id="findByMap" parameterType="map" resultType="User">
SELECT * FROM user
WHERE username = #{username} AND age = #{age}
</select>
// 調(diào)用
Map<String, Object> params = new HashMap<>();
params.put("username", "張三");
params.put("age", 25);
List<User> users = userMapper.findByMap(params);
8.5 集合參數(shù)
// Mapper 接口
int batchInsert(List<User> users);
List<User> findByIds(@Param("ids") List<Long> ids);
// XML
<insert id="batchInsert">
INSERT INTO user (username, email, age) VALUES
<foreach collection="list" item="user" separator=",">
(#{user.username}, #{user.email}, #{user.age})
</foreach>
</insert>
<select id="findByIds" resultType="User">
SELECT * FROM user WHERE id IN
<foreach collection="ids" item="id" open="(" close=")" separator=",">
#{id}
</foreach>
</select>
9. 完整實(shí)戰(zhàn)案例
9.1 場(chǎng)景:電商訂單系統(tǒng)
數(shù)據(jù)庫(kù)設(shè)計(jì)
-- 用戶表
CREATE TABLE user (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
username VARCHAR(50) NOT NULL UNIQUE,
email VARCHAR(100),
password VARCHAR(100),
balance DECIMAL(10, 2) DEFAULT 0.00,
status VARCHAR(20) DEFAULT 'active',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- 商品表
CREATE TABLE product (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(100) NOT NULL,
price DECIMAL(10, 2) NOT NULL,
stock INT NOT NULL DEFAULT 0,
category VARCHAR(50)
);
-- 訂單表
CREATE TABLE `order` (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
order_no VARCHAR(50) NOT NULL UNIQUE,
user_id BIGINT NOT NULL,
total_amount DECIMAL(10, 2) NOT NULL,
status VARCHAR(20) DEFAULT 'pending',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES user(id)
);
-- 訂單明細(xì)表
CREATE TABLE order_item (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
order_id BIGINT NOT NULL,
product_id BIGINT NOT NULL,
product_name VARCHAR(100),
price DECIMAL(10, 2) NOT NULL,
quantity INT NOT NULL,
subtotal DECIMAL(10, 2) NOT NULL,
FOREIGN KEY (order_id) REFERENCES `order`(id),
FOREIGN KEY (product_id) REFERENCES product(id)
);
-- 插入測(cè)試數(shù)據(jù)
INSERT INTO user (username, email, password, balance) VALUES
('張三', 'zhangsan@test.com', '123456', 1000.00),
('李四', 'lisi@test.com', '123456', 500.00);
INSERT INTO product (name, price, stock, category) VALUES
('iPhone 15', 5999.00, 100, '手機(jī)'),
('MacBook Pro', 12999.00, 50, '電腦'),
('AirPods', 1299.00, 200, '耳機(jī)');
實(shí)體類
// User.java
package com.example.demo.domain;
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.List;
@Data
public class User {
private Long id;
private String username;
private String email;
private String password;
private BigDecimal balance;
private String status;
private LocalDateTime createdAt;
}
// Product.java
@Data
public class Product {
private Long id;
private String name;
private BigDecimal price;
private Integer stock;
private String category;
}
// Order.java
@Data
public class Order {
private Long id;
private String orderNo;
private Long userId;
private BigDecimal totalAmount;
private String status;
private LocalDateTime createdAt;
// 關(guān)聯(lián)數(shù)據(jù)
private User user;
private List<OrderItem> items;
}
// OrderItem.java
@Data
public class OrderItem {
private Long id;
private Long orderId;
private Long productId;
private String productName;
private BigDecimal price;
private Integer quantity;
private BigDecimal subtotal;
}
Mapper 接口
// ProductMapper.java
package com.example.demo.mapper;
import com.example.demo.domain.Product;
import org.apache.ibatis.annotations.*;
import java.util.List;
@Mapper
public interface ProductMapper {
@Select("SELECT * FROM product WHERE id = #{id}")
Product findById(@Param("id") Long id);
@Select("SELECT * FROM product WHERE category = #{category}")
List<Product> findByCategory(@Param("category") String category);
@Update("UPDATE product SET stock = stock - #{quantity} WHERE id = #{id} AND stock >= #{quantity}")
int reduceStock(@Param("id") Long id, @Param("quantity") Integer quantity);
@Update("UPDATE product SET stock = stock + #{quantity} WHERE id = #{id}")
int increaseStock(@Param("id") Long id, @Param("quantity") Integer quantity);
}
// OrderMapper.java
@Mapper
public interface OrderMapper {
// XML 實(shí)現(xiàn)
int insert(Order order);
Order findById(@Param("id") Long id);
Order findByIdWithDetails(@Param("id") Long id);
List<Order> findByUserId(@Param("userId") Long userId);
int updateStatus(@Param("id") Long id, @Param("status") String status);
}
// OrderItemMapper.java
@Mapper
public interface OrderItemMapper {
@Insert("INSERT INTO order_item (order_id, product_id, product_name, price, quantity, subtotal) " +
"VALUES (#{orderId}, #{productId}, #{productName}, #{price}, #{quantity}, #{subtotal})")
@Options(useGeneratedKeys = true, keyProperty = "id")
int insert(OrderItem item);
@Select("SELECT * FROM order_item WHERE order_id = #{orderId}")
List<OrderItem> findByOrderId(@Param("orderId") Long orderId);
int batchInsert(@Param("items") List<OrderItem> items);
}
OrderMapper.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.demo.mapper.OrderMapper">
<!-- 基礎(chǔ)結(jié)果映射 -->
<resultMap id="BaseResultMap" type="com.example.demo.domain.Order">
<id property="id" column="id"/>
<result property="orderNo" column="order_no"/>
<result property="userId" column="user_id"/>
<result property="totalAmount" column="total_amount"/>
<result property="status" column="status"/>
<result property="createdAt" column="created_at"/>
</resultMap>
<!-- 完整結(jié)果映射:訂單 + 用戶 + 訂單項(xiàng) -->
<resultMap id="OrderDetailMap" type="com.example.demo.domain.Order" extends="BaseResultMap">
<!-- 關(guān)聯(lián)用戶信息 -->
<association property="user" javaType="com.example.demo.domain.User">
<id property="id" column="user_id"/>
<result property="username" column="username"/>
<result property="email" column="email"/>
</association>
<!-- 關(guān)聯(lián)訂單項(xiàng)列表 -->
<collection property="items" ofType="com.example.demo.domain.OrderItem">
<id property="id" column="item_id"/>
<result property="orderId" column="order_id"/>
<result property="productId" column="product_id"/>
<result property="productName" column="product_name"/>
<result property="price" column="price"/>
<result property="quantity" column="quantity"/>
<result property="subtotal" column="subtotal"/>
</collection>
</resultMap>
<!-- 插入訂單 -->
<insert id="insert" parameterType="com.example.demo.domain.Order"
useGeneratedKeys="true" keyProperty="id">
INSERT INTO `order` (order_no, user_id, total_amount, status)
VALUES (#{orderNo}, #{userId}, #{totalAmount}, #{status})
</insert>
<!-- 根據(jù)ID查詢訂單 -->
<select id="findById" parameterType="long" resultMap="BaseResultMap">
SELECT * FROM `order` WHERE id = #{id}
</select>
<!-- 查詢訂單詳情(包含用戶和訂單項(xiàng)) -->
<select id="findByIdWithDetails" parameterType="long" resultMap="OrderDetailMap">
SELECT
o.id, o.order_no, o.user_id, o.total_amount, o.status, o.created_at,
u.username, u.email,
oi.id AS item_id, oi.order_id, oi.product_id,
oi.product_name, oi.price, oi.quantity, oi.subtotal
FROM `order` o
LEFT JOIN user u ON o.user_id = u.id
LEFT JOIN order_item oi ON o.id = oi.order_id
WHERE o.id = #{id}
</select>
<!-- 根據(jù)用戶ID查詢訂單列表 -->
<select id="findByUserId" parameterType="long" resultMap="BaseResultMap">
SELECT * FROM `order`
WHERE user_id = #{userId}
ORDER BY created_at DESC
</select>
<!-- 更新訂單狀態(tài) -->
<update id="updateStatus">
UPDATE `order`
SET status = #{status}
WHERE id = #{id}
</update>
</mapper>
OrderItemMapper.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.demo.mapper.OrderItemMapper">
<!-- 批量插入訂單項(xiàng) -->
<insert id="batchInsert">
INSERT INTO order_item (order_id, product_id, product_name, price, quantity, subtotal)
VALUES
<foreach collection="items" item="item" separator=",">
(#{item.orderId}, #{item.productId}, #{item.productName},
#{item.price}, #{item.quantity}, #{item.subtotal})
</foreach>
</insert>
</mapper>
Service 層
package com.example.demo.service;
import com.example.demo.domain.*;
import com.example.demo.mapper.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
@Service
public class OrderService {
@Autowired
private OrderMapper orderMapper;
@Autowired
private OrderItemMapper orderItemMapper;
@Autowired
private ProductMapper productMapper;
@Autowired
private UserMapper userMapper;
/**
* 創(chuàng)建訂單(事務(wù)管理)
*/
@Transactional(rollbackFor = Exception.class)
public Order createOrder(Long userId, List<OrderItemRequest> itemRequests) {
// 1. 計(jì)算訂單總額
BigDecimal totalAmount = BigDecimal.ZERO;
List<OrderItem> items = new ArrayList<>();
for (OrderItemRequest request : itemRequests) {
// 查詢商品
Product product = productMapper.findById(request.getProductId());
if (product == null) {
throw new RuntimeException("商品不存在: " + request.getProductId());
}
// 檢查庫(kù)存
if (product.getStock() < request.getQuantity()) {
throw new RuntimeException("庫(kù)存不足: " + product.getName());
}
// 扣減庫(kù)存
int updated = productMapper.reduceStock(product.getId(), request.getQuantity());
if (updated == 0) {
throw new RuntimeException("庫(kù)存扣減失敗: " + product.getName());
}
// 計(jì)算小計(jì)
BigDecimal subtotal = product.getPrice().multiply(new BigDecimal(request.getQuantity()));
totalAmount = totalAmount.add(subtotal);
// 構(gòu)建訂單項(xiàng)
OrderItem item = new OrderItem();
item.setProductId(product.getId());
item.setProductName(product.getName());
item.setPrice(product.getPrice());
item.setQuantity(request.getQuantity());
item.setSubtotal(subtotal);
items.add(item);
}
// 2. 創(chuàng)建訂單
Order order = new Order();
order.setOrderNo("ORD" + UUID.randomUUID().toString().substring(0, 8).toUpperCase());
order.setUserId(userId);
order.setTotalAmount(totalAmount);
order.setStatus("pending");
orderMapper.insert(order);
// 3. 創(chuàng)建訂單項(xiàng)
for (OrderItem item : items) {
item.setOrderId(order.getId());
}
orderItemMapper.batchInsert(items);
// 4. 返回完整訂單信息
return orderMapper.findByIdWithDetails(order.getId());
}
/**
* 查詢訂單詳情
*/
public Order getOrderDetail(Long orderId) {
return orderMapper.findByIdWithDetails(orderId);
}
/**
* 查詢用戶訂單列表
*/
public List<Order> getUserOrders(Long userId) {
return orderMapper.findByUserId(userId);
}
/**
* 取消訂單
*/
@Transactional(rollbackFor = Exception.class)
public void cancelOrder(Long orderId) {
Order order = orderMapper.findById(orderId);
if (order == null) {
throw new RuntimeException("訂單不存在");
}
if (!"pending".equals(order.getStatus())) {
throw new RuntimeException("訂單狀態(tài)不允許取消");
}
// 恢復(fù)庫(kù)存
List<OrderItem> items = orderItemMapper.findByOrderId(orderId);
for (OrderItem item : items) {
productMapper.increaseStock(item.getProductId(), item.getQuantity());
}
// 更新訂單狀態(tài)
orderMapper.updateStatus(orderId, "cancelled");
}
// DTO 類
@lombok.Data
public static class OrderItemRequest {
private Long productId;
private Integer quantity;
}
}
Controller 層
package com.example.demo.controller;
import com.example.demo.domain.Order;
import com.example.demo.service.OrderService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("/api/orders")
public class OrderController {
@Autowired
private OrderService orderService;
/**
* 創(chuàng)建訂單
*/
@PostMapping
public Order createOrder(@RequestBody CreateOrderRequest request) {
return orderService.createOrder(request.getUserId(), request.getItems());
}
/**
* 查詢訂單詳情
*/
@GetMapping("/{orderId}")
public Order getOrderDetail(@PathVariable Long orderId) {
return orderService.getOrderDetail(orderId);
}
/**
* 查詢用戶訂單列表
*/
@GetMapping("/user/{userId}")
public List<Order> getUserOrders(@PathVariable Long userId) {
return orderService.getUserOrders(userId);
}
/**
* 取消訂單
*/
@PostMapping("/{orderId}/cancel")
public void cancelOrder(@PathVariable Long orderId) {
orderService.cancelOrder(orderId);
}
@lombok.Data
public static class CreateOrderRequest {
private Long userId;
private List<OrderService.OrderItemRequest> items;
}
}
9.2 測(cè)試示例
package com.example.demo;
import com.example.demo.domain.*;
import com.example.demo.mapper.*;
import com.example.demo.service.OrderService;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import java.util.ArrayList;
import java.util.List;
@SpringBootTest
public class MyBatisTests {
@Autowired
private UserMapper userMapper;
@Autowired
private ProductMapper productMapper;
@Autowired
private OrderService orderService;
@Test
public void testFindUser() {
User user = userMapper.findById(1L);
System.out.println("用戶信息: " + user);
}
@Test
public void testCreateOrder() {
// 構(gòu)建訂單請(qǐng)求
List<OrderService.OrderItemRequest> items = new ArrayList<>();
OrderService.OrderItemRequest item1 = new OrderService.OrderItemRequest();
item1.setProductId(1L);
item1.setQuantity(2);
items.add(item1);
OrderService.OrderItemRequest item2 = new OrderService.OrderItemRequest();
item2.setProductId(3L);
item2.setQuantity(1);
items.add(item2);
// 創(chuàng)建訂單
Order order = orderService.createOrder(1L, items);
System.out.println("訂單創(chuàng)建成功: " + order);
}
@Test
public void testQueryOrderDetail() {
Order order = orderService.getOrderDetail(1L);
System.out.println("訂單詳情: " + order);
System.out.println("訂單項(xiàng)數(shù)量: " + order.getItems().size());
}
}
10. 最佳實(shí)踐
10.1 注解 vs XML 如何選擇?
| 場(chǎng)景 | 推薦方式 | 原因 |
|---|---|---|
| 簡(jiǎn)單CRUD | 注解 | 代碼簡(jiǎn)潔,易于維護(hù) |
| 復(fù)雜SQL | XML | 可讀性好,易于調(diào)試 |
| 動(dòng)態(tài)SQL | XML | 功能更強(qiáng)大 |
| 多表關(guān)聯(lián) | XML | 結(jié)果映射更清晰 |
| 項(xiàng)目規(guī)范 | 混合使用 | 根據(jù)實(shí)際情況選擇 |
10.2 命名規(guī)范
// Mapper 接口方法命名
findById() // 查詢單個(gè)
findAll() // 查詢所有
findByXxx() // 條件查詢
insert() // 插入
update() // 更新
deleteById() // 刪除
count() // 統(tǒng)計(jì)
batchInsert() // 批量操作
10.3 性能優(yōu)化
- 避免 N+1 查詢問題
<!-- 不推薦:會(huì)產(chǎn)生 N+1 查詢 -->
<select id="findOrders" resultMap="OrderMap">
SELECT * FROM order
</select>
<select id="findItems" resultType="OrderItem">
SELECT * FROM order_item WHERE order_id = #{orderId}
</select>
<!-- 推薦:使用 JOIN 一次查詢 -->
<select id="findOrdersWithItems" resultMap="OrderWithItemsMap">
SELECT o.*, oi.*
FROM order o
LEFT JOIN order_item oi ON o.id = oi.order_id
</select>
- 使用分頁(yè)
<select id="findByPage" resultType="User">
SELECT * FROM user
LIMIT #{limit} OFFSET #{offset}
</select>
- 只查詢需要的字段
<!-- 不推薦 -->
<select id="findUsers" resultType="User">
SELECT * FROM user
</select>
<!-- 推薦 -->
<select id="findUsers" resultType="User">
SELECT id, username, email FROM user
</select>
10.4 事務(wù)管理
@Service
public class UserService {
@Autowired
private UserMapper userMapper;
@Autowired
private AccountMapper accountMapper;
/**
* 使用 @Transactional 確保數(shù)據(jù)一致性
*/
@Transactional(rollbackFor = Exception.class)
public void registerUser(User user) {
// 1. 創(chuàng)建用戶
userMapper.insert(user);
// 2. 創(chuàng)建賬戶
Account account = new Account();
account.setUserId(user.getId());
account.setBalance(BigDecimal.ZERO);
accountMapper.insert(account);
// 如果發(fā)生異常,以上操作都會(huì)回滾
}
}
10.5 SQL 注入防護(hù)
<!-- 推薦:使用 #{} 預(yù)編譯 -->
<select id="findByUsername" resultType="User">
SELECT * FROM user WHERE username = #{username}
</select>
<!-- 危險(xiǎn):使用 ${} 字符串拼接,可能 SQL 注入 -->
<select id="findByUsername" resultType="User">
SELECT * FROM user WHERE username = '${username}'
</select>
<!-- ${} 適用場(chǎng)景:動(dòng)態(tài)表名、列名 -->
<select id="findFromTable" resultType="User">
SELECT * FROM ${tableName} WHERE id = #{id}
</select>
10.6 日志配置
# application.yml
logging:
level:
# 打印 MyBatis SQL 日志
com.example.demo.mapper: DEBUG
mybatis:
configuration:
# 使用標(biāo)準(zhǔn)輸出打印 SQL
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
10.7 常見問題
- 自增主鍵回填
<insert id="insert" useGeneratedKeys="true" keyProperty="id">
INSERT INTO user (username) VALUES (#{username})
</insert>
- 枚舉類型處理
// 實(shí)體類
public class Order {
private OrderStatus status; // 枚舉
}
public enum OrderStatus {
PENDING, PAID, SHIPPED, COMPLETED, CANCELLED
}
// Mapper
@Update("UPDATE `order` SET status = #{status} WHERE id = #{id}")
int updateStatus(@Param("id") Long id, @Param("status") OrderStatus status);
- 日期時(shí)間處理
// 使用 Java 8 時(shí)間類型
@Data
public class User {
private LocalDateTime createdAt;
private LocalDate birthday;
private LocalTime loginTime;
}
總結(jié)
快速上手步驟
- ? 添加依賴:mybatis-spring-boot-starter + 數(shù)據(jù)庫(kù)驅(qū)動(dòng)
- ? 配置數(shù)據(jù)源:application.yml 配置數(shù)據(jù)庫(kù)連接
- ? 創(chuàng)建實(shí)體類:對(duì)應(yīng)數(shù)據(jù)庫(kù)表
- ? 創(chuàng)建Mapper接口:定義數(shù)據(jù)庫(kù)操作方法
- ? 選擇實(shí)現(xiàn)方式:
- 簡(jiǎn)單操作 → 注解(@Select/@Insert/@Update/@Delete)
- 復(fù)雜操作 → XML 配置文件
- ? Service層調(diào)用:注入Mapper,編寫業(yè)務(wù)邏輯
- ? Controller層暴露:提供REST API
核心要點(diǎn)
- @Mapper 注解標(biāo)記接口
- @MapperScan 掃描包路徑
- #{參數(shù)} 預(yù)編譯參數(shù)(防SQL注入)
- ${參數(shù)} 字符串替換(動(dòng)態(tài)表名/列名)
- resultMap 自定義結(jié)果映射
- 動(dòng)態(tài)SQL 使用 if/choose/foreach/set 等標(biāo)簽
- @Transactional 事務(wù)管理
參考資源
- 官方文檔:https://mybatis.org/mybatis-3/zh/index.html
- Spring Boot集成:https://mybatis.org/spring-boot-starter/mybatis-spring-boot-autoconfigure/
現(xiàn)在你已經(jīng)掌握了 MyBatis 的核心知識(shí),可以開始在項(xiàng)目中實(shí)踐了!
建議從簡(jiǎn)單的 CRUD 操作開始,逐步嘗試復(fù)雜的關(guān)聯(lián)查詢和動(dòng)態(tài) SQL。遇到問題時(shí),多查看日志中的 SQL 語句,理解 MyBatis 的執(zhí)行過程。

浙公網(wǎng)安備 33010602011771號(hào)