MyBatis教程大全


 MyBatis SQL 映射

     MyBatis select 标签

     MyBatis 多数据库支持

     MyBatis selectKey 标签作用

     MyBatis @SelectKey注解用法介绍

     MyBatis @SelectKey注解用法详细介绍

     MyBatis keyProperty 属性介绍

     MyBatis insert、update 和 delete 元素

     MyBatis sql 元素

 MyBatis SQL 参数映射

     MyBatis SQL 参数映射

 MyBatis 动态SQL

     MyBatis 动态SQL与数据准备

     MyBatis if 标签

     MyBatis if else 用法

     MyBatis choose、when、otherwise 标签

     MyBatis where 标签

     MyBatis set 标签

     MyBatis foreach 标签

     MyBatis bind 标签

     MyBatis trim 标签

 MyBatis SQL 结果映射

 MyBatis SQL 结果之关系映射

 MyBatis 使用介绍

     MyBatis typeAliases 类型别名

     MyBatis typeHandlers 类型处理器

     MyBatis Transaction 事务接口

     MyBatis transactionManager 事务管理

     SqlSessionFactory 介绍

     MyBatis 核心对象 SqlSession

     MyBatis 初始化 创建 Session 实例

     MyBatis ObjectFactory 对象工厂

     MyBatis缓存机制:一级缓存和二级缓存

     MyBatis 常用注解

 MyBatis 配置文件

     MyBatis 配置文件

 MyBatis 映射

     MyBatis 映射简介

     MyBatis ResultMap 映射

     MyBatis 自动映射

     MyBatis 高级映射

     MyBatis 集合映射

     MyBatis 关联映射

     MyBatis 一对一关联映射

     MyBatis 一对多关联映射

     MyBatis 多对多关联映射

     MyBatis 一对一(注解形式)

     MyBatis 一对多(注解形式)

     MyBatis 多对多(注解形式)

     MyBatis resultMap 元素

 MyBatis 面试题库

     #{}和${}的区别是什么?

     数据库链接中断如何处理?

     数据库插入重复如何处理?

     事务执行过程中宕机的应对处理方式

     Java客户端中的一个Connection问题

MyBatis的多对多关联关系

本文修订于2019年3月17日

SQL脚本

在实际项目开发中,多对多关系也是非常常见的关系,比如,一个购物系统中,一个用户可以有多个订单,这是一对多的关系;一个订单中可以购买多种商品,一种商品也可以属于多个不同的订单,订单和商品就是多对多的关系。对于数据库中多对多关系建议使用一个中间表来维护关系,中间表中的订单d作为外键参照订单表的id,商品id作为外键参照商品表的id。下面我们就用一个简单示例来看看MyBatis怎么处理多对多关系。

首先,在数据库创建三个表:TB_USER、TB_ARTICLE和TB_ORDER,再创建一个中间表维护TB_ARTICLE和TB_ORDER的关系,并插入测试数据。SQL脚本如下:

drop table if exists tb_user;

-- 创建用户表
CREATE TABLE tb_user (
id INT PRIMARY KEY AUTO_INCREMENT,
username VARCHAR(18),
loginname VARCHAR(18),
PASSWORD VARCHAR(18),
phone VARCHAR(18),
address VARCHAR(18)
);

-- 插入用户表测试数据
INSERT INTO tb_user (username,loginname,PASSWORD,phone,address)
VALUES('马云','jack','123456','13600000000','杭州');


drop table if exists tb_article;

-- 创建商品表
CREATE TABLE tb_article(
id INT PRIMARY KEY AUTO_INCREMENT,
NAME VARCHAR(18),
price DOUBLE,
remark VARCHAR(18)
);
-- 插入商品表测试数据
INSERT INTO tb_article(NAME,price,remark) VALUES('精通Python自然语言处理',108.9,'经典著作');

INSERT INTO tb_article(NAME,price,remark) VALUES('自然语言处理原理与实践',99.9,'经典著作');

INSERT INTO tb_article(NAME,price,remark) VALUES('大数据架构详解',89.9,'经典著作');

INSERT INTO tb_article(NAME,price,remark) VALUES('推荐系统实践',69.9,'经典著作');


drop table if exists tb_order;

-- 创建订单表
CREATE TABLE tb_order(
id INT PRIMARY KEY AUTO_INCREMENT,
CODE VARCHAR(32),
total DOUBLE,
user_id INT,
FOREIGN KEY (user_id) REFERENCES tb_user(id)
);

