# 06 Mybatis

## resultMap

resultMap 中的标签如下：

| 元素名称        | 描述               |
| ----------- | ---------------- |
| result      | 字段映射             |
| constructor | 实例化类时，注入结果到构造方法中 |
| association | 关联一个对象           |
| collection  | 关联多个对象           |

### result

result 用于字段映射,给定一个实体类：

```java
@Data
public class User {
    private String id;
    private String username;
    private String password;
    private String address;
    private String email;
}
```

假若数据库别名为 `uid`，则查询语句：

```xml
<resultMap id="getUserByIdMap" type="User">
  <result property="id" column="uid"></result>
</resultMap>

<select id="getUsers" resultType="User">
  SELECT
    u.id as uid,
    u.username,
    u.password,
    u.address,
    u.email
  FROM USER u
  WHERE u.id=#{id}
</select>
```

### constructor

使用 `constructor` 元素将结果注入构造方法里,先给 User 添加构造方法：

```java
public User(String id, String name) {
  this.id = id + "--------";
  this.username = name + "--------";
}
```

```xml
<resultMap id="getUserByIdMap" type="User">
  <constructor>
    <idArg column="id" name="id" javaType="string"></idArg>
    <arg column="username" name="name" javaType="string"></arg>
  </constructor>
</resultMap>
```

其中 column 代表数据库字段名称或者别名；name 则是构造方法中的参数名称；javaType 指定了参数的类型。

这样之后，结果集中的 id 和 username 属性都会发生变化：

```
{
  "id": "1001--------",
  "username": "后羿--------",
  "password": "123456",
  "address": "北京市海淀区",
  "email": "510273027@qq.com"
}
```

### association

给 User 类添加角色：

```java
@Data
public class User {
    private Role role;
}
```

查询用户时查询角色：

```xml
<resultMap id="userMap" type="User">
  <id property="id" column="id"></id>
  <result property="username" column="username"></result>
  <result property="password" column="password"></result>
  <result property="address" column="address"></result>
  <result property="email" column="email"></result>

  <association property="role" javaType="Role">
    <id property="id" column="role_id"></id>
    <result property="name" column="role_name"></result>
  </association>
</resultMap>

<select id="getUserById" resultType="User">
    SELECT
        u.id,
        u.username,
        u.password,
        u.address,
        u.email,
        r.id as 'role_id',
        r.name as 'role_name'
    FROM
        USER u
            LEFT JOIN user_roles ur ON u.id = ur.user_id
            LEFT JOIN role r ON r.id = ur.role_id
    where u.id=#{id}
</select>
```

最后展示信息：

```
{
    "id": "1001",
    "username": "后羿",
    "password": "123456",
    "address": "北京市海淀区",
    "email": "510273027@qq.com",
    "role": {
        "id": "3",
        "name": "射手"
    }
}
```

### collection

#### 嵌套结果映射

如果用户有多个角色，可以使用 `collection` 来返回角色列表：

```java
@Data
public class User {
    private List<Role> roles;
}
```

修改 `resultMap` 返回结果：

```xml
<resultMap id="userMap" type="User">
  <id property="id" column="id"></id>
  <result property="username" column="username"></result>
  <result property="password" column="password"></result>
  <result property="address" column="address"></result>
  <result property="email" column="email"></result>

  <collection property="roles" ofType="Role">
    <id property="id" column="role_id"></id>
    <result property="name" column="role_name"></result>
  </collection>
</resultMap>
```

最后展示信息：

```
{
    "id": "1003",
    "username": "貂蝉",
    "password": "123456",
    "address": "北京市东城区",
    "email": "510273027@qq.com",
    "roles": [
        {
            "id": "1",
            "name": "中单"
        },
        {
            "id": "2",
            "name": "打野"
        }
    ]
}
```

#### 嵌套 Select 查询

假设这么一张表：

| id   | name | url             | parent\_id |
| ---- | ---- | --------------- | ---------- |
| 1    | 系统管理 |                 | 0          |
| 1001 | 用户管理 | /user           | 1          |
| 1002 | 角色管理 | /role           | 1          |
| 1003 | 单位管理 | /employer       | 1          |
| 2    | 系统管理 |                 | 0          |
| 2001 | 系统管理 | /system/monitor | 2          |
| 2002 | 数据管理 | /data/monitor   | 2          |

需要将表的数据分成两级菜单返回，而不是平铺展示，新建 `Menu` 实体类：

