多对多映射

优质
小牛编辑
132浏览
2023-12-01

有两张数据表,通过第三张数据表来表示关联关系,我们称之为多对多的映射

如上图,通过一个中间数据表的两个字段,分别指向两个对象的主键,可以实现多对多映射。所以,Pet.foods(一个 List<Food>
或者 Food.pets(一个List<Pet>)就是多对多映射。

在 POJO 中配置多对多映射

在 POJO 类中字段中增加注解 @ManyMany

@Table("t_food")
public class Food extends Pojo {

    @ManyMany(relation = "t_pet_food", from = "foodid", to = "petid")
    // 1.r.59之前需要写target参数
    // @ManyMany(target = Pet.class, relation = "t_pet_food", from = "foodid", to = "petid")
    private List<Pet> pets;

    public List<Pet> getPets() {
        return pets;
    }

    public void setPets(List<Pet> pets) {
        this.pets = pets;
    }

}

在 Food 对象中必须存在一个 List<Pet> 类型的字段,你的多对多映射就需要配置在这个字段上。通过 @ManyMany 注解告诉 Nutz.Dao
对象 Food 和 Pet 之间的关系,其中:

  • 1.r.59之前你需要使用 target 表示你要映射的对象类型
  • relation 为中间数据表的表名,它也支持动态表名
  • from 是中间数据表的字段名,这个字段将储存主对象的主键(上例的 Food 的主键)
  • to 是中间数据表的字段名,这个字段将储存映射对象的主键(上例的 Pet 的主键)

因此:

  • 数据库中必须存在一个中间表 t_pet_food
    • 该表有一个字段 foodid 对应到 Food 对象的主键
    • 该表有一个字段 petid 对应到 Pet 对象的主键
  • Nutz.Dao 通过 @ManyMany 这四个属性了解到:
    • 目标的 POJO 类 : Pet
    • 关联表(或者说:中间表):t_pet_food
    • 关联表的 foodid 字段对应到是本 POJO (Food)主键
    • 关联表的 petid 字段对应到是目标 POJO (Pet) 主键

NutDao 是如何连接关联表的

比如,下面的例子

//我们有两个 POJO
public class Pet {
    @Id
    public int id;
    @Name
    public String name;
}
//-------------------------------
public class Food {
    @Id
    public int id;
    @Name
    public String name;
}

我们设计了一个关联表关联这两个对象,表示一个宠物爱吃什么样的食物

t_pet_food
===================
 pid | fid
-----+------
 3   | 6
 9   | 8

那么我们可以为 Pet 类声明一个多对多关联

public class Pet {
    @Id
    public int id;
    @Name
    public String name;
    @ManyMany(relation="t_pet_food", 
            from="pid",
            to="fid")
    public List<Food> foods;
}

可以看到,我们指明了,Pet 类的 foods 字段,通过中间表 t_pet_food 来获取一组 Food 对象。
因为 @ManyMany 是声明在 Pet 类的字段上的,那么 Pet 类就被称为所谓的"宿主对象",而 Food
则是所谓的"目标对象"。from 指明关联表的 pid 字段的值代表宿主对象,而 to 指明 fid
字段代表目标对象。

看到这里,有的心思缜密的同学肯定会有一个小小怀疑,NutDao 怎么能知道 pid 对应到 Pet 哪个
字段呢?靠猜吗?恭喜你,答对了。NutDao 在解析到这个注解的时候,会看看 Pet 类,你是否在某个
字段上声明了 @Id 注解,如果没有,则试图看看你有没有在某个字段上声明了 @Name 注解。
当然,如果你没有声明 @Id 注解,而用整数字段作为 pid 一定会出错的,你必须把关联表改成:

t_pet_food
===================
 pid       | fid
-----------+------
 xiaobai   | 6
 xiaoqiang | 8

这个约定有点死板对吗?并且如果你用 VARCHAR 作为 pid 的数据表字段类型,但是你的 Pet 却
声明了 @Id 注解,一样会错,因为 @Id 注解优先。

读到这里,你一定感到很郁闷,因为你实在不想改变你的关联表,没关系,你可以下面一样声明你的
@ManyMany 注解

...
@ManyMany(relation="t_pet_food", 
            from="pid:name", 
            to="fid")
public List<Food> foods;
...

看,你为 from 声明了一个 "pid:name",这个冒号后面的,就是大声告诉 NutDao 请用 Pet.name
来映射这个 pid 字段。这样 NutDao 就不会自己瞎猜了。

同理,to 也有一样的属性。并且冒号后面的并不用一定是 PK 字段,只要是惟一性字段均可

说到这里,我不得不解释一下,NutDao 主要是通过你提供的注解来分析 POJO 的。我曾经考虑过,是不是
少让大家提供几个注解,我自行分析数据表,然后总能做出合理的决定。但是 ... 数据种类实在太多了,
各自有各自的脾气,从植物学的角度来说,这很有可能是一个一望无际的大坑,如果我那么做了,估计
我会有相当长的一段时间在坑里幸福的遨游,所以我收起了自己的胆量,弱弱的给出了一组注解,以便
我能用更少的代码做更多的事情。当然,就 @ManyMany 这个用法,用冒号分隔,通过注解指明映射字段,
我想应该还是可以被多数人接受的。

你不仅可以在集合类型字段上声明一对多映射

本 POJO 类的 @Many 映射,可以不止声明在 List 对象上,它还可以声明在

  • 数组
  • Map
  • POJO

详情,可以参看 一对多映射 的相关描述

插入操作

如果你已经实现准备好了这样的对象:

Food food = new Food("Fish");

List<Pet> pets = new ArrayList<Pet>();
pets.add(new Pet("XiaoBai"));
pets.add(new Pet("XiaoHei"));

food.setPets(pets);

那么你可以一次将 food 以及它对应的 pets 一起插入到数据表中,并在关联表中插入对应的记录

dao.insertWith(food, "pets");

Nutz.Dao 会根据正则表达式 "pets" 寻找可以被匹配上的映射字段(只要声明了 @One, @Many, @ManyMany 任何一个注解,都是映射字段)
并根据注解具体的配置信息,执行相应的 SQL。比如上面的操作,会实际上:

执行 SQL : INSERT INTO t_food (name) VALUES("Fish");
执行 SQL 获取 最大值: SELECT MAX(id) FROM t_food  // 假设返回的值是 6
循环 food.pets
    执行 SQL: INSERT INTO t_pet (name) VALUES("XiaoBai");
    执行 SQL 获取 最大值: SELECT MAX(id) FROM t_pet  // 假设返回的值是 97
    执行 SQL 插入关联: INSERT INTO t_pet_food (foodid, petid) VALUES(6, 97);
    ...

这里通过 SELECT MAX 来获取插入的最大值,是默认的做法,如果你想修改这个默认做法,请参看 关于主键一章。

  • 这里因为是多对多映射,所以会首先插入主对象并循环插入映射对象,以便获得双发的主键
  • 如果你的对象中包括多个 @ManyMany 字段,被你的正则式匹配上,那么这些字段对应的字段(如果不为null)都会被匹配,一次被执行

当然,你要想选择仅仅只插入映射字段的话,你可以:

dao.insertLinks(food,"pets");

如果 food.id 的值为 6,那么上述操作实际上会执行:

循环 food.pets
    执行 SQL: INSERT INTO t_pet (name) VALUES("XiaoBai");
    执行 SQL 获取 最大值: SELECT MAX(id) FROM t_pet  // 假设返回的值是 97
    执行 SQL 插入关联: INSERT INTO t_pet_food (foodid, petid) VALUES(6, 97);
    ...

看,并不会插入 food 对象。

如果你已经存在了 food 和 pets 对象,你仅仅打算将它们关联起来,那么你可以

dao.insertRelation(food,"pets");

如果 food.id 的值为 6,那么上述操作实际上会执行:

循环 food.pets
    执行 SQL 插入关联: INSERT INTO t_pet_food (foodid, petid) VALUES(6, 97);
    ...

看,仅仅只会插入 food 和 pets 的关联

获取操作

仅仅获取映射对象:

Food food = dao.fetch(Food.class, "Fish");
dao.fetchLinks(food, "pets");

这会执行操作:

执行 SQL: SELECT * FROM t_food WHERE name='Fish'; // 如果 food.id 是6
执行 SQL: SELECT * FROM t_pet WHERE id IN (SELECT petid FROM t_pet_food WHERE foodid=6)

但是 Nutz.Dao 没有提供一次获取 food 对象以及 pets 对象的方法,因为,你完全可以把上面的两句话写在一行上:

Food food = dao.fetchLinks(dao.fetch(Food.class, "Fish"), "pets");

然后,你可以通过 food.getPets() 得到 Nutz.Dao 为 food.pets 字段设置的值。

更新操作

同时更新 food 和 pet

dao.updateWith(food, "pets");

这会执行

执行SQL: UPDATE t_food ....
循环 food.pets 并依次执行SQL: UPDATE t_pet ...

仅仅更新 pets

dao.updateLinks(food, "pets");

这会执行

循环 food.pets 并依次执行SQL: UPDATE t_pet ...

删除操作

同时删除 food 和 pets

dao.deleteWith(food, "pets");

仅仅删除 pets

dao.deleteLinks(food, "pets");

清除 pets

dao.clearLinks(food, "pets");

删除与清除的区别在于

  • 删除不仅会删掉 t_pet_food 里的记录,还会逐个调用 dao.delete 来删除 pet 对象。
  • 而清除只会执行一条 SQL 来删除 t_pet_food 中的记录(即中间表中的记录),但是 t_pet 和 t_food 表中的数据不会被删除。