java

关注公众号 jb51net

关闭
首页 > 软件编程 > java > mybatis多表关联查询

MyBatis连接池、动态 SQL 与多表关联查询的注意事项

作者:越来越无动于衷

本文将从连接池原理出发,深入讲解动态 SQL 的常用标签,并通过实例演示一对多、多对多等复杂关联查询的实现,帮助你掌握 MyBatis 的进阶用法,感兴趣的朋友一起看看吧

MyBatis 作为一款灵活的持久层框架,除了基础的 CRUD 操作,还提供了连接池管理、动态 SQL 以及多表关联查询等高级特性。本文将从连接池原理出发,深入讲解动态 SQL 的常用标签,并通过实例演示一对多、多对多等复杂关联查询的实现,帮助你掌握 MyBatis 的进阶用法。

一、MyBatis 连接池:提升数据库交互性能

连接池是存储数据库连接的容器,它的核心作用是避免频繁创建和关闭连接,从而减少资源消耗、提高程序响应速度。在 MyBatis 中,连接池的配置通过dataSource标签的type属性实现,支持三种类型的连接池:

1. 连接池类型详解

<dataSource type="POOLED">
    <property name="driver" value="${jdbc.driver}"/>
    <property name="url" value="${jdbc.url}"/>
    <property name="username" value="${jdbc.username}"/>
    <property name="password" value="${jdbc.password}"/>
</dataSource>

UNPOOLED:不使用连接池
每次执行 SQL 时都会创建新的连接,使用后直接关闭。适用于低并发场景,性能较差,一般仅用于简单测试。

配置示例:

<dataSource type="UNPOOLED">
    <!-- 同POOLED的属性配置 -->
</dataSource>
<dataSource type="JNDI">
    <property name="data_source" value="java:comp/env/jdbc/mybatis_db"/>
</dataSource>

2. 连接池的优势

二、动态 SQL:灵活拼接 SQL 语句

在实际开发中,查询条件往往是动态变化的(如多条件筛选、批量操作等)。MyBatis 的动态 SQL 标签可以优雅地解决 SQL 语句拼接问题,避免手动拼接导致的语法错误和 SQL 注入风险。

1. <if>标签:条件判断

<if>标签用于根据参数值动态生成 SQL 片段,常用来处理多条件查询。

示例场景:根据用户名和性别查询用户(参数非空时才添加条件)。

UserMapper 接口

public interface UserMapper {
    // 条件查询用户
    List<User> findByWhere(User user);
}

UserMapper.xml 配置

<select id="findByWhere" parameterType="user" resultType="user">
    select * from user
    <where>
        <!-- 当username非空且非空字符串时,添加条件 -->
        <if test="username != null and username != ''">
            and username like #{username}
        </if>
        <!-- 当sex非空且非空字符串时,添加条件 -->
        <if test="sex != null and sex != ''">
            and sex = #{sex}
        </if>
    </where>
</select>
@Test
public void testFindByWhere() {
    User user = new User();
    user.setUsername("%zz%"); // 模糊查询包含"zz"的用户名
    user.setSex("m");
    List<User> list = userMapper.findByWhere(user);
    // 遍历结果...
}

说明test属性中的表达式用于判断参数是否有效,where标签会自动处理多余的andor,避免 SQL 语法错误。 

2. <foreach>标签:遍历集合

<foreach>标签用于遍历集合或数组,常用来处理in查询或批量操作。

场景 1:查询 ID 在指定集合中的用户(in查询)

public class User {
    private List<Integer> ids; // 存储多个ID
    // 省略getter、setter
}
List<User> findByIds(User user);
<select id="findByIds" parameterType="user" resultType="user">
    select * from user
    <where>
        <!-- 
            collection:集合属性名(此处为ids)
            open:SQL片段开头
            close:SQL片段结尾
            separator:元素分隔符
            item:遍历的元素别名
        -->
        <foreach collection="ids" open="id in (" separator="," close=")" item="id">
            #{id}
        </foreach>
    </where>