```java
@Data
public class Menu {
    private String id;
    private String name;
    private String url;
    private String parent_id;
    private List<Menu> childMenu;
}
```

这里使用 `childMenu` 来展示二级菜单。然后来写 SQL 语句，如果没有传入 `parent_id` 字段，则查询一级菜单：

```xml
<select id="getMenus" resultMap="menusMap">
  SELECT
    m.id,
    m.name,
    m.url,
    m.parent_id
  FROM
    m_menu m
  where 1=1
  <choose>
    <when test="parent_id!=null">
      and m.parent_id = #{parent_id}
    </when>
    <otherwise>
      and m.parent_id = '0'
    </otherwise>
  </choose>
</select>
```

然后定义返回结果：

```xml
<resultMap id="menusMap" type="Menu">
  <id property="id" column="id"></id>
  <result property="name" column="name"></result>
  <result property="url" column="url"></result>
  <result property="m_desc" column="m_desc"></result>
  <result property="parent_id" column="parent_id"></result>

  <collection property="childMenu" ofType="Menu" select="getMenus"  column="{parent_id=id}"></collection>
</resultMap>
```

`collection` 元素含义：

* `property="childMenu"` 对应的是菜单中的子级菜单列表
* `ofType="Menu"` 对应返回数据的类型
* `select="getMenus"` 指定了 SELECT 语句的 id
* `column="{parent_id=id}"` 则是参数的表达式

整体含义为：通过 getMenus 这个 SELECT 语句来获取一级菜单中的 childMenu 属性结果；在上面的 SELECT 语句中，需要传递一个 parent\_id 参数；这个参数的值就是一级菜单中的 id。

最终结果展示信息：

```
[
    {
        "id": "1",
        "name": "系统管理",
        "parent_id": "0",
        "childMenu": [
            {
                "id": "1001",
                "name": "用户管理",
                "url": "/user",
                "parent_id": "1"
            },
            {
                "id": "1002",
                "name": "角色管理",
                "url": "/role",
                "parent_id": "1"
            },
            {
                "id": "1003",
                "name": "单位管理",
                "url": "/employer",
                "parent_id": "1"
            }
        ]
    },
    {
        "id": "2",
        "name": "平台监控",
        "parent_id": "0",
        "childMenu": [
            {
                "id": "2001",
                "name": "系统监控",
                "url": "/system/monitor",
                "parent_id": "2"
            },
            {
                "id": "2002",
                "name": "数据监控",
                "url": "/data/monitor",
                "parent_id": "2"
            }
        ]
    }
]
```

### 自动填充关联对象

在 Mybatis 解析返回值的时候，第一步是获取返回值类型，拿到 Class 对象，然后获取构造器，设置可访问并返回实例，然后又把它包装成 MetaObject 对象。

从数据库 rs 中拿到结果之后，会调用 `MetaObject.setValue(String name, Object value)` 来填充对象。在这过程中，它会以 `.` 来分隔这个 name 属性。如果 name 属性中包含 `.` 符号，就找到 `.` 符号之前的属性名称，把它当做一个实体对象来。

还是以上面获取用户角色为例，在该 SQL 语句：

```xml
<select id="getUserList" resultType="User">
  SELECT
    u.id,
    u.username,
    u.password,
    u.address,
    u.email,
    r.id as 'role.id',
    r.name as 'role.name'
  FROM
    USER u
      LEFT JOIN user_roles ur ON u.id = ur.user_id
      LEFT JOIN role r ON r.id = ur.role_id
</select>
```

## join

* left join（左联接）：返回包括左表中的所有记录和右表中联结字段相等的记录
* right join（右联接）：返回包括右表中的所有记录和左表中联结字段相等的记录
* inner join（内连接）：只返回两个表中联结字段相等的行
* outer join（全连接）：只要左表和右表其中一个表中存在匹配，则返回

假若有两张表 A 和 B，分别如下：

| aID |    aNum   |
| :-: | :-------: |
|  1  | a20050111 |
|  2  | a20050112 |
|  3  | a20050113 |
|  4  | a20050114 |
|  5  | a20050115 |

| bID |    bName   |
| :-: | :--------: |
|  1  | 2006032401 |
|  2  | 2006032402 |
|  3  | 2006032403 |
|  4  | 2006032404 |
|  8  | 2006032408 |

### left join