-- 插入订单表测试数据
INSERT INTO tb_order(CODE,total,user_id)VALUES('20180315ORDER1212',388.6,1);
INSERT INTO tb_order(CODE,total,user_id)VALUES('20180315ORDER1213',217.8,1);


drop table if exists tb_item;
-- 创建中间表
CREATE TABLE tb_item(
order_id INT,
article_id INT,
amount INT,
PRIMARY KEY (order_id,article_id),
FOREIGN KEY (order_id) REFERENCES  tb_order(id),
FOREIGN KEY (article_id) REFERENCES tb_article(id)
);

-- 创建插入中间表数据
INSERT INTO tb_item(order_id,article_id,amount)VALUES(1,1,1);

INSERT INTO tb_item(order_id,article_id,amount)VALUES(1,2,1);

INSERT INTO tb_item(order_id, article_id,amount)VALUES(1,3,2);

INSERT INTO tb_item(order_id, article_id,amount)VALUES(2,4,2);

INSERT INTO tb_item(order_id, article_id,amount)VALUES(2,1,1);

提示:
t_order表的user_id作为外键参照tb_user表的主键id。tb_item表作为中间表,用来维护tb_article和tb_order的多对多关系,tb_item表的order_id作为外键参照tb_order表的主键id,article_id作为外键参照tb_article 表的主键id

实体类

接下来, 建一个User对象、一个Article对象和一个Order对象分别映射tb_user、tb_article和tb_order表。

import java.io.Serializable;
import java.util.List;

public class User implements Serializable
{
    private static final long serialVersionUID = 1L;
    private Integer id; // 用户id,主键
    private String username; // 用户名
    private String loginname; // 登录名
    private String password; // 密码
    private String phone; // 联系电话
    private String address; // 收货地址

    // 用户和订单是一对多的关系,即一个用户可以有多个订单
    private List<Order> orders;

    public User()
    {
        super();
    }

    public User(String username, String loginname, String password, String phone, String address)
    {
        super();
        this.username = username;
        this.loginname = loginname;
        this.password = password;
        this.phone = phone;
        this.address = address;
    }

    public Integer getId()
    {
        return id;
    }

    public void setId(Integer id)
    {
        this.id = id;
    }

    public String getUsername()
    {
        return username;
    }

    public void setUsername(String username)
    {
        this.username = username;
    }

    public String getLoginname()
    {
        return loginname;
    }

    public void setLoginname(String loginname)
    {
        this.loginname = loginname;
    }

    public String getPassword()
    {
        return password;
    }

    public void setPassword(String password)
    {
        this.password = password;
    }

    public String getPhone()
    {
        return phone;
    }

    public void setPhone(String phone)
    {
        this.phone = phone;
    }

    public String getAddress()
    {
        return address;
    }

    public void setAddress(String address)
    {
        this.address = address;
    }

    public List<Order> getOrders()
    {
        return orders;
    }

    public void setOrders(List<Order> orders)
    {
        this.orders = orders;
    }

    @Override
    public String toString()
    {
        return "User [id=" + id + ", username=" + username + ", loginname=" + loginname + ", password=" + password
                + ", phone=" + phone + ", address=" + address + "]";
    }

}

用户和订单是一对多的关系,即一个用户可以有多个订单。在User类中定义了一个orders属性,该属性是一个List集合,用来映射一对多的关联关系,表示一个用户有多个订单。

import java.io.Serializable;
import java.util.List;

public class Order implements Serializable
{
    private static final long serialVersionUID = 1L;
    private Integer id; // 订单id,主键
    private String code; // 订单编号
    private Double total; // 订单总金额

    // 订单和用户是多对一的关系,即一个订单只属于一个用户
    private User user;

    // 订单和商品是多对多的关系,即一个订单可以包含多种商品
    private List<Article> articles;

    public Order()
    {
        super();
    }

    public Order(String code, Double total)
    {
        super();
        this.code = code;
        this.total = total;
    }

    public Integer getId()
    {
        return id;
    }

    public void setId(Integer id)
    {
        this.id = id;
    }

    public String getCode()
    {
        return code;
    }

    public void setCode(String code)
    {
        this.code = code;
    }

    public Double getTotal()
    {
        return total;
    }

