对于任何应用程序来说最为普遍最具挑战性的任务,就是从数据库中读取和持久化数据信息。尽管symfony完整的框架没有默认集成ORM,但是symfony标准版,集成了很多程序,还自带集成了Doctrine这样一个库,主要的目的是给开发者一个强大的工具,让你工作起来更加容易。在本章,你会学会doctrine的基本理念并且能够了解如何轻松使用数据库。
Doctrine可以完全脱离symfony使用,并且在symfony中是否使用也是可选的。本章主要了解Doctrine的ORM,其目的是让你的对象映射到数据库中(如MySQL, PostgreSQL和Microsoft SQL)。如果你喜欢使用原始的数据库查询,这很容易,可以了解cookbook 中的”How to Use Doctrine DBAL“。
你也可以使用Doctrine ODM库将数据持久化到MongoDB。更多信息请参阅”DoctrineMongoDBBundle“。
一个简单的例子:一个产品
了解Doctrine是如何工作的最简单的方式就是看一个实际的应用。在本章,你需要配置你的数据库,创建一个Product对象,持久化它到数据库并且把它抓取回来。
配置数据库
在你真正开始之前,你需要配置你的数据库链接信息。按照惯例,这些信息通常配置在app/config/parameters.yml文件中:
1 2 3 4 5 6 7 8 9 |
# app/config/parameters.yml parameters: database_driver: pdo_mysql database_host: localhost database_name: test_project database_user: root database_password: password # ... |
将配置信息定义到parameters.yml仅仅是一个惯例。定义在该文件中的配置信息将会被主配置文件在安装Doctrine时引用。
12345678 # app/config/config.ymldoctrine:dbal:driver: "%database_driver%"host: "%database_host%"dbname: "%database_name%"user: "%database_user%"password: "%database_password%"通过把数据库信息分离到一个特定的文件中,你可以很容易的为每个服务器保存不同的版本。你也可以在项目外轻松存储数据库配置(一些敏感信息),就像apache配置一样。更多信息请参阅How to Set external Parameters in the Service Container.
现在Doctrine知道你的数据库配置了,你可以用它来创建一个数据库了。
1 |
$ php app/console doctrine:database:create |
设置数据库为UTF8
即便对于经验丰富的程序员来说,一个常犯的错误是,在Symfony项目开始后,忘记设置他们的数据库默认字符集和校对规则,仅把大部分数据库给出的latin类型的校对作为默认。他们也许在第一次操作时会记得,但到了后面敲打两行相关的常规命令之后,就完全忘掉了。
12 $ php app/console doctrine:database:drop --force$ php app/console doctrine:database:create在Doctrine里直接指派默认字符集是不可能的,因为doctrine会根据环境配置,尽可能多地去适应各种“不可知”情形。解决办法之一,是去配置“服务器级别”的默认信息。
设置UTF8为MySql的默认字符集是非常简单的,只要在数据库配置文件中加几行代码就可以了(一般是my.cnf文件)
1234 [mysqld]# Version 5.5.3 introduced "utf8mb4", which is recommendedcollation-server = utf8mb4_general_ci # Replaces utf8_general_cicharacter-set-server = utf8mb4 # Replaces utf8我们推荐避免使用Mysql的uft8字符集,因为它并不兼容4-byte unicode字符,如果字符串中有这种字符会被清空。不过这种情况被修复了,参考《新型utf8mb4字符集》
如果你想要使用SQLite作为数据库,你需要设置path为你的数据库路径
123456 # app/config/config.ymldoctrine:dbal:driver: pdo_sqlitepath: "%kernel.root_dir%/sqlite.db"charset: UTF8
创建一个实体类
假设你创建一个应用程序,其中有些产品需要展示。即时不考虑Doctrine或者数据库,你也应该知道你需要一个Product对象来表现这些产品。在你的AppBundle的Entity目录下创建一个类。
1 2 3 4 5 6 7 8 9 |
// src/AppBundle/Entity/Product.php namespace AppBundle\Entity; class Product { protected $name; protected $price; protected $description; } |
这样的类经常被称为“Entity”,意味着一个基础类保存数据。它们简单来满足你应用程序的业务需要。不过现在它还不能被保存到数据库中,因为现在它只不过还是个简单的PHP类。
一旦你学习了Doctrine背后的概念,你可以让Doctrine来为你创建实体类。他会问你一些问题来创建entity:
1 |
$ php app/console doctrine:generate:entity |
添加映射信息
Doctrine允许你使用一种更加有趣的方式对数据库进行操作,而不是只是获取基于列表的行到数组中。Doctrine允许你保存整个对象到数据库或者把对象从数据库中取出。这些都是通过映射PHP类到一个数据库表,PHP类的属性对应数据库表的列来实现的。
因为Doctrine能够做这些,所以你仅仅只需要创建一个meatdata,或者配置告诉Doctrine的Product类和它的属性应该如何映射到数据库。这些metadata可以被定义成各种格式,包括YAML,XML或者通过声明直接定义到Product类中。
annotations:
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 33 |
// src/AppBundle/Entity/Product.php namespace AppBundle\Entity; use Doctrine\ORM\Mapping as ORM; /** * @ORM\Entity * @ORM\Table(name="product") */ class Product { /** * @ORM\Column(type="integer") * @ORM\Id * @ORM\GeneratedValue(strategy="AUTO") */ protected $id; /** * @ORM\Column(type="string", length=100) */ protected $name; /** * @ORM\Column(type="decimal", scale=2) */ protected $price; /** * @ORM\Column(type="text") */ protected $description; } |
一个bundle只可以接受一种metadata定义格式。比如,不能把YAML定义的metadata和声明PHP实体类一起混用。
表名是可选的,如果省略,将基于entity类的名称自动确定。
Doctrine允许你去选择各种不同的字段类型,每个字段都有自己的配置。有关字段类型信息,请看Doctrine Field Types Reference。
你也可以查看Doctrine官方文档Basic Mapping Documentation关于映射信息的所有细节。如果你使用annotations,你需要所有的注释都有ORM(例如 ORM\Column()),这些doctrine模板并没有。你还需要去引入use Doctrine\ORM\Mapping as ORM;声明,它是用来引进ORM注册前缀的。
小心你的类名和属性很可能就被映射到一个受保护的SQL字段(如group和user)。举例,如果你的entity类名称为Group,那么,在默认情况下,你的表名为group,在一些引擎中可能导致SQL错误。请查看 Reserved SQL keywords documentation,他会告诉你如何正确的规避这些名称。另外,你可以自由简单的映射到不同的表名和字段名,来选择你的数据库纲要。请查看Doctrine的Persistent classes和Property Mapping文档。
当使用其他的库或者程序(例如 Doxygen)它们使用了注释,你应该把@IgnoreAnnotation注释添加到该类上来告诉Symfony忽略它们。
比如我们要阻止@fn 声明抛出异常,可以这样:
12345 /*** @IgnoreAnnotation("fn")*/class Product// ...
生产Getters和Setters
尽管Doctrine现在知道了如何持久化Product对象到数据库,但是类本身是不是有用呢。因为Product仅仅是一个标准的PHP类,你需要创建getter和setter方法(比如getName(),setName())来访问它的属性(因为它的属性是protected),幸运的是Doctrine可以为我们做这些:
1 |
$ php app/console doctrine:generate:entities AppBundle/Entity/Product |
该命令可以确保Product类所有的getter和setter都被生成。这是一个安全的命令行,你可以多次运行它,它只会生成那些不存在的getters和setters,而不会替换已有的。
请记住doctrine entity引擎生产简单的getters/setters。你应该检查生成的实体,调整getter/setter逻辑为自己想要的。
关于doctrine:generate:entities命令
用它你可以生成getters和setters。
用它在配置@ORM\Entity(repositoryClass=”…”)声明的情况下,生成repository类。
用它可以为1:n或者n:m生成合适的构造器。
该命令会保存一个原来Product.php文件的备份Product.php~。 有些时候可也能够会造成“不能重新声明类”错误,你可以放心的删除它,来消除错误。您还可以使用–no-backup选项,来防止产生这些配置文件。
当然你没有必要依赖于该命令行,Doctrine不依赖于代码生成,像标准的PHP类,你只需要保证它的protected/private属性拥有getter和setter方法即可。主要由于用命令行去创建是,一种常见事。
你也可以为一个bundle或者整个实体命名空间内的所有已知实体(任何包含Doctrine映射声明的PHP类)来生成getter和setter:
1 2 3 4 5 |
# generates all entities in the AppBundle $ php app/console doctrine:generate:entities AppBundle # generates all entities of bundles in the Acme namespace $ php app/console doctrine:generate:entities Acme |
Doctrine不关心你的属性是protected还是private,或者这些属性是否有getter或setter。之所以生成这些getter或者setter完全是因为你需要跟你的PHP对象进行交流需要它们。
创建数据库表和模式
现在我们有了一个可用的Product类和它的映射信息,所以Doctrine知道如何持久化它。当然,现在Product还没有相应的product数据库表在数据库中。幸运的是,Doctrine可以自动创建所有的数据库表。
1 |
$ php app/console doctrine:schema:update --force |
说真的,这条命令是出奇的强大。它会基于你的entities的映射信息,来比较现在的数据库,并生成所需要的新数据库的更新SQl语句。换句话说,如果你想添加一个新的属性映射元数据到Product并运行该任务,它将生成一个alert table 语句来添加新的列到已经存在的product表中。
一个更好的发挥这一优势的功能是通过migrations,它允许你生成这些SQL语句。并存储到一个迁移类,并能有组织的运行在你的生产环境中,系统为了安全可靠地跟踪和迁移数据库。
现在你的数据库中有了一个全功能的product表,它的每个列都会被映射到你指定的元数据。
持久化对象到数据库
现在我们有了一个Product实体和与之映射的product数据库表。你可以把数据持久化到数据库里。在Controller内,它非常简单。添加下面的方法到bundle的DefaultController中。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
// src/AppBundle/Controller/DefaultController.php // ... use AppBundle\Entity\Product; use Symfony\Component\HttpFoundation\Response; // ... public function createAction() { $product = new Product(); $product->setName('A Foo Bar'); $product->setPrice('19.99'); $product->setDescription('Lorem ipsum dolor'); $em = $this->getDoctrine()->getManager(); $em->persist($product); $em->flush(); return new Response('Created product id '.$product->getId()); } |
如果你想演示这个案例,你需要去创建一个路由指向这个action,让他工作。
本文展示了在控制器中使用Doctrine的getDoctrine()方法。这个方法是获取doctrine服务最便捷的方式。你能在服务中的任何其他地方使用doctrine注入该服务。更多关于常见自己的服务信息,请参阅Service Container。
在看看前面例子的详情:
在本节10-13行,你实例化$product对象,就像其他任何普通的php对象一样。
15行获取doctrine实体管理对象,这是负责处理数据库持久化过程和读取对象的。
16行persist()方法告诉Doctrine去“管理”这个$product对象。还没有在数据库中使用过语句。
17行党这个flush()方法被调用,Doctrine会查看它管理的所有对象,是否需要被持久化到数据库。在本例子中,这个$product对象还没有持久化,所以这个entity管理就会执行一个insert语句并且会在product表中创建一行数据。
事实上,Doctrine了解你所有的被管理的实体,当你调用flush()方法时,它会计算出所有的变化,并执行最有效的查询可能。 他利用准备好的缓存略微提高性能。比如,你要持久化总是为100的产品对象,然后调用flush()方法。Doctrine会创建一个唯一的预备语句并重复使用它插入。
在创建和更新对象时,工作流是相同的。在下一节中,如果记录已经存在数据库中,您将看到Doctrine如何聪明的自动发出一个Update语句。
Doctrine提供了一个类库允许你通过编程,加载测试数据到你的项目。该类库为DoctrineFixturesBundle(http://symfony.com/doc/current/bundles/DoctrineFixturesBundle/index.html)
从数据库中获取对象
从数据库中获取对象更容易,举个例子,假如你配置了一个路由来,用它的ID显示特定的product。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
public function showAction($id) { $product = $this->getDoctrine() ->getRepository('AppBundle:Product') ->find($id); if (!$product) { throw $this->createNotFoundException( 'No product found for id '.$id ); } // ... do something, like pass the $product object into a template } |
你可以使用@ParamConverter注释不用编写任何代码就可以实现同样的功能。更多信息请查看FrameworkExtraBundle文档。
当你查询某个特定的产品时,你总是需要使用它的”respository”。你可以认为Respository是一个PHP类,它的唯一工作就是帮助你从某个特定类哪里获取实体。你可以为一个实体对象访问一个repository对象,如下:
1 2 |
$repository = $this->getDoctrine() ->getRepository('AppBundle:Product'); |
其中appBundle:Product是简洁写法,你可以在Doctrine中任意使用它来替代实体类的全限定名称(例如AppBundle\Entity\Product)。只要你的entity在你的bundle的Entity命名空间下它就会工作。
你一旦有了Repository,你就可以访问其所有分类的帮助方法了。
1 2 3 4 5 6 7 8 9 10 11 12 |
// query by the primary key (usually "id") $product = $repository->find($id); // dynamic method names to find based on a column value $product = $repository->findOneById($id); $product = $repository->findOneByName('foo'); // find *all* products $products = $repository->findAll(); // find a group of products based on an arbitrary column value $products = $repository->findByPrice(19.99); |
当然,你也可以使用复杂的查询,想了解更多请阅读Querying for Objects 。
你也可以有效利用findBy和findOneBy方法的优势,很容易的基于多个条件来获取对象。
1 2 3 4 5 6 7 8 9 10 |
// query for one product matching by name and price $product = $repository->findOneBy( array('name' => 'foo', 'price' => 19.99) ); // query for all products matching the name, ordered by price $products = $repository->findBy( array('name' => 'foo'), array('price' => 'ASC') ); |
当你去渲染页面,你可以在网页调试工具的右下角看到许多的查询。
如果你单机该图标,分析页面将打开,显示你的精确查询。
如果你的页面查询超过了50个它会变成黄色。这可能表明你的程序有问题。
更新对象
一旦你从Doctrine中获取了一个对象,那么更新它就变得很容易了。假设你有一个路由映射一个产品id到一个controller的updateaction。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
public function updateAction($id) { $em = $this->getDoctrine()->getManager(); $product = $em->getRepository('AppBundle:Product')->find($id); if (!$product) { throw $this->createNotFoundException( 'No product found for id '.$id ); } $product->setName('New product name!'); $em->flush(); return $this->redirectToRoute('homepage'); } |
更新一个对象包括三步:
1.从Doctrine取出对象
2.修改对象
3.在实体管理者上调用flush()方法
注意调用 $em->persist($product) 在这里没有必要。我们回想一下,调用该方法的目的主要是告诉Doctrine来管理或者“观察”$product对象。在这里,因为你已经取到了$product对象了,说明已经被管理了。
删除对象
删除一个对象,需要从实体管理者那里调用remove()方法。
1 2 |
$em->remove($product); $em->flush(); |
正如你想的那样,remove()方法告诉Doctrine你想从数据库中移除指定的实体。真正的删除查询没有被真正的执行,直到flush()方法被调用。
查询对象
你已经看到了repository对象允许你执行一些基本的查询而不需要你做任何的工作。
1 2 3 |
$repository->find($id); $repository->findOneByName('Foo'); |
当然,Doctrine 也允许你使用Doctrine Query Language(DQL)写一些复杂的查询,DQL类似于SQL,只是它用于查询一个或者多个实体类的对象,而SQL则是查询一个数据库表中的行。
在Doctrinez中查询时,你有两种选择:写纯Doctrine查询 或者 使用Doctrine的查询创建器。
使用Doctrine’s Query Builder查询对象
假设你想查询产品,需要返回价格高于19.99的产品,并且要求按价格从低到高排列。你可以使用Doctrine的QueryBuilder:
1 2 3 4 5 6 7 8 9 10 |
$repository = $this->getDoctrine() ->getRepository('AppBundle:Product'); $query = $repository->createQueryBuilder('p') ->where('p.price > :price') ->setParameter('price', '19.99') ->orderBy('p.price', 'ASC') ->getQuery(); $products = $query->getResult(); |
QueryBuilder对象包含了创建查询的所有必须的方法。通过调用getQuery()方法,查询创建器将返回一个标准的Query对象。它跟我们直接写查询对象效果相同。
记住setParameter()方法。当Doctrine工作时,外部的值,会通过“占位符”(上面例子的:price)传入,来防止SQL注入攻击。
该getResult()方法返回一个结果数组。想要得到一个结果,你可以使用getSingleResult()(这个方法在没有结果时会抛出一个异常)或者getOneOrNullResult():
1 |
$product = $query->getOneOrNullResult(); |
更多Doctrine’s Query Builder的信息请阅读Query Builder。
使用DQL查询对象
不爱使用QueryBuilder,你还可以直接使用DQL查询:
1 2 3 4 5 6 7 8 9 |
$em = $this->getDoctrine()->getManager(); $query = $em->createQuery( 'SELECT p FROM AppBundle:Product p WHERE p.price > :price ORDER BY p.price ASC' )->setParameter('price', '19.99'); $products = $query->getResult(); |
如果你习惯了写SQL,那么对于DQL也应该不会感到陌生。它们之间最大的不同就是你需要思考对象,而不是数据库表行。正因为如此,所以你从AppBundle:Product选择并给它定义别名p。(你看和上面完成的结果一样)。
该DQL语法强大到令人难以置信,允许您轻松地在之间加入实体(稍后会介绍关系)、组等。更多信息请参阅Doctrine Query Language文档。
自定义Repository类
在上面你已经开始在controller中创建和使用负责的查询了。为了隔离,比阿育测试和重用这些查询,一个好的办法是为你的实体创建一个自定义的repository类并添加相关逻辑查询方法。
要定义repository类,首先需要在你的映射定义中添加repository类的声明:
1 2 3 4 5 6 7 8 9 10 11 12 |
// src/AppBundle/Entity/Product.php namespace AppBundle\Entity; use Doctrine\ORM\Mapping as ORM; /** * @ORM\Entity(repositoryClass="AppBundle\Entity\ProductRepository") */ class Product { //... } |
然后通过运行跟之前生成丢失的getter和setter方法同样的命令行,Doctrine会为你自动生成repository类。
1 |
$ php app/console doctrine:generate:entities AppBundle |
下面,添加一个新方法findAllOrderedByName() 到新生成的repository类。该方法将查询所有的Product实体,并按照字符顺序排序。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
// src/AppBundle/Entity/ProductRepository.php namespace AppBundle\Entity; use Doctrine\ORM\EntityRepository; class ProductRepository extends EntityRepository { public function findAllOrderedByName() { return $this->getEntityManager() ->createQuery( 'SELECT p FROM AppBundle:Product p ORDER BY p.name ASC' ) ->getResult(); } } |
在Repository类中可以通过$this->getEntityManager()方法类获取entity管理。
你就可以像使用默认的方法一样使用这个新定义的方法了:
1 2 3 |
$em = $this->getDoctrine()->getManager(); $products = $em->getRepository('AppBundle:Product') ->findAllOrderedByName(); |
当使用一个自定义的repository类时,你依然可以访问原有的默认查找方法,比如find() 和findAll()等。
实体的关系/关联
假设你应用程序中的产品属于一确定的分类。这时你需要一个分类对象和一种把Product和Category对象联系在一起的方式。首先我们创建Category实体,我们最终要通过Doctrine来对其进行持久化,所以我们这里让Doctrine来帮我们创建这个类。
1 2 3 |
$ php app/console doctrine:generate:entity \ --entity="AppBundle:Category" \ --fields="name:string(255)" |
该命令行为你生成一个Category实体,包含id字段和name字段以及相关的getter和setter方法。
关系映射
关联Category和Product两个实体,首先在Category类中创建一个products属性:
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") */ protected $products; public function __construct() { $this->products = new ArrayCollection(); } } |
首先,由于一个Category对象将涉及到多个Product对象,一个products数组属性被添加到Category类保存这些Product对象。其次,这不是因为Doctrine需要它,而是因为在应用程序中为每一个Category来保存一个Product数组非常有用。
代码中__construct()方法非常重要,因为Doctrine需要$products属性成为一个ArrayCollection对象,它跟数组非常类似,但会灵活一些。如果这让你感觉不舒服,不用担心。试想他是一个数组,你会欣然接受它。
上面注释所用的targetEntity 的值可以使用合法的命名空间引用任何实体,而不仅仅是定义在同一个类中的实体。 如果要关系一个定义在不同的类或者bundle中的实体则需要输入完全的命名空间作为目标实体。
接下来,因为每个Product类可以关联一个Category对象,所有添加一个$category属性到Product类:
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") */ protected $category; } |
到现在为止,我们添加了两个新属性到Category和Product类。现在告诉Doctrine来为它们生成getter和setter方法。
1 |
$ php app/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列以及新的外键。
1 |
$ php app/console doctrine:schema:update --force |
这个任务只能在开发期间使用。有一种更健壮的方法来有条理的更新你的生产数据库,请查看migrations.
保存相关实体
现在让我们来看看Controller内的代码如何处理:
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 |
// ... 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('Main Products'); $product = new Product(); $product->setName('Foo'); $product->setPrice(19.99); $product->setDescription('Lorem ipsum dolor'); // relate this product to the category $product->setCategory($category); $em = $this->getDoctrine()->getManager(); $em->persist($category); $em->persist($product); $em->flush(); return new Response( 'Created product id: '.$product->getId() .' and category 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默默的为你执行了第二次查询,查找一个与该产品相关的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返回一个代理对象来代替真正的对象:
12345678 $product = $this->getDoctrine()->getRepository('AppBundle:Product')->find($id);$category = $product->getCategory();// prints "Proxies\AppBundleEntityCategoryProxy"echo get_class($category);该代理对象继承了Category对象,从外表到行为都非常像category对象。所不同的是,通过这个代理对象,Doctrine可以延迟查询真正的Category对象数据,直到真正需要它时(调用$category->getName())。
Doctrine生成了代理对象并把它存储到cache目录中,尽管你可能从来没有发现过它。记住它这一点很重要。
我们可以通过join连接来一次性取出product和category数据。这时Doctrine将会返回真正的Category对象,因为不需要延迟加载。
join相关记录
在之前的我们的查询中,会产生两次查询操作,一次是获取原对象,一次是获取关联对象。
请记住,你可以通过网页调试工具查看请求的所有查询。
当然,如果你想一次访问两个对象,你可以通过一个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; } } |
现在你就可以在你的controller中一次性查询一个产品对象和它关联的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 Documentation.
如果你使用注释,你需要预先在所有注释加ORM\(如ORM\OneToMany),这些在doctrine官方文档里没有。你还需要声明use Doctrine\ORM\Mapping as ORM;才能使用annotations的ORM。
配置
Doctrine是高度可配置的,但是你可能永远不用关心他们。要想了解更多关于Doctrine的配置信息,请查看config reference。
生命周期回调
有时候你可能需要在一个实体被创建,更新或者删除的前后执行一些操作。这些操作方法处在一个实体不同的生命周期阶段,所以这些行为被称为”生命周期回调“。
如果你用annotations方式,开启一个生命周期回调,需要如下设置:(如果你不喜欢你也可以使用yaml和xml方式)
1 2 3 4 5 6 7 8 |
/** * @ORM\Entity() * @ORM\HasLifecycleCallbacks() */ class Product { // ... } |
现在你可以告诉Doctrine在任何可用的生命周期事件上来执行一个方法了。比如,假设你想在一个新的实体第一次被创建时设置创建日期列(created)为当前日期。
1 2 3 4 5 6 7 8 9 |
// src/AppBundle/Entity/Product.php /** * @ORM\PrePersist */ public function setCreatedAtValue() { $this->createdAt = new \DateTime(); } |
上面的例子假设你已经创建了createdAt属性(为在此处显示)。
现在在实体第一次被保存时,Doctrine会自动调用这个方法使created日期自动设置为当前日期。
还有一些其他的生命周期事件,你可以使用它。更多生命周期事件和生命周期回调,请查看Doctrine的Lifecycle Events documentation。
生命周期回调和事件监听
注意到setCreatedValue()方法不需要接收任何参数。这是生命周期回调通常的做法和惯例:生命周期回调应该是简单方法,更关注于实体内部传输数据。比如设置一个创建/更新字段,生成一个定量值等。
如果你需要一些比较大的行为活动,像执行日志或者发送邮件,你应该注册一个扩展类作为事件监听器或接收器给它赋予访问所需资源的权利。想了解更多,请参阅How to Register Event Listeners and Subscribers.
Doctrine字段类型参考
Doctrine配备了大量可用的字段类型。它们每一个都能映射PHP数据类型到特定的列类型,无论你使用什么数据库。对于每一个字段类型,Column都可以被进一步配置,可以设置length
, nullable行为,name或者其他配置。想查看更多信息请参阅Doctrine的Mapping Types documentation。
总结
有了Doctrine,你可以集中精力到你的对象以及怎样把它应用于你的应用程序中,而不必担心数据库持久化。因为Doctrine允许你使用任何的PHP对象保存你的数据并依靠映射元数据信息来联系一个对象到特定的数据库表。
尽管Doctrine围绕着一个简单的概念发展而来,但是它不可思议的强大。允许你创建复杂的查询和订阅事件,通过订阅事件你可以在整个持久化过程中执行一些不同的行为。