```sql
select * from A
left join B on A.aID = B.bID
```

结果如下：

| aID |    aNum   |  bID |    bName   |
| :-: | :-------: | :--: | :--------: |
|  1  | a20050111 |   1  | 2006032401 |
|  2  | a20050112 |   2  | 2006032402 |
|  3  | a20050113 |   3  | 2006032403 |
|  4  | a20050114 |   4  | 2006032404 |
|  5  | a20050115 | NULL |    NULL    |

left join 是以 A 表的记录为基础的，A 可以看成左表，B 可以看成右表，left join 是以左表为准的。换句话说，左表 A 的记录将会全部表示出来，而右表 B 只会显示符合搜索条件的记录（例子中为: A.aID = B.bID），B 表记录不足的地方均为 NULL。

### right join

```sql
select * from A
right join B on A.aID = B.bID
```

结果如下：

|  aID |    aNum   | bID |    bName   |
| :--: | :-------: | :-: | :--------: |
|   1  | a20050111 |  1  | 2006032401 |
|   2  | a20050112 |  2  | 2006032402 |
|   3  | a20050113 |  3  | 2006032403 |
|   4  | a20050114 |  4  | 2006032404 |
| NULL |    NULL   |  8  | 2006032408 |

right join 和 left join 的结果刚好相反，这次是以右表 B 为基础的，A 表不足的地方用 NULL 填充。

### inner join

```sql
select * from A
inner join B on A.aID = B.bID
```

结果如下：

| aID |    aNum   | bID |    bName   |
| :-: | :-------: | :-: | :--------: |
|  1  | a20050111 |  1  | 2006032401 |
|  2  | a20050112 |  2  | 2006032402 |
|  3  | a20050113 |  3  | 2006032403 |
|  4  | a20050114 |  4  | 2006032404 |

inner join 产生左表（A）和右表（B）的交集。

### outer join

```sql
select * from A
outer join B on A.aID = B.bID
```

结果如下：

|  aID |    aNum   |  bID |    bName   |
| :--: | :-------: | :--: | :--------: |
|   1  | a20050111 |   1  | 2006032401 |
|   2  | a20050112 |   2  | 2006032402 |
|   3  | a20050113 |   3  | 2006032403 |
|   4  | a20050114 |   4  | 2006032404 |
|   5  | a20050115 | NULL |    NULL    |
| NULL |    NULL   |   8  | 2006032408 |

outer join 产生左表（A）和右表（B）的并集。

### 实际用例

已之前写过的 Netty 聊天系统为例，用户查询好友添加请求和自己的好友列表，查询语句如下：

```xml
<select id="queryFriendRequestList" parameterType="String" resultType="com.chanshiyu.pojo.vo.UsersVO">
  select u.id, u.username, u.nickname, u.avatar
  from friends_request f
  left join users u
  on f.send_user_id = u.id
  where f.accept_user_id = #{acceptUserId}
</select>

<select id="queryFriendList" parameterType="String" resultType="com.chanshiyu.pojo.vo.UsersVO">
  select u.id, u.username, u.nickname, u.avatar
  from my_friends m
  left join users u
  on m.my_friend_user_id = u.id
  where m.my_user_id = #{myUserId}
</select>
```

## 通用 Mapper

通用 Mapper 就是为了解决单表增删改查，基于 Mybatis 的插件。开发人员不需要编写 SQL，不需要在 DAO 中增加方法，只要写好实体类，就能支持相应的增删改查方法。

### Select

方法：`List<T> select(T record);`\
说明：根据实体中的属性值进行查询，查询条件使用等号。

方法：`T selectByPrimaryKey(Object key);`\
说明：根据主键字段进行查询，方法参数必须包含完整的主键属性，查询条件使用等号。

方法：`List<T> selectAll();`\
说明：查询全部结果，`select(null)` 方法能达到同样的效果。

方法：`T selectOne(T record);`\
说明：根据实体中的属性进行查询，只能有一个返回值，有多个结果是抛出异常，查询条件使用等号。

方法：`int selectCount(T record);`\
说明：根据实体中的属性查询总数，查询条件使用等号。

### Insert

方法：`int insert(T record);`\
说明：保存一个实体，null 的属性也会保存，不会使用数据库默认值。

方法：`int insertSelective(T record);`\
说明：保存一个实体，null 的属性不会保存，会使用数据库默认值。

### Update