    public void setTotal(Double total)
    {
        this.total = total;
    }

    public User getUser()
    {
        return user;
    }

    public void setUser(User user)
    {
        this.user = user;
    }

    public List<Article> getArticles()
    {
        return articles;
    }

    public void setArticles(List<Article> articles)
    {
        this.articles = articles;
    }

    @Override
    public String toString()
    {
        return "Order [id=" + id + ", code=" + code + ", total=" + total + "]";
    }

}

订单和用户是多对一的关系,一个订单只属于一个用户,在Order类中定义了一个user属性,用来映射多对一的关联关系,表示该订单的用户;订单和商品是多对多的关系,即一个订单中可以包含多种商品,在Order类中定义了一个articles属性,该属性是一个List集合,用来映射多对多的关联关系,表示一个订单中包含多种商品。

import java.io.Serializable;
import java.util.List;

public class Article implements Serializable
{
    private static final long serialVersionUID = 1L;
    private Integer id; // 商品id,主键
    private String name; // 商品名称
    private Double price; // 商品价格
    private String remark; // 商品描述

    // 商品和订单是多对多的关系,即一种商品可以包含在多个订单中
    private List<Order> orders;

    public Article()
    {
        super();
    }

    public Article(String name, Double price, String remark)
    {
        super();
        this.name = name;
        this.price = price;
        this.remark = remark;
    }

    public Integer getId()
    {
        return id;
    }

    public void setId(Integer id)
    {
        this.id = id;
    }

    public String getName()
    {
        return name;
    }

    public void setName(String name)
    {
        this.name = name;
    }

    public Double getPrice()
    {
        return price;
    }

    public void setPrice(Double price)
    {
        this.price = price;
    }

    public String getRemark()
    {
        return remark;
    }

    public void setRemark(String remark)
    {
        this.remark = remark;
    }

    public List<Order> getOrders()
    {
        return orders;
    }

    public void setOrders(List<Order> orders)
    {
        this.orders = orders;
    }

    @Override
    public String toString()
    {
        return "Article [id=" + id + ", name=" + name + ", price=" + price + ", remark=" + remark + "]";
    }

}

商品和订单是多对多的关系,即一种商品可以出现在多个订单中。在Article 类中定义了一个orders 属性,该属性是一个List集合,用来映射多对多的关联关系,表示该商品关联的多个订单。

映射文件

<mapper namespace="cn.mybatis.mydemo3.mapper.UserMapper">

    <resultMap type="cn.mybatis.mydemo3.domain.User"
        id="userResultMap">
        <id property="id" column="id" />
        <result property="username" column="username" />
        <result property="loginname" column="loginname" />
        <result property="password" column="password" />
        <result property="phone" column="phone" />
        <result property="address" column="address" />
        <!-- 一对多关联映射:collection -->
        <collection property="orders" javaType="ArrayList"
            column="id" ofType="cn.mybatis.mydemo3.domain.Order"
            select="cn.mybatis.mydemo3.mapper.OrderMapper.selectOrderByUserId"
            fetchType="lazy">
            <id property="id" column="id" />
            <result property="code" column="code" />
            <result property="total" column="total" />
        </collection>
    </resultMap>

    <select id="selectUserById" parameterType="int"
        resultMap="userResultMap">
        SELECT * FROM tb_user WHERE id = #{id}
    </select>

</mapper>

UserMapper.xml中定义了一一个<select.../>,其根据id查询用户信息。由于User 类除了简单的属性id、usemame、loginame、password和address之外,还有一个关联对象orders,所以返回的是一个名为userResultMap的resultMap。由于orders是一个List集合,因此userResultMap 中使用了<collection.../>元素映射一对多的关联关系,select属性表示会使用columm属性的id值作为参数执行OrderMapper中定义的selectOrderByUserId查询该用户所下的所有订单,查询出的数据将被封装到property表示的orders对象当中。注意,一对多使用的都是lazy(懒加载)。

