> Symfony中文手册 > 如何处理Doctrine Associations / Relations

如何处理Doctrine Associations / Relations

假设你应用程序中的产品属于一确定的分类。这时你需要一个Category类对象并一种方法把ProductCategory对象联系在一起。

首先我们创建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 映射。这很重要,因为相对关系的性质决定使用哪个映射元数据。它也决定了哪些类,必须持有一个对其他类的引用。

关联CategoryProduct两个实体,首先创建一个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去使用productcategory_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)。

到现在为止,我们添加了两个新属性到CategoryProduct类。现在告诉Doctrine来为它们生成getter和setter方法。

1
$ php bin/console doctrine:generate:entities AppBundle

我们先不看Doctrine的元数据,你现在有两个类CategoryProduct,并且拥有一个一对多的关系。该Category类包含一个数组Product对象,Product包含一个Category对象。换句话说,你已经创建了你所需要的类了。事实上把这些需要的数据持久化到数据库上是次要的。

现在,让我们来看看在Product类中为$category配置的元数据。它告诉Doctrine关系类是Category并且它需要保存categoryidproduct表的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()
        );
    }
}

现在,一个单独的行被添加到categoryproduct表中。新产品的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对象的集合。

关系和代理类

“延迟加载”成为可能,是因为Doctrine返回一个代理对象来代替真正的对象:

1
2
3
4
5
6
7
8
9
$product = $this->getDoctrine()
    ->getRepository('AppBundle:Product')
    ->find($productId);
 
$category = $product->getCategory();
 
// prints "Proxies\AppBundleEntityCategoryProxy"
dump(get_class($category));
die();

该代理对象继承了Category对象,从外表到行为都非常像category对象。所不同的是,通过这个代理对象,Doctrine可以延迟查询真正的Category对象数据,直到真正需要它时(调用$category->getName())。

Doctrine生成了代理对象并把它存储到cache目录中,尽管你可能从来没有发现过它。记住它这一点很重要。

我们可以通过join连接来一次性取出product和category数据。这时Doctrine将会返回真正的Category对象,因为不需要延迟加载。

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