方法：`int updateByPrimaryKey(T record);`\
说明：根据主键更新实体全部字段，null 值会被更新。

方法：`int updateByPrimaryKeySelective(T record);`\
说明：根据主键更新属性不为 null 的值。

### Delete

方法：`int delete(T record);` 说明：根据实体属性作为条件进行删除，查询条件使用等号。

方法：`int deleteByPrimaryKey(Object key);` 说明：根据主键字段进行删除，方法参数必须包含完整的主键属性。

### Example

方法：`List<T> selectByExample(Object example);` 说明：根据 Example 条件进行查询 重点：这个查询支持通过 Example 类指定查询列，通过 selectProperties 方法指定查询列。

方法：`int selectCountByExample(Object example);` 说明：根据 Example 条件进行查询总数。

方法：`int updateByExample(@Param("record") T record, @Param("example") Object example);` 说明：根据 Example 条件更新实体 record 包含的全部属性，null 值会被更新。

方法：`int updateByExampleSelective(@Param("record") T record, @Param("example") Object example);` 说明：根据 Example 条件更新实体 record 包含的不是 null 的属性值。

方法：`int deleteByExample(Object example);` 说明：根据 Example 条件删除数据。

## Example 的使用

### 查询

```java
Example example = new Example(Country.class);
example.setForUpdate(true);
example.createCriteria().andGreaterThan("id", 100).andLessThan("id",151);
example.or().andLessThan("id", 41);
List<Country> countries = mapper.selectByExample(example);

// DEBUG [main] - ==> Preparing: SELECT id,countryname,countrycode FROM country WHERE ( id > ? and id < ? ) or ( id < ? ) ORDER BY id desc FOR UPDATE
// DEBUG [main] - ==> Parameters: 100(Integer), 151(Integer), 41(Integer)
```

### 动态 SQL

```java
Example example = new Example(Country.class);
Example.Criteria criteria = example.createCriteria();
if(query.getCountryname() != null){
    criteria.andLike("countryname", query.getCountryname() + "%");
}
if(query.getId() != null){
    criteria.andGreaterThan("id", query.getId());
}
List<Country> countries = mapper.selectByExample(example);

// DEBUG [main] - ==> Preparing: SELECT id,countryname,countrycode FROM country WHERE ( countryname like ? ) ORDER BY id desc
// DEBUG [main] - ==> Parameters: China%(String)
```

### 排序

```java
Example example = new Example(Country.class);
example.orderBy("id").desc().orderBy("countryname").orderBy("countrycode").asc();
List<Country> countries = mapper.selectByExample(example);

// DEBUG [main] - ==> Preparing: SELECT id,countryname,countrycode FROM country order by id DESC,countryname,countrycode ASC
// DEBUG [main] - ==> Parameters:
```

### 去重

```java
CountryExample example = new CountryExample();
//设置 distinct
example.setDistinct(true);
example.createCriteria().andCountrynameLike("A%");
example.or().andIdGreaterThan(100);
List<Country> countries = mapper.selectByExample(example);

// DEBUG [main] - ==> Preparing: SELECT distinct id,countryname,countrycode FROM country WHERE ( countryname like ? ) or ( Id > ? ) ORDER BY id desc
// DEBUG [main] - ==> Parameters: A%(String), 100(Integer)
```

### 设置查询列

```java
Example example = new Example(Country.class);
example.selectProperties("id", "countryname");
List<Country> countries = mapper.selectByExample(example);

// DEBUG [main] - ==> Preparing: SELECT id , countryname FROM country ORDER BY id desc
// DEBUG [main] - ==> Parameters:
```

### Example.builder 方式

```java
Example example = Example.builder(Country.class)
        .select("countryname")
        .where(Sqls.custom().andGreaterThan("id", 100))
        .orderByAsc("countrycode")
        .forUpdate()
        .build();
List<Country> countries = mapper.selectByExample(example);

// DEBUG [main] - ==> Preparing: SELECT countryname FROM country WHERE ( id > ? ) order by countrycode Asc FOR UPDATE
// DEBUG [main] - ==> Parameters: 100(Integer)
```

### Weekend

> 使用 6.2 和 6.3 中的 Example 时，需要自己输入属性名，例如 "countryname"，假设输入错误，或者数据库有变化，这里很可能就会出错，因此基于 Java 8 的方法引用是一种更安全的用法，如果你使用 Java 8，你可以试试 Weekend。