</select>
@Test
public void testFindByIds() {
    User user = new User();
    List<Integer> ids = new ArrayList<>();
    ids.add(1);
    ids.add(2);
    ids.add(3);
    user.setIds(ids);
    List<User> list = userMapper.findByIds(user); // 查询ID为1、2、3的用户
}

场景 2:批量查询(or条件)

如需生成id = 1 or id = 2 or id = 3形式的 SQL,只需调整<foreach>openseparator

<foreach collection="ids" open="id = " separator="or id = " item="id">
    #{id}
</foreach>

3. <sql>与<include>标签:SQL 片段复用

对于频繁使用的 SQL 片段(如查询字段、表名等),可以用<sql>标签定义,再通过<include>标签引用,减少代码冗余。

示例:复用查询用户的 SQL 片段。

<!-- 定义SQL片段 -->
<sql id="userColumns">
    id, username, birthday, sex, address
</sql>
<!-- 引用SQL片段 -->
<select id="findAll" resultType="user">
    select <include refid="userColumns"/> from user
</select>

说明id为片段唯一标识,refid指定要引用的片段 ID,适用于多表查询中重复的字段列表。

三、一对多查询:用户与账户的关联

在实际业务中,表之间往往存在关联关系(如用户与账户:一个用户可以有多个账户)。MyBatis 通过<collection>标签处理一对多关联查询。

1. 表结构与实体类设计

实体类设计

public class Account implements Serializable {
    private Integer id;
    private Integer uid; // 关联用户ID
    private Double money;
    // 关联的用户对象
    private User user; 
    // 省略getter、setter
}
public class User implements Serializable {
    private Integer id;
    private String username;
    // 关联的账户列表
    private List<Account> accounts; 
    // 省略getter、setter
}

2. 多对一查询(账户关联用户)

查询所有账户,并关联查询所属用户的信息。

public interface AccountMapper {
    List<Account> findAll();
}
<select id="findAll" resultMap="accountMap">
    <!-- 关联查询账户和用户 -->
    select a.*, u.username, u.address 
    from account a
    left join user u on a.uid = u.id
</select>
<!-- 定义结果映射 -->
<resultMap id="accountMap" type="account">
    <id property="id" column="id"/>
    <result property="uid" column="uid"/>
    <result property="money" column="money"/>
    <!-- 关联用户对象(多对一) -->
    <association property="user" javaType="user">
        <result property="username" column="username"/>
        <result property="address" column="address"/>
    </association>
</resultMap>

说明<association>标签用于映射关联的单个对象,javaType指定对象类型。

3. 一对多查询(用户关联账户)

查询所有用户,并关联查询其名下的所有账户。

public interface UserMapper {
    // 查询用户及关联的账户
    List<User> findOneToMany();
}
<select id="findOneToMany" resultMap="userAccountMap">
    select u.*, a.id as aid, a.money 
    from user u
    left join account a on u.id = a.uid
</select>
<resultMap id="userAccountMap" type="user">
    <id property="id" column="id"/>
    <result property="username" column="username"/>
    <result property="birthday" column="birthday"/>
    <result property="sex" column="sex"/>
    <result property="address" column="address"/>
    <!-- 关联账户列表(一对多) -->
    <collection property="accounts" ofType="account">
        <id property="id" column="aid"/> <!-- 注意别名避免与用户ID冲突 -->
        <result property="money" column="money"/>
    </collection>
</resultMap>

说明<collection>标签用于映射关联的集合对象,ofType指定集合中元素的类型。

四、多对多查询:用户与角色的关联

多对多关系需要通过中间表实现(如用户与角色:一个用户可拥有多个角色,一个角色可分配给多个用户,通过user_role表关联)。

1. 表结构与实体类设计

实体类设计

Account 类(多对一:一个账户属于一个用户):

