如何处理Doctrine Associations / Relations
假设你应用程序中的产品属于一确定的分类。这时你需要一个Category
类对象并一种方法把Product
和Category
对象联系在一起。
首先我们创建Category
实体,我们最终要通过Doctrine来对其进行持久化,所以我们这里让Doctrine来帮我们创建这个类。
1 2 3 |
$ PHP bin/console doctrine:generate:entity --no-interaction \
--entity="AppBundle:Category" \
--fields="name:string(255)" |
该命令行为你生成一个Category
实体,包含id
字段和name
字段以及相关的getter和setter方法。
关系映射元数据 ¶
在这个例子中,每个分类都关联许多的产品,每个产品只能有一个类别相关联。这种关系可以概况为:多个产品到一个分类(或者说,一个分类到多个产品)。
从Product
实体的视角来说,他是一个many-to-one映射。从Category
实体来说,他是一个 one-to-many 映射。这很重要,因为相对关系的性质决定使用哪个映射元数据。它也决定了哪些类,必须持有一个对其他类的引用。
关联Category
和Product
两个实体,首先创建一个Category
属性到一个products
类,注释,如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
// src/AppBundle/Entity/Product.php
// ...
class Product
{
// ...
/**
* @ORM\ManyToOne(targetEntity="Category", inversedBy="products")
* @ORM\JoinColumn(name="category_id", referencedColumnName="id")
*/
private $category;
} |
1 2 3 4 5 6 7 8 9 10 11 |
# src/AppBundle/Resources/config/doctrine/Product.orm.yml
AppBundle\Entity\Product:
type: entity
# ...
manyToOne:
category:
targetEntity: Category
inversedBy: products
joinColumn:
name: category_id
referencedColumnName: id |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
<!-- src/AppBundle/Resources/config/doctrine/Product.orm.xml -->
<?xml version="1.0" encoding="UTF-8" ?>
<doctrine-mapping xmlns="Http://doctrine-project.org/schemas/orm/doctrine-mapping"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://doctrine-project.org/schemas/orm/doctrine-mapping
http://doctrine-project.org/schemas/orm/doctrine-mapping.xsd">
<entity name="AppBundle\Entity\Product">
<!-- ... -->
<many-to-one
field="category"
target-entity="Category"
inversed-by="products"
join-column="category">
<join-column name="category_id" referenced-column-name="id" />
</many-to-one>
</entity>
</doctrine-mapping> |
这种many-to-one的映射关系很重要。他告诉Doctrine去使用product
表category_id
去关联category
表。
接下来,由于一个Category
对象将涉及到多个Product
对象,一个products
数组属性被添加到Category
类保存这些Product
对象。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
// src/AppBundle/Entity/Category.php
// ...
use Doctrine\Common\Collections\ArrayCollection;
class Category
{
// ...
/**
* @ORM\OneToMany(targetEntity="Product", mappedBy="category")
*/
private $products;
public function __construct()
{
$this->products = new ArrayCollection();
}
} |
1 2 3 4 5 6 7 8 9 10 |
# src/AppBundle/Resources/config/doctrine/Category.orm.yml
AppBundle\Entity\Category:
type: entity
# ...
oneToMany:
products:
targetEntity: Product
mappedBy: category
# Don't forget to initialize the collection in
# the __construct() method of the entity |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
<!-- src/AppBundle/Resources/config/doctrine/Category.orm.xml -->
<?xml version="1.0" encoding="UTF-8" ?>
<doctrine-mapping xmlns="http://doctrine-project.org/schemas/orm/doctrine-mapping"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://doctrine-project.org/schemas/orm/doctrine-mapping
http://doctrine-project.org/schemas/orm/doctrine-mapping.xsd">
<entity name="AppBundle\Entity\Category">
<!-- ... -->
<one-to-many
field="products"
target-entity="Product"
mapped-by="category" />
<!--
don't forget to init the collection in
the __construct() method of the entity
-->
</entity>
</doctrine-mapping> |
尽管前面的关系映射many-to-one是强制性的,但one-to-many映射是可选的。由于一个Category
对象将涉及到多个Product
对象,一个products
数组属性被添加到Category
类保存这些Product
对象。其次,这不是因为Doctrine需要它,而是因为在应用程序中为每一个Category
来保存一个Product
数组非常有用。
代码中构造方法非常重要。他不是一个传统的array
,这个$products
属性一定要去实现这种类型的Collection
接口。在这个案例中,我们使用ArrayCollection
,它跟数组非常类似,但会灵活一些。如果这让你感觉不舒服,不用担心。试想他是一个array
,你会欣然接受它。
理解inversedBy和mappedBy的用法,请看Doctrine's的Association Updates文档
上面注释所用的targetEntity 的值可以使用合法的命名空间引用任何实体,而不仅仅是定义在同一个类中的实体。 如果要关系一个定义在不同的类或者bundle中的实体则需要输入完全的命名空间作为目标实体(targetEntity)。
到现在为止,我们添加了两个新属性到Category
和Product
类。现在告诉Doctrine来为它们生成getter和setter方法。
1 |
$ php bin/console doctrine:generate:entities AppBundle |
我们先不看Doctrine的元数据,你现在有两个类Category
和Product
,并且拥有一个一对多的关系。该Category
类包含一个数组Product
对象,Product
包含一个Category
对象。换句话说,你已经创建了你所需要的类了。事实上把这些需要的数据持久化到数据库上是次要的。
现在,让我们来看看在Product
类中为$category
配置的元数据。它告诉Doctrine关系类是Category
并且它需要保存category
的id
到product
表的category_id
字段。
换句话说,相关的分类对象将会被保存到$category
属性中,但是在底层,Doctrine会通过存储category的id值到product
表的category_id
列持久化它们的关系。
Category
类中$product
属性的元数据配置不是特别重要,它仅仅是告诉Doctrine去查找Product.category
属性来计算出关系映射是什么。
在继续之前,一定要告诉Doctrine添加一个新的category
表和product.category_id
列以及新的外键。
$ php bin/console doctrine:schema:update --force
保存相关实体 ¶
现在让我们来看看控制器内的代码如何处理:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
// ...
use AppBundle\Entity\Category;
use AppBundle\Entity\Product;
use Symfony\Component\Httpfoundation\Response;
class DefaultController extends Controller
{
public function createProductAction()
{
$category = new Category();
$category->setName('Computer Peripherals');
$product = new Product();
$product->setName('Keyboard');
$product->setPrice(19.99);
$product->setDescription('Ergonomic and stylish!');
// relate this product to the category
$product->setCategory($category);
$em = $this->getDoctrine()->getManager();
$em->persist($category);
$em->persist($product);
$em->flush();
return new Response(
'Saved new product with id: '.$product->getId()
.' and new category with id: '.$category->getId()
);
}
} |
现在,一个单独的行被添加到category
和product
表中。新产品的product.categroy_id
列被设置为新category表中的id
的值。Doctrine会为你管理这些持久化关系。
获取相关对象 ¶
当你需要获取相关的对象时,你的工作流跟以前一样。首先获取$product
对象,然后访问它的相关Category
。
1 2 3 4 5 6 7 8 9 10 |
public function showAction($id)
{
$product = $this->getDoctrine()
->getRepository('AppBundle:Product')
->find($id);
$categoryName = $product->getCategory()->getName();
// ...
} |
在这个例子中,你首先基于产品id
查询一个Product
对象。他仅仅查询产品数据并把数据给$product
对象。接下来,当你调用$product->getCategory()->getName()
时,Doctrine默默的为你执行了第二次查询,查找一个与该Product
相关的category
,它生成一个$category
对象返回给你。
重要的是你很容易的访问到了product的相关category对象。但是category的数据并不会被取出来而直到你请求category的时候。这就是延迟加载。
你也可以从其它方向进行查询:
1 2 3 4 5 6 7 8 9 10 |
public function showProductsAction($id)
{
$category = $this->getDoctrine()
->getRepository('AppBundle:Category')
->find($id);
$products = $category->getProducts();
// ...
} |
在这种情况下,同样的事情发生了。你首先查查一个category
对象,然后Doctrine制造了第二次查询来获取与之相关联的所有Product
对象。只有在你调用->getProducts()
时才会执行一次。 $products
变量是一个通过它的category_id
的值跟给定的category
对象相关联的所有Product
对象的集合。
join相关记录 ¶
在之前的我们的查询中,会产生两次查询操作,一次是获取原对象(例如一个Category
),一次是获取关联对象(Product
)。
请记住,你可以通过网页调试工具查看请求的所有查询。
当然,如果你想一次访问两个对象,你可以通过一个join连接来避免二次查询。把下面的方法添加到ProductRepository
类中:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
// src/AppBundle/Entity/ProductRepository.php
public function findOneByIdJoinedToCategory($id)
{
$query = $this->getEntityManager()
->createQuery(
'SELECT p, c FROM AppBundle:Product p
JOIN p.category c
WHERE p.id = :id'
)->setParameter('id', $id);
try {
return $query->getSingleResult();
} catch (\Doctrine\ORM\NoResultException $e) {
return null;
}
} |
现在你就可以在你的控制器中一次性查询一个产品对象和它关联的category
对象信息了。
1 2 3 4 5 6 7 8 9 10 |
public function showAction($id)
{
$product = $this->getDoctrine()
->getRepository('AppBundle:Product')
->findOneByIdJoinedToCategory($id);
$category = $product->getCategory();
// ...
} |
更多关联信息 ¶
本节中已经介绍了一个普通的实体关联,一对多关系。对于更高级的关联和如何使用其他的关联(例如 一对一,多对一),请参见 doctrine 的Association Mapping文档.
如果你使用注释,你需要预先在所有注释加ORM\
(如ORM\OneToMany
),这些在doctrine官方文档里没有。你还需要声明use Doctrine\ORM\Mapping as ORM;
才能使用annotations的ORM
。