```java
List<Country> selectByWeekendSql = mapper.selectByExample(new Example.Builder(Country.class)
        .where(WeekendSqls.<Country>custom().andLike(Country::getCountryname, "%a%")
                .andGreaterThan(Country::getCountrycode, "123"))
        .build());

// DEBUG [main] - ==> Preparing: SELECT id,countryname,countrycode FROM country WHERE ( countryname like ? and countrycode > ? )
// DEBUG [main] - ==> Parameters: %a%(String), 123(String)
```

在代码中的 `Country::getCountryname` 就是方法引用，通过该方法可以自动转换对应的列名。

## Tips

### 打印 SQL 日志

格式：`logging.level.Mapper 类的包=debug`。

```yml
logging:
  level:
    com.chanshiyu.mapper: debug
```

### 插入数据并返回自增 ID

修改 Mapper 文件：

```java
import tk.mybatis.mapper.common.special.InsertUseGeneratedKeysMapper;
import tk.mybatis.mapper.provider.SpecialProvider;

public interface ChatMsgMapper extends InsertUseGeneratedKeysMapper<ChatMsg> {

    @Options(useGeneratedKeys = true, keyProperty = "id")
    @InsertProvider(type = SpecialProvider.class, method = "dynamicSQL")
    int insertUseGeneratedKeys(ChatMsg chatMsg);

}
```

示例：

```java
@Transactional(propagation = Propagation.REQUIRED)
@Override
public int createChatMsg(ChatMsg chatMsg) {
    chatMsgMapper.insertUseGeneratedKeys(chatMsg);
    return chatMsg.getId();
}
```

### in() 查询排序

查询结果按 `in()` 中的 ID 排序：

```xml
<select id="queryTestList" resultMap="TestResult">
  select * from test
  <where>
    id in
    <foreach item="item" index="index" collection="ids" open="(" close=")" separator=",">
      #{item}
    </foreach>
    order by field
    <foreach item="item" index="index" collection="ids" open="(id," close=")" separator=",">
      #{item}
    </foreach>
  </where>
</select>
```

示例：

```java
List<Test> queryTestList(@Param("ids") List<String> ids);

// select * FROM test where id in (1,3,2,5)  order by field (id,1,3,2,5);
```

### find\_in\_set()

举例：有个文章表里面有个 type 字段，它存储的是文章类型，有 1 头条、2 推荐、3 热点、4 图文等等。现在有篇文章他既是头条，又是热点，还是图文，type 中以 1,3,4 的格式存储。那我们如何用 sql 查找所有 type 中有 4 的图文类型的文章呢？

这里就需要用到 mysql 的 `Find_IN_SET()` 函数：

```java
select * from article where FIND_IN_SET('4', type);
```

`FIND_IN_SET(str,strlist)` 参数说明：

* str 要查询的字符串
* strlist 字段名，参数以`,`分隔，如 (1,2,6,8)

查询字段 strlist 中包含 str 的结果，返回结果为 null 或记录：

```sql
SELECT FIND_IN_SET('b', 'a,b,c,d'); --  2
SELECT FIND_IN_SET('e', 'a,b,c,d'); -- 0
```

示例：

```xml
<select id="queryTest" resultMap="TestResult">
  select * from test
  <where>
    <if test="myIds != null">
      FIND_IN_SET(#{myIds,jdbcType=VARCHAR},test_ids)
    </if>
  </where>
</select>
```

```java
List<TestResult> queryTest(@Param("myIds") String myIds);

// select * from app_models where FIND_IN_SET('3',type_ids)
```

`Find_IN_SET` 和 `like` 的区别：`like` 是广泛的模糊匹配，字符串中没有分隔符，`Find_IN_SET` 是精确匹配，字段值以英文`,`分隔，`Find_IN_SET` 查询的结果要小于 `like` 查询的结果。

### 批量更新多个不同值的字段

```xml
<update id="updateBatch"  parameterType="java.util.List">
  <foreach collection="list" item="item" index="index" open="" close="" separator=";">
    update course
    <set>
      name=${item.name}
    </set>
    where id = ${item.id}
  </foreach>
</update>
```

