本文修订于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.User"
            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知识有一定的了解。

标签: none

入群须知:

凡是加入我群者,皆要严守群规,每周六、日是群规反思日。群规的要义有三点∶

(1)坚持系统化的学习方式,由量变到质变。仅仅解决工作中的问题,并不叫系统化的学习。

(2)坚持以价值为导向的学习方式,扔掉低价值知识[配置、调参、安装],聚焦高价值知识[结构、算法、优化],推动量变到质变的进程。

已有 3 条评论

  1. vay vay

    sql语句有问题

  2. wsw321 wsw321

    教程不错,SQL 脚本太多错误了,我把改好的发这里。
    -- 创建用户表
    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','杭州');
    -- 创建商品表
    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,'经典著作');
    -- 创建订单表
    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);
    -- 创建中间表
    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);

  3. 好的,谢谢你啊。为你的热心点赞,我这周开始准备把全站内容走查一遍。

添加新评论