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

标签: none

[2024年]-[红黑树技术联盟介绍]-[成员故事]


[2024年]-[JavaOffer]-[训练营]


已有 8 条评论

  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);

    1. 夜羽 夜羽

      多对多不是两者之间吗 难道是3个或者4个之间 ?

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

  4. Exia Exia

    不过还是挺感谢的,有空看看其它文章

  5. 彭伟中 彭伟中

    感谢无私奉献!

  6. 夜羽 夜羽

    例子不对吧 多对多不应该是类似 两个对象之间可以做的多对多了
    看例子是 一个用户有个多个订单 一个对订单有多个商品 这不就是一对多的顺延吗

    一对多不该是
    类似几个人共同支付一个订单 进行拼单的动作么

    这样就是 一个人名下可以有多个订单被支付 一个订单被多个人同时支付

    这样人与订单之间不就是多对对了么, 就是说用订单去查 能查到拼单的人,通过人去查就可以知道这个人有多少订单

    1. 啊这 啊这

      “一个订单中可以购买多种商品,一种商品也可以属于多个不同的订单”
      这才是多对多的例子,你看错了 而且多对多就是2个一对多组成的啊

添加新评论