参考文章：\
[MyBatis 通用 Mapper4](https://github.com/abel533/Mapper/wiki)

### 字符串替换

通过任何一列从表中 select 数据时：

```java
@Select("select * from user where ${column} = #{value}")
User findByColumn(@Param("column") String column, @Param("value") String value);
```

### 多行插入

```xml
<insert id="insertAuthor" useGeneratedKeys="true"
    keyProperty="id">
  insert into Author (username, password, email, bio) values
  <foreach item="item" collection="list" separator=",">
    (#{item.username}, #{item.password}, #{item.email}, #{item.bio})
  </foreach>
</insert>
```

### 批量查询

```xml
WHERE id IN
<foreach collection="ids" item="item" index="index" open="(" separator="," close=")" >
    #{item}
</foreach>


<if test="tags!=null">
  AND
  <foreach collection="tags" item="tag" index="index" open=" (" separator=" OR " close=")">
       d.tag LIKE CONCAT('%', #{tag}, '%')
  </foreach>
</if>
```

`CONCAT('%', #{tag}, '%')` 可以用 `bind` 实现：

```xml
<select id="selectBlogsLike" resultType="Blog">
    <bind name="pattern" value="'%' + title + '%'" />
    SELECT * FROM BLOG
    WHERE title LIKE #{pattern}
</select>
```

### 时间比较

```xml
WHERE create_time <![CDATA[>=]]> #{startTime} AND create_time <![CDATA[<=]]> #{endTime}
WHERE create_time BETWEEN #{startTime} AND #{endTime}
WHERE create_time BETWEEN CONCAT(DATE(#{startDate})," 00:00:00") AND CONCAT(DATE(#{endDate})," 23:59:59")
WHERE create_time BETWEEN DATE(#{startDate}) AND DATE_ADD(DATE(#{endDate}),INTERVAL 1 DAY)
```

### 批量更新

**更新单条记录：**

```xml
UPDATE course SET name = 'course1' WHERE id = 'id1';
```

**更新多条记录的同一个字段为同一个值：**

```xml
UPDATE course SET name = 'course1' WHERE id in ('id1', 'id2', 'id3);
```

**更新多条记录为多个字段为不同的值：**

比较普通的写法，是通过循环，依次执行 update 语句：

```xml
<update id="updateBatch"  parameterType="java.util.List">
    <foreach collection="list" item="item" index="index" open="" close="" separator=";">
        update course
        <set>
            name=${item.name}
        </set>
        where id = ${item.id}
    </foreach>
</update>
```

这样做一条记录 update 一次，性能比较差，容易造成阻塞。

MySQL 没有提供直接的方法来实现批量更新，但可以使用 case when 语法来实现这个功能：

```xml
UPDATE course
    SET name = CASE id
        WHEN 1 THEN 'name1'
        WHEN 2 THEN 'name2'
        WHEN 3 THEN 'name3'
    END,
    title = CASE id
        WHEN 1 THEN 'New Title 1'
        WHEN 2 THEN 'New Title 2'
        WHEN 3 THEN 'New Title 3'
    END
WHERE id IN (1,2,3)
```

这条 sql 的意思是，如果 id 为 1，则 name 的值为 name1，title 的值为 New Title1；依此类推。

### 查询结果排序

单字段排序：

```java
example.setOrderByClause("`AFTER_CHECK_TIME` DESC");
```

多个字段排序：

```java
// 查询结果先按`index`字段排序，如果相同，则按`AFTER_CHECK_TIME`排序
example.setOrderByClause("`index` ASC,`AFTER_CHECK_TIME` ASC");
```

## Mybatis Plus

### apply sql 注入风险

```java
// 有注入风险
apply("date_format(dateColumn,'%Y-%m-%d') = '2008-08-08' or true or true")

// 安全
apply("date_format(dateColumn,'%Y-%m-%d') = {0}", "2008-08-08 or true or true")
```

### condition 用法

```java
queryWrapper.like(StringUtils.isNotBlank(name), "name", name);
```

### 分组查询

```java
/*
 * 按照直属上级分组，查询每组的平均年龄、最大年龄、最小年龄，并且只取年龄总和小于500的组。
 * select avg(age) avg_age, min(age) min_age, max(age) max_age
 * from user
 * group by manager_id
 * having sum(age) < 500;
 */

queryWrapper.select("avg(age) avg_age", "min(age) min_age", "max(age) max_age")
            .groupBy("manager_id")
            .having("sum(age) < {0}", 500);
List<Map<String, Object>> userList = userMapper.selectMaps(queryWrapper);
```


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://chanshiyu.gitbook.io/blog/hou-duan/mysql/06-mybatis.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
