如何处理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。