public class Account implements Serializable {
    private Integer id;
    private Integer uid; // 关联用户ID
    private Double money;
    // 关联的用户对象
    private User user; 
    // 省略getter、setter
}
public class User implements Serializable {
    private Integer id;
    private String username;
    // 关联的账户列表
    private List<Account> accounts; 
    // 省略getter、setter
}

2. 多对多查询实现

查询所有角色,并关联查询拥有该角色的用户信息。

public interface RoleDao {
    List<Role> findAll();
}
<select id="findAll" resultMap="roleMap">
    SELECT r.*, u.id as user_id, u.username 
    FROM role r
    JOIN user_role ur ON r.id = ur.RID
    JOIN user u ON u.id = ur.UID
</select>
<resultMap type="role" id="roleMap">
    <id property="id" column="id"/>
    <result property="role_name" column="role_name"/>
    <result property="role_desc" column="role_desc"/>
    <!-- 关联用户列表(多对多) -->
    <collection property="users" ofType="user">
        <id property="id" column="user_id"/> <!-- 别名避免与角色ID冲突 -->
        <result property="username" column="username"/>
    </collection>
</resultMap>
@Test
public void testFindAllRoles() {
    List<Role> roles = roleDao.findAll();
    for (Role role : roles) {
        System.out.println("角色:" + role.getRole_name());
        System.out.println("关联用户:" + role.getUsers());
    }
}

说明:多对多查询本质是双向的一对多查询,通过中间表建立关联,同样使用<collection>标签映射集合对象。

五、MyBatis 延迟加载策略

1. 延迟加载的概念

延迟加载(Lazy Loading)是一种数据库查询优化策略,其核心思想是:仅在需要使用关联数据时才进行实际查询。与立即加载(Eager Loading)相比,延迟加载避免了不必要的数据库访问,提高了系统性能。

对比示例(一对多关系)

2. 应用场景选择

场景加载策略示例
多对一关系立即加载查询账户时,同时加载所属用户
一对多 / 多对多延迟加载查询用户时,暂不加载账户信息

3. 多对一延迟加载实现

(1)配置文件示例

<!-- AccountMapper.xml -->
<resultMap type="Account" id="accountMap">
    <id column="id" property="id"/>
    <result column="uid" property="uid"/>
    <result column="money" property="money"/>
    <!-- 配置延迟加载:通过select属性指定关联查询方法 -->
    <association property="user" javaType="User" 
                 select="com.qcbyjy.mapper.UserMapper.findById" 
                 column="uid">
        <id column="id" property="id"/>
        <result column="username" property="username"/>
    </association>
</resultMap>

(2)核心配置参数

<!-- SqlMapConfig.xml -->
<settings>
    <!-- 开启延迟加载功能 -->
    <setting name="lazyLoadingEnabled" value="true"/>
    <!-- 禁用积极加载(默认false,按需加载) -->
    <setting name="aggressiveLazyLoading" value="false"/>
</settings>

 测试方法

@Test
    public void testFindAll1() throws IOException {
        // 先加载主配置文件,加载到输入流中
        InputStream inputStream = Resources.getResourceAsStream("SqlMapConfig.xml");
        // 创建SqlSessionFactory对象,创建SqlSession对象
        SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(inputStream);
        // 创建SqlSession对象
        SqlSession session = factory.openSession();
        // 获取代理对象
        AccountMapper mapper = session.getMapper(AccountMapper.class);
        // 1. 查询主对象(账户)
        List<Account> accounts = mapper.findAll();
        System.out.println("===== 主查询已执行 =====");
        // 2. 遍历账户,但不访问关联的用户
        for (Account account : accounts) {
            System.out.println("账户ID:" + account.getId() + ",金额:" + account.getMoney());
        }
        System.out.println("===== 未访问关联对象 =====");
        // 3. 首次访问关联的用户
        for (Account account : accounts) {
            System.out.println("===== 开始访问用户 =====");
            System.out.println("用户名:" + account.getUser().getUsername()); // 触发懒加载
            System.out.println("===== 访问用户结束 =====");
        }
        // 关闭资源
        session.close();
        inputStream.close();
    }