<mapper namespace="cn.mybatis.mydemo3.mapper.OrderMapper">

    <resultMap type="cn.mybatis.mydemo3.domain.Order"
        id="orderResultMap">
        <id property="id" column="oid" />
        <result property="code" column="code" />
        <result property="total" column="total" />
        <!-- 多对一关联映射:association -->
        <association property="user"
            javaType="cn.mybatis.mydemo3.domain.User">
            <id property="id" column="id" />
            <result property="username" column="username" />
            <result property="loginname" column="loginname" />
            <result property="password" column="password" />
            <result property="phone" column="phone" />
            <result property="address" column="address" />
        </association>
        <!-- 多对多映射的关键:collection -->
        <collection property="articles" javaType="ArrayList"
            column="oid" ofType="cn.mybatis.mydemo3.domain.Article"
            select="cn.mybatis.mydemo3.mapper.ArticleMapper.selectArticleByOrderId"
            fetchType="lazy">
            <id property="id" column="id" />
            <result property="name" column="name" />
            <result property="price" column="price" />
            <result property="remark" column="remark" />
        </collection>
    </resultMap>

    <!-- 注意,如果查询出来的列同名,例如tb_user表的id和tb_order表的id都是id,同名,需要使用别名区分 -->
    <select id="selectOrderById" parameterType="int"
        resultMap="orderResultMap">
        SELECT u.*,o.id AS oid,CODE,total,user_id
        FROM tb_user u,tb_order o
        WHERE u.id = o.user_id
        AND o.id = #{id}
    </select>

    <!-- 根据userid查询订单 -->
    <select id="selectOrderByUserId" parameterType="int"
        resultType="cn.mybatis.mydemo3.domain.Order">
        SELECT * FROM tb_order WHERE user_id = #{id}
    </select>

</mapper>

OrderMapper.xml中定义了一个<select id="selectOrderByUserId".../>其根据用户id查询订单信息,返回的是简单的Order对象。还定义了一<select id="selectOrderById.../>,其根据订单id 查询订单信息,由于 Order类和用户是多对一关系,和商品是多对多关系,而多对一通常都是立即加载,因此SQL语句是一条关联了tb_user和tb_order的多表查询语句。查询结果返回一个名为orderResultMap的resultMap。orderResultMap中使用了< association>元素映射多对一的关联关系,其将查询到的用户信息装载到Order 对象的user属性当中;orderResutMap中还使用了<collection..>元素映射多对多的关联关系,select属性表示会使用column 属性的oid 值作为参数执行ArticleMapper 中定义的selectArticleByOrderd 查询该订单中的所有商品,查询出的数据将被封装到property表示的articles对象当中。注意,一对多使用的都是lazy(懒加载)。

提示:
因为多表查询返回的结果集中tb user有个id列,tb_order也有个id列,当列同名时,MyBatis使用的元素中的column属性如果是id,则MyBatis会默认使用查询出的第一个id列。为了区分同名的列,最好的方法是给列取一个别名。SQL 语句中的o.id AS o.oid,resultMap中的column="oid"就是指使用的是tb_order 表的id:

<mapper namespace="cn.mybatis.mydemo3.mapper.ArticleMapper">

    <select id="selectArticleByOrderId" parameterType="int"
        resultType="cn.mybatis.mydemo3.domain.Article">
        SELECT * FROM tb_article WHERE id IN (
        SELECT article_id FROM tb_item WHERE order_id = #{id}
        )
    </select>

</mapper>

ArticleMape.xml 中定义了一个<select id="selectArticleByOrderId".../>其根据订单id查询订单关联的所有商品,由于订单和商品是多对多的关系,数据库使用了一一个中间表tb_item维护多对多的关系,故此处使用了一个子查询,首先根据订单id定位到中间表中查询出所有的商品,之后根据所有商品的id 查询出所有的商品信息,并将这些信息封装到Atrticle对象当中。

映射接口

import cn.mybatis.mydemo3.domain.User;

public interface UserMapper
{
    User selectUserById(int id);
}

import cn.mybatis.mydemo3.domain.Order;

public interface OrderMapper
{
    Order selectOrderById(int id);
}

import cn.mybatis.mydemo3.domain.Article;

public interface ArticleMapper
{
    Article selectArticleByOrderId(int id);
}

测试类


public class App
{
    public static void main(String[] args) throws Exception
    {
        // 读取mybatis-config.xml文件
        InputStream inputStream = Resources.getResourceAsStream("mybatis-config.xml");
        // 初始化mybatis,创建SqlSessionFactory类的实例
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
        // 创建Session实例
        SqlSession session = sqlSessionFactory.openSession();

        App a = new App();

        // 根据用户id查询用户,测试一对多
        //        a.testSelectUserById(session);
        // 根据订单id查询订单,测试多对多
        a.testSelectOrderById(session);

        // 提交事务
        session.commit();
        // 关闭Session
        session.close();
    }

