今天谈的是Laravel中一个非要有用,但一开始可能有点难理解的功能。Pivot 表是两个“主表”之间关系的中间表。
Pivot 表实例
在官方文档中,他们用 用户-角色(User-Role) 关系来做例子,用户可能会属于多个角色,反之亦然。为了使大家理解的更清楚,我们这里举另一个例子:商店(Shops )与商品(Products)。
我们假设一个商家有多个商店遍布全国各地,并且有各种各样的商品,他们需要存储哪个商品是由哪个商店卖出的信息。这是一个多对多关系非常好的例子:一个商品可以属于多个商店,而一个商店又会有很多的商品。
所以这里就有一个潜在的数据库结构:
shops
– id
– name
products
– id
– name
product_shop
– product_id
– shop_id
列表中最后一个表 product_shop 就是标题中被称作“pivot”的表。这里有几点需要注意的:
- Pivot表的名字应该包含两个表名的单数形式,由下划线分开,并按字母顺序排列,所以我们最终得到的是 product_shop,而不是 shop_product 。
- 你可以使用
artisan make:migration
命令来创建 pivot 表,也可以通过 Jeffrey Way 的扩展包Laravel 5 Generators Extended 中的artisan make:migration:pivot
命令。 - Pivot 表字段:默认情况下,只需包含两个字段–分别对应两个表的外键,我们这里是 product_id 和 shop_id。如果需要的话,你也可以添加跟多字段,后面我们会进行讨论。
多对多关系模型:BelongsToMany
我们已经有了数据库表和迁移,现在让我们为它们创建模型。这里主要是分配一个多对多的关系,它可以在任何一个主表模型中定义。
选择1:app/Shop.php
class Shop extends Model { /** * The products that belong to the shop. */ public function products() { return $this->belongsToMany('App\Product'); } }
选择2:app/Product.php
class Product extends Model { /** * The shops that belong to the product. */ public function shops() { return $this->belongsToMany('App\Shop'); } }
实际上,你也可以都做,主要是看你在代码中如何使用这个关系–你需要 $shop->products
还是 $product->shops
,或者两者都需要。
现在的这个声明,Laravel会假设 pivot 表命名遵守上面提到的规则,也就是 product_shop
。假如不是的话(比如是复数形式),你可以提供第二个参数:
public function products() { return $this->belongsToMany('App\Products', 'products_shops'); }
此外,你还可以指定 pivot 表中字段的名称,如果它们不用于 product_id
和 shop_id
。只需要添加两个参数:第一个是当前模型的字段,另一个是需要关联的模型的字段:
public function products() { return $this->belongsToMany('App\Products', 'products_shops', 'shops_id', 'products_id'); }
这里的一个主要好处是,你不需要创建一个单独的 ProductShop 模型。
多对多关系管理:attach-detach-sync
现在已经准备好了数据表和模型,那么,现在的问题是我们如果只用两个模型而不用第三个中间模型来保存数据呢?看看下面几点。
例如,我们需要给当前 商店(shop>) 实例添加一个 商品(product),我们可以使用关系函数中的 attach()
方法:
$shop = Shop::find($shop_id); $shop->products()->attach($product_id);
结果就是我们会给 product_shop 表中添加新的一列,值分别为 $product_id
和 $shop_id
的值。
相似的,我们也可以解除(detach)一个关系,假设我们把一个商品从商店中移除:
$shop->products()->detach($product_id);
或者更进一步,删除一个特定商店中所有的商品,只需要调用该方法,不传递参数:
$shop->products()->detach();
你可以用过传递数组参数来附加和分离关系:
$shop->products()->attach([123, 456, 789]); $shop->products()->detach([321, 654, 987]);
另一个非要有用的函数是更新整个 pivot 表。一个常见的例子是:管理面板中,一个商品下面有多个商店的多选框,在执行更新操作的时候,需要检查所有的商店,删除新的多选数组中不存在的,并添加或更新有的。一件头疼的事情。
但现在不是了,有一个叫做 sync()
的方法,它接受一个数组参数,然后会自动执行完所有的同步工作。
$product->shops()->sync([1, 2, 3]);
结果就是,不管之前 product_shop 表中的值是怎么样的,调用该方法之后,只会有 shop_id
是1、2、3的三行。
给 Pivot 表添加额外列
我上面提到了,你很可能想给 pivot 表添加额外的字段。在我们的例子中,保存特定商店中商品的数量和时间戳是有必要的。我们可以跟通常一样使用 migration
来添加字段,但我们还需要对模型进行一些修改:
public function products() { return $this->belongsToMany('App\Products') ->withPivot('products_amount', 'price') ->withTimestamps(); }
你可以看到,我们能通过一个简单的 withTimestamps()
方法来添加时间戳,以及通过给 withPivot()
方法添加参数来添加额外的字段。
现在我们就可以在代码的循环中使用这些值了,有一个叫做 pivot
的属性:
foreach ($shop->products as $product) { echo $product->pivot->price; }
基本上,->pivot
表示 pivot 中间表,并以此来访问我们提到的字段,如 created_at
。
现在,如何在调用 attach()
的方法是添加这些值呢?该方法接受另一个数组参数,你可以把所有需要的附加字段添加到其中。
$shop->products()->attach(1, ['products_amount' => 100, 'price' => 49.99]);
总结
因此,pivot 表可以非常方便的处理 Eloquent 中的多对多关系,它不需要为这个中间表单独的创建一个模型。希望本文对你有所帮助。
yuqifeng 2015/10/19 14:23
发现文中有一个错误。在shop模型中,返回一个belognsToManys时,也就是第8行,应该是product,不是复数形式。
Specs 2015/10/19 22:17
@ 多谢指正~
Wu Yu 2016/06/06 16:26
你好,这种{多对多}的关系中能不能通过laravel自己的Eloquent 实现呢?blogs - blog_tag - tags 现在可以通过$tag = App\Tag::find($arg); 通过$tag->blogs找到所有所属的blog ,反之毅然但是实际需求中,blog的tag可以全部显示在一页,但是一个tag下的文章可能有很多,需要分页tag下的blog实现分页该如何实现呢?
Specs 2016/06/06 21:44
@ 这本身就是 Laravel 的 Eloquent 实现的呀~ 分页就和普通的一样,类似:$tag->articles()->latest()->simplePaginate(10);
Wu Yu 2016/06/09 00:45
@ 感谢!,开始用的3张表的join,还是Eloquent看着清晰多了
LIJUNWEI 2016/11/27 18:19
请问Specs,pivot表中应该使用物理外键吗?另外就是pivot的主键应该是单独的一个自增id好还是联合table_a_id和table_b_id呢?有点犹豫
Specs 2016/12/01 09:28
@ 其实那个自增 ID 并没有什么用。给另外两个主 ID 加上索引就好了
666 2020/08/08 16:49
厉害了