执行findAll()时,日志仅输出账户表的查询 SQL 。遍历账户但不访问用户时,无新的 SQL 输出

首次访问account.getUser()时,日志输出用户表的查询 SQL(按需加载) 

(3)工作原理

当执行account.getUser()时,MyBatis 会:

  1. 检查lazyLoadingEnabled是否为true
  2. 通过select属性调用UserMapper.findById(uid)方法;
  3. 将结果封装到Account.user属性中。

4. 一对多延迟加载实现

(1)配置文件示例

<!-- UserMapper.xml -->
<resultMap type="User" id="userMap">
    <id column="id" property="id"/>
    <result column="username" property="username"/>
    <!-- 配置延迟加载:集合属性 -->
    <collection property="accounts" ofType="Account" 
                select="com.qcbyjy.mapper.AccountMapper.findByUid" 
                column="id">
        <id column="id" property="id"/>
        <result column="money" property="money"/>
    </collection>
</resultMap>

(2)延迟加载触发时机

List<User> users = userMapper.findAll();
for (User user : users) {
    // 调用getAccounts()时触发延迟查询
    System.out.println(user.getAccounts()); 
}

测试代码:

    @Test
    public void testFindAllq() throws Exception {
        // 调用方法
        List<User> list = mapper.findAll();
        for (User user : list) {
            System.out.println(user.getUsername());
            System.out.println(user.getAccounts());
            System.out.println("==============");
        }
    }

5. 延迟加载注意事项

  1. N+1 查询问题:延迟加载可能导致 N+1 查询(主查询 1 次,关联查询 N 次),需结合二级缓存优化。
  2. Session 生命周期:延迟加载需确保关联查询时SqlSession未关闭(可通过openSession(true)保持会话)。
  3. 序列化问题:延迟加载的对象在序列化时可能丢失代理状态,需通过<setting name="serializationFactory" value="..."/>配置。

七、MyBatis 缓存机制

1. 缓存的基本概念

缓存是一种内存临时存储技术,用于减少数据库访问次数,提高系统响应速度。适合缓存的数据特点:

2. 一级缓存(SqlSession 级缓存)

(1)缓存原理

(2)缓存验证示例

@Test
public void testFirstLevelCache() {
    try (SqlSession session = sqlSessionFactory.openSession()) {
        UserMapper mapper = session.getMapper(UserMapper.class);
        // 第一次查询:触发SQL
        User user1 = mapper.findById(1);
        // 第二次查询:命中缓存
        User user2 = mapper.findById(1);
        System.out.println(user1 == user2); // 输出true(同一对象)
    }
}

(3)缓存失效场景

以下操作会导致一级缓存清空:

3. 一级缓存源码分析

核心源码位于BaseExecutor类:

// BaseExecutor.java
@Override
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
    BoundSql boundSql = ms.getBoundSql(parameter);
    // 1. 创建缓存Key
    CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql);
    // 2. 查询一级缓存
    return query(ms, parameter, rowBounds, resultHandler, key, boundSql);
}
@Override
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
    // 从本地缓存中获取
    List<E> list = localCache.getObject(key);
    if (list != null) {
        // 缓存命中
        handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
        return list;
    } else {
        // 缓存未命中,查询数据库
        return queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
    }
}

4. 一级缓存的应用建议

八、延迟加载与一级缓存的协同工作

当延迟加载与一级缓存结合时,需注意:

  1. 关联查询缓存:延迟加载的关联对象(如user.getAccounts())会被存入一级缓存;
  2. 会话隔离:不同SqlSession的延迟加载结果相互独立;
  3. 数据一致性:若主对象已缓存,关联对象的变更可能无法实时反映。

mysql缓存存的是语句,稍有修改就会更新缓存。一级缓存,输出的对象是同一个,二级缓存输出不同是因为通过序列化组装了两

一、一级缓存(SqlSession 级缓存)—— 同一个对象实例