    // 测试一对多,查询班级User(一)的时候级联查询订单Order(多)  
    public void testSelectUserById(SqlSession session)
    {
        // 获得UserMapper接口的代理对象
        UserMapper um = session.getMapper(UserMapper.class);
        // 调用selectUserById方法
        User user = um.selectUserById(1);
        // 查看查询到的user对象信息
        System.out.println(user.getId() + " " + user.getUsername());
        // 查看user对象关联的订单信息
        List<Order> orders = user.getOrders();
        for (Order order : orders)
        {
            System.out.println(order);
        }
    }

    // 测试多对多,查询订单Order(多)的时候级联查询订单的商品Article(多)  
    public void testSelectOrderById(SqlSession session)
    {
        // 获得OrderMapper接口的代理对象
        OrderMapper om = session.getMapper(OrderMapper.class);
        // 调用selectOrderById方法
        Order order = om.selectOrderById(2);
        // 查看查询到的order对象信息
        System.out.println(order.getId() + " " + order.getCode() + " " + order.getTotal());
        // 查看order对象关联的用户信息
        User user = order.getUser();
        System.out.println(user);
        // 查看order对象关联的商品信息
        List<Article> articles = order.getArticles();
        for (Article article : articles)
        {
            System.out.println(article);
        }
    }
}

运行App类的main方法,首先测试testSelectUserById方法,根据用户id查询用户。控制台显示如下:

DEBUG [main]--> Preparing: SELBCT * FROM tb_user WHERE id = ?
DEBUG [main]==> Parameters: 1(Integer)
DEBUG [main]<-- Total : 1
1 马云
DEBUG [main]--> Preparing: SELBCT * FROM tb_order WHERE user_ id = ?
DEBUG [main]--> Parameters: 1(Integer)
DEBUG [mainj<== Total : 2
Order [id=1,code=20180315ORDER1212,tota1=388.6]
order [1d=2,code=20180315ORDER1213,total=217.81]

可以看到,MyBatis执行了根据用户id查询用户的SQL语句,查询出了用户信息; 由于测试方法中立即又获取了用户的订单集合,故MyBatis又执行了根据用户id查询订单的SQL语句,查询出了该用户的两个订单。
接下来测试testSelectOrderById方法,根据订单id查询订单信息。控制台显示如下:
DEBUG Emain]-->Preparing: SELECT u.*, o.id AS oid, CODE, total, user_id FROM tb.user u, tb.order o WHERE u.id = o.user_id AND o.id = ?
DEBUG (main]--> Parameters: 2(Integer)
DEBUG [main]<==
2 20180315ORDER1213 217.8
User [id=1,username=马云,loginame=jack,password=123456,phone=13600000000,address=杭州]
可以看到,MyBatis执行了一个多表连接查询,同时查询出了订单信息和用户信息,由于测试方法中注释了查询订单中的商品代码,故MyBatis采用了懒加载机制,没有立即查询商品信息。

取消testSelectOrderByld方法中查询订单中的商品的代码注释,再次执行。控制台显示如下:

DEBUG [main]--> Preparing: SBLECT u.*, o.id AS o.oid, CODE, total, user_id FROM tb user u, tb_order o WHERE u.id = o.user_id AND o_id = ?
DEBUG [main]--> Parameters: 2(Integer)
DEBUG [main]<== Total: 1
2 20180315ORDER1213 217.8
User [id=1,username=马云,loginname=jack,password=123456,phone-13600000000,address=杭州]
DEBUG [main]--> Preparing: SELBCT* FROM tb_article WHERE id IN (SEIBCT artiche_id FROM tb_item WHERB order_id = ?)
DEBUG [main]--> Parameters: 2(Integer)
DEBUG [main]<== Total: 2
Article [id=1,name=精通Python自然语言处理,price=108.9,remark=经典著作]
Article [id=4,name=推荐系统实践,price=69.9,remark=经典著作]

可以看到,MyBatis 执行了ArticleMapper.xm中定义的子查询,查询出了订单所关联的所有商品信息。

提示:
多对多查询因为关联到中间表查询,所以读者需要对数据库的SQL知识有一定的了解。