1. 缓存本质与作用范围

一级缓存是 SqlSession 私有 的本地缓存,MyBatis 默认开启。在同一个 SqlSession 内,只要查询条件、SQL 语句相同,MyBatis 会直接从缓存取结果,不会重复访问数据库。

2. “输出对象是同一个” 的原因

try (SqlSession session = sqlSessionFactory.openSession()) {
    UserMapper mapper = session.getMapper(UserMapper.class);
    // 第一次查询,从数据库加载,存入一级缓存
    User user1 = mapper.getUserById(1);  
    // 第二次查询,命中一级缓存,直接返回 user1 的引用
    User user2 = mapper.getUserById(1);  
    System.out.println(user1 == user2); // 输出 true,是同一个对象实例
}

3. 缓存失效场景

当执行 updateinsertdeletecommitclose 等操作时,一级缓存会被清空 。后续查询会重新从数据库加载数据,存入新的对象实例到缓存。

二、二级缓存(Mapper 级缓存)—— 不同对象实例(因序列化 / 反序列化)

1. 缓存本质与作用范围

二级缓存是 Mapper 作用域 的缓存,需手动开启(在 Mapper XML 或注解中配置 )。它可以在多个 SqlSession 间共享,底层通常依赖序列化 / 反序列化机制存储数据 。

2. “输出不同对象” 的原因

// 开启二级缓存后,不同 SqlSession 测试
try (SqlSession session1 = sqlSessionFactory.openSession()) {
    UserMapper mapper1 = session1.getMapper(UserMapper.class);
    User user1 = mapper1.getUserById(1); 
    session1.commit(); // 提交后,数据可能同步到二级缓存(取决于配置)
}
try (SqlSession session2 = sqlSessionFactory.openSession()) {
    UserMapper mapper2 = session2.getMapper(UserMapper.class);
    User user2 = mapper2.getUserById(1); // 命中二级缓存,反序列化生成新对象
    System.out.println(user1 == user2); // 输出 false,是不同对象实例
}

3. 二级缓存的核心特点

三、一、二级缓存的核心差异对比

对比项一级缓存二级缓存
作用范围SqlSession 私有Mapper 作用域(跨 SqlSession 共享)
对象实例同一对象引用反序列化生成新对象
开启方式默认开启需手动配置(<cache> 标签或注解)
存储机制直接存对象引用存序列化后的字节流
数据一致性依赖 SqlSession 内操作,易维护需注意多表关联、更新同步问题

四、实际开发中的注意事项

  1. 一级缓存的 “隐式风险”
    若在同一个 SqlSession 内,先查询再更新数据,由于一级缓存未及时清理(需手动 commit/close 触发 ),可能拿到旧数据。建议在增删改后,及时 commit 或 close SqlSession,保证缓存与数据库一致。

  2. 二级缓存的 “使用前提”
    启用二级缓存时,实体类必须实现 Serializable 接口(否则序列化报错 );同时,若涉及多表关联查询,需注意缓存的更新策略(比如某张表数据变化后,关联的 Mapper 缓存需及时刷新 )。

  3. 缓存的合理选择

    • 一级缓存适合短生命周期的 SqlSession(如单次请求内的多次查询 );
    • 二级缓存适合查询频率高、数据变化少的场景(如系统字典表 ),但需谨慎处理数据更新后的缓存同步。

简单来说,一级缓存是 “同一个对象复用”,二级缓存是 “序列化后重新组装对象”,这种差异由它们的作用范围和存储机制决定。开发中根据业务场景合理利用缓存,既能提升性能,又能避免数据一致性问题~ 若你在实际配置或调试缓存时遇到具体问题(比如二级缓存不生效、序列化报错 ),可以接着展开说场景帮你分析 。

到此这篇关于MyBatis连接池、动态 SQL 与多表关联查询的文章就介绍到这了,更多相关mybatis多表关联查询内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

您可能感兴趣的文章:
阅读全文