Home > Article > Backend Development > Database and Doctrine (reprinted from http://www.111cn.net/phper/332/85987.htm), www.111cn.netphper_PHP tutorial
For any application The most common and challenging task is to read and persist data information from the database. Although the complete symfony framework does not integrate ORM by default, the symfony standard version integrates many programs and also integrates a library like Doctrine. The main purpose is to give developers a powerful tool to make your work easier. In this chapter, you will learn the basic concepts of doctrine and see how to use a database easily.
Doctrine can be used completely without symfony, and whether to use it in symfony is also optional. This chapter mainly understands Doctrine's ORM, whose purpose is to map your objects to databases (such as MySQL, PostgreSQL and Microsoft SQL). If you prefer to use raw database queries, it's easy, check out the "How to Use Doctrine DBAL" cookbook.
You can also use the Doctrine ODM library to persist data to MongoDB. See "DoctrineMongoDBBundle" for more information.
A simple example: a product
The easiest way to understand how Doctrine works is to see a practical application. In this chapter, you need to configure your database, create a Product object, persist it to the database and retrieve it back.
Configuration Database
Before you actually get started, you need to configure your database link information. By convention, this information is usually configured in the app/config/parameters.yml file:
<span>#</span><span> app/config/parameters.yml</span> parameters:<span> database_driver</span>:<span> pdo_mysql database_host</span>:<span> localhost database_name</span>:<span> test_project database_user</span>:<span> root database_password</span>:<span> password </span><span>#</span><span> ...</span>
Defining configuration information into parameters.yml is just a convention. The configuration information defined in this file will be referenced by the main configuration file when installing Doctrine.
<span>#</span><span> app/config/config.yml</span> doctrine:<span> dbal</span>:<span> driver</span>: "%database_driver%"<span> host</span>: "%database_host%"<span> dbname</span>: "%database_name%"<span> user</span>: "%database_user%"<span> password</span>: "%database_password%"
By separating the database information into a specific file, you can easily save different versions for each server. You can also easily store database configuration (some sensitive information) outside the project, just like apache configuration. For more information, see How to Set external Parameters in the Service Container.
Now that Doctrine knows your database configuration, you can use it to create a database.
$ php app/console doctrine:database:create
Set the database to UTF8
Even for experienced programmers, a common mistake is to forget to set their database default character set and collation rules after starting a Symfony project, and only use the latin type collation given by most databases as the default. They may remember it the first time they operate it, but after typing two lines of related regular commands later, they completely forget it.
$ php app/console doctrine:database:drop --force
$ php app/console doctrine:database:create
It is impossible to directly assign a default character set in Doctrine, because doctrine will adapt to as many "unknown" situations as possible according to the environment configuration. One solution is to configure "server-level" default information.
Setting UTF8 as the default character set for MySql is very simple. Just add a few lines of code to the database configuration file (usually the my.cnf file)
[mysqld]
# Version 5.5.3 introduced "utf8mb4", which is recommended
collation-server = utf8mb4_general_ci # Replaces utf8_general_ci
character-set-server = utf8mb4 # Replaces utf8
We recommend avoiding using MySQL's uft8 character set because it is not compatible with 4-byte unicode characters and will be cleared if there are such characters in the string. However, this situation has been fixed, please refer to "New utf8mb4 Character Set"
---------------------------------------------Create a new database This section is not as convenient as operating it directly in mysql------------------------------------------ --------
If you want to use SQLite as the database, you need to set path to your database path
<span>#</span><span> app/config/config.yml</span> doctrine:<span> dbal</span>:<span> driver</span>:<span> pdo_sqlite path</span>: "%kernel.root_dir%/sqlite.db"<span> charset</span>:<span> UTF8 </span>
Create an entity class
Suppose you create an application in which some products need to be displayed. Even regardless of Doctrine or the database, you should know that you need a Product object to represent these products. Create a class in the Entity directory of your AppBundle.
<span>//</span><span> src/AppBundle/Entity/Product.php</span> <span>namespace AppBundle\Entity; </span><span>class</span><span> Product { </span><span>protected</span> <span>$name</span><span>; </span><span>protected</span> <span>$price</span><span>; </span><span>protected</span> <span>$description</span><span>; }</span>
Such a class is often called an "Entity", meaning a base class that holds data. They are simple to meet the business needs of your application. But now it cannot be saved to the database, because now it is just a simple PHP class.
Once you learn the concepts behind Doctrine, you can let Doctrine create entity classes for you. He will ask you some questions to create the entity:
$ php app/console doctrine:generate:entity
Add mapping information
Doctrine allows you to operate on the database in a more interesting way than just getting list-based rows into an array. Doctrine allows you to save entire objects to the database or retrieve objects from the database. These are achieved by mapping PHP classes to a database table, and the attributes of the PHP class correspond to the columns of the database table.
因为Doctrine能够做这些,所以你仅仅只需要创建一个meatdata,或者配置告诉Doctrine的Product类和它的属性应该如何映射到数据库。这些metadata可以被定义成各种格式,包括YAML,XML或者通过声明直接定义到Product类中。
<span>annotations: </span><span>//</span><span> src/AppBundle/Entity/Product.php</span> <span>namespace AppBundle\Entity; </span><span>use</span> Doctrine\ORM\Mapping <span>as</span><span> ORM; </span><span>/*</span><span>* * @ORM\Entity * @ORM\Table(name="product") </span><span>*/</span> <span>class</span><span> Product { </span><span>/*</span><span>* * @ORM\Column(type="integer") * @ORM\Id * @ORM\GeneratedValue(strategy="AUTO") </span><span>*/</span> <span>protected</span> <span>$id</span><span>; </span><span>/*</span><span>* * @ORM\Column(type="string", length=100) </span><span>*/</span> <span>protected</span> <span>$name</span><span>; </span><span>/*</span><span>* * @ORM\Column(type="decimal", scale=2) </span><span>*/</span> <span>protected</span> <span>$price</span><span>; </span><span>/*</span><span>* * @ORM\Column(type="text") </span><span>*/</span> <span>protected</span> <span>$description</span><span>; }</span>
一个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 声明抛出异常,可以这样:
<span>/*</span><span>* * @IgnoreAnnotation("fn") </span><span>*/</span> <span>class</span><span> Product </span><span>//</span><span> ...</span>
生产Getters和Setters
尽管Doctrine现在知道了如何持久化Product对象到数据库,但是类本身是不是有用呢。因为Product仅仅是一个标准的PHP类,你需要创
建getter和setter方法(比如getName(),setName())来访问它的属性(因为它的属性是protected),幸运的是
Doctrine可以为我们做这些:
$ 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:
<span>#</span><span> generates all entities in the AppBundle</span> $ php app/console doctrine:generate:<span>entities AppBundle </span><span>#</span><span> generates all entities of bundles in the Acme namespace</span> $ php app/console doctrine:generate:entities Acme
Doctrine不关心你的属性是protected还是private,或者这些属性是否有getter或setter。之所以生成这些getter或者setter完全是因为你需要跟你的PHP对象进行交流需要它们。
创建数据库表和模式
现在我们有了一个可用的Product类和它的映射信息,所以Doctrine知道如何持久化它。当然,现在Product还没有相应的product数据库表在数据库中。幸运的是,Doctrine可以自动创建所有的数据库表。
$ php app/console doctrine:schema:update --force
说真的,这条命令是出奇的强大。它会基于你的entities的映射信息,来比较现在的数据库,并生成所需要的新数据库的更新SQl语句。换句话说,如 果你想添加一个新的属性映射元数据到Product并运行该任务,它将生成一个alert table 语句来添加新的列到已经存在的product表中。
一个更好的发挥这一优势的功能是通过migrations,它允许你生成这些SQL语句。并存储到一个迁移类,并能有组织的运行在你的生产环境中,系统为了安全可靠地跟踪和迁移数据库。
现在你的数据库中有了一个全功能的product表,它的每个列都会被映射到你指定的元数据。
持久化对象到数据库
现在我们有了一个Product实体和与之映射的product数据库表。你可以把数据持久化到数据库里。在Controller内,它非常简单。添加下面的方法到bundle的DefaultController中。
<span>//</span><span> src/AppBundle/Controller/DefaultController.php // ...</span> <span>use</span><span> AppBundle\Entity\Product; </span><span>use</span><span> Symfony\Component\HttpFoundation\Response; </span><span>//</span><span> ...</span> <span>public</span> <span>function</span><span> createAction() { </span><span>$product</span> = <span>new</span><span> Product(); </span><span>$product</span>->setName('A Foo Bar'<span>); </span><span>$product</span>->setPrice('19.99'<span>); </span><span>$product</span>->setDescription('Lorem ipsum dolor'<span>); </span><span>$em</span> = <span>$this</span>->getDoctrine()-><span>getManager(); </span><span>$em</span>->persist(<span>$product</span><span>); </span><span>$em</span>-><span>flush</span><span>(); </span><span>return</span> <span>new</span> Response('Created product id '.<span>$product</span>-><span>getId()); }</span>
如果你想演示这个案例,你需要去创建一个路由指向这个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。
<span>public</span> <span>function</span> showAction(<span>$id</span><span>) { </span><span>$product</span> = <span>$this</span>-><span>getDoctrine() </span>->getRepository('AppBundle:Product'<span>) </span>->find(<span>$id</span><span>); </span><span>if</span> (!<span>$product</span><span>) { </span><span>throw</span> <span>$this</span>-><span>createNotFoundException( </span>'No product found for id '.<span>$id</span><span> ); } </span><span>//</span><span> ... do something, like pass the $product object into a template</span> }
你可以使用@ParamConverter注释不用编写任何代码就可以实现同样的功能。更多信息请查看FrameworkExtraBundle文档。
当你查询某个特定的产品时,你总是需要使用它的”respository”。你可以认为Respository是一个PHP类,它的唯一工作就是帮助你从某个特定类哪里获取实体。你可以为一个实体对象访问一个repository对象,如下:
<span>$repository</span> = <span>$this</span>-><span>getDoctrine() </span>->getRepository('AppBundle:Product');
其中appBundle:Product是简洁写法,你可以在Doctrine中任意使用它来替代实体类的全限定名称(例如AppBundle\Entity\Product)。只要你的entity在你的bundle的Entity命名空间下它就会工作。你一旦有了Repository,你就可以访问其所有分类的帮助方法了。
<span>//</span><span> query by the primary key (usually "id")</span> <span>$product</span> = <span>$repository</span>->find(<span>$id</span><span>); </span><span>//</span><span> dynamic method names to find based on a column value</span> <span>$product</span> = <span>$repository</span>->findOneById(<span>$id</span><span>); </span><span>$product</span> = <span>$repository</span>->findOneByName('foo'<span>); </span><span>//</span><span> find *all* products</span> <span>$products</span> = <span>$repository</span>-><span>findAll(); </span><span>//</span><span> find a group of products based on an arbitrary column value</span> <span>$products</span> = <span>$repository</span>->findByPrice(19.99);
当然,你也可以使用复杂的查询,想了解更多请阅读Querying for Objects 。
你也可以有效利用findBy和findOneBy方法的优势,很容易的基于多个条件来获取对象。
<span>//</span><span> query for one product matching by name and price</span> <span>$product</span> = <span>$repository</span>-><span>findOneBy( </span><span>array</span>('name' => 'foo', 'price' => 19.99<span>) ); </span><span>//</span><span> query for all products matching the name, ordered by price</span> <span>$products</span> = <span>$repository</span>-><span>findBy( </span><span>array</span>('name' => 'foo'), <span>array</span>('price' => 'ASC'<span>) );</span>
当你去渲染页面,你可以在网页调试工具的右下角看到许多的查询。
doctrine_web_debug_toolbar
如果你单机该图标,分析页面将打开,显示你的精确查询。
如果你的页面查询超过了50个它会变成黄色。这可能表明你的程序有问题。
更新对象
一旦你从Doctrine中获取了一个对象,那么更新它就变得很容易了。假设你有一个路由映射一个产品id到一个controller的updateaction。
<span>public</span> <span>function</span> updateAction(<span>$id</span><span>) { </span><span>$em</span> = <span>$this</span>->getDoctrine()-><span>getManager(); </span><span>$product</span> = <span>$em</span>->getRepository('AppBundle:Product')->find(<span>$id</span><span>); </span><span>if</span> (!<span>$product</span><span>) { </span><span>throw</span> <span>$this</span>-><span>createNotFoundException( </span>'No product found for id '.<span>$id</span><span> ); } </span><span>$product</span>->setName('New product name!'<span>); </span><span>$em</span>-><span>flush</span><span>(); </span><span>return</span> <span>$this</span>->redirectToRoute('homepage'<span>); }</span>
更新一个对象包括三步:
1.从Doctrine取出对象
2.修改对象
3.在实体管理者上调用flush()方法
注意调用 $em->persist($product) 在这里没有必要。我们回想一下,调用该方法的目的主要是告诉Doctrine来管理或者“观察”$product对象。在这里,因为你已经取到了$product对象了,说明已经被管理了。
删除对象
删除一个对象,需要从实体管理者那里调用remove()方法。
<span>$em</span>->remove(<span>$product</span><span>); </span><span>$em</span>-><span>flush</span>();
正如你想的那样,remove()方法告诉Doctrine你想从数据库中移除指定的实体。真正的删除查询没有被真正的执行,直到flush()方法被调用。
查询对象
你已经看到了repository对象允许你执行一些基本的查询而不需要你做任何的工作。
<span>$repository</span>->find(<span>$id</span><span>); </span><span>$repository</span>->findOneByName('Foo');
当然,Doctrine 也允许你使用Doctrine Query Language(DQL)写一些复杂的查询,DQL类似于SQL,只是它用于查询一个或者多个实体类的对象,而SQL则是查询一个数据库表中的行。
在Doctrinez中查询时,你有两种选择:写纯Doctrine查询 或者 使用Doctrine的查询创建器。
使用Doctrine’s Query Builder查询对象
假设你想查询产品,需要返回价格高于19.99的产品,并且要求按价格从低到高排列。你可以使用Doctrine的QueryBuilder:
<span>$repository</span> = <span>$this</span>-><span>getDoctrine() </span>->getRepository('AppBundle:Product'<span>); </span><span>$query</span> = <span>$repository</span>->createQueryBuilder('p'<span>) </span>->where('p.price > :price'<span>) </span>->setParameter('price', '19.99'<span>) </span>->orderBy('p.price', 'ASC'<span>) </span>-><span>getQuery(); </span><span>$products</span> = <span>$query</span>->getResult();
QueryBuilder对象包含了创建查询的所有必须的方法。通过调用getQuery()方法,查询创建器将返回一个标准的Query对象。它跟我们直接写查询对象效果相同。
记住setParameter()方法。当Doctrine工作时,外部的值,会通过“占位符”(上面例子的:price)传入,来防止SQL注入攻击。
该getResult()方法返回一个结果数组。想要得到一个结果,你可以使用getSingleResult()(这个方法在没有结果时会抛出一个异常)或者getOneOrNullResult():
<span>$product</span> = <span>$query</span>->getOneOrNullResult();
更多Doctrine’s Query Builder的信息请阅读Query Builder。
使用DQL查询对象
不爱使用QueryBuilder,你还可以直接使用DQL查询:
<span>$em</span> = <span>$this</span>->getDoctrine()-><span>getManager(); </span><span>$query</span> = <span>$em</span>-><span>createQuery( </span>'<span>SELECT p FROM AppBundle:Product p WHERE p.price > :price ORDER BY p.price ASC</span>'<span> )</span>->setParameter('price', '19.99'<span>); </span><span>$products</span> = <span>$query</span>->getResult();
如果你习惯了写SQL,那么对于DQL也应该不会感到陌生。它们之间最大的不同就是你需要思考对象,而不是数据库表行。正因为如此,所以你从AppBundle:Product选择并给它定义别名p。(你看和上面完成的结果一样)。
该DQL语法强大到令人难以置信,允许您轻松地在之间加入实体(稍后会介绍关系)、组等。更多信息请参阅Doctrine Query Language文档。
自定义Repository类
在上面你已经开始在controller中创建和使用负责的查询了。为了隔离,测试和重用这些查询,一个好的办法是为你的实体创建一个自定义的repository类并添加相关逻辑查询方法。
要定义repository类,首先需要在你的映射定义中添加repository类的声明:
<span>//</span><span> src/AppBundle/Entity/Product.php</span> <span>namespace AppBundle\Entity; </span><span>use</span> Doctrine\ORM\Mapping <span>as</span><span> ORM; </span><span>/*</span><span>* * @ORM\Entity(repositoryClass="AppBundle\Entity\ProductRepository") </span><span>*/</span> <span>class</span><span> Product { </span><span>//</span><span>...</span> }
然后通过运行跟之前生成丢失的getter和setter方法同样的命令行,Doctrine会为你自动生成repository类。
$ php app/console doctrine:generate:entities AppBundle
下面,添加一个新方法findAllOrderedByName() 到新生成的repository类。该方法将查询所有的Product实体,并按照字符顺序排序。
<span>//</span><span> src/AppBundle/Entity/ProductRepository.php</span> <span>namespace AppBundle\Entity; </span><span>use</span><span> Doctrine\ORM\EntityRepository; </span><span>class</span> ProductRepository <span>extends</span><span> EntityRepository { </span><span>public</span> <span>function</span><span> findAllOrderedByName() { </span><span>return</span> <span>$this</span>-><span>getEntityManager() </span>-><span>createQuery( </span>'SELECT p FROM AppBundle:Product p ORDER BY p.name ASC'<span> ) </span>-><span>getResult(); } }</span>
在Repository类中可以通过$this->getEntityManager()方法类获取entity管理。
你就可以像使用默认的方法一样使用这个新定义的方法了:
<span>$em</span> = <span>$this</span>->getDoctrine()-><span>getManager(); </span><span>$products</span> = <span>$em</span>->getRepository('AppBundle:Product'<span>) </span>->findAllOrderedByName();
当使用一个自定义的repository类时,你依然可以访问原有的默认查找方法,比如find() 和findAll()等。
实体的关系/关联
假设你应用程序中的产品属于一确定的分类。这时你需要一个分类对象和一种把Product和Category对象联系在一起的方式。首先我们创建Category实体,我们最终要通过Doctrine来对其进行持久化,所以我们这里让Doctrine来帮我们创建这个类。
$ php app/console doctrine:generate:<span>entity \ </span>--entity="AppBundle:Category"<span> \ </span>--fields="name:string(255)"
该命令行为你生成一个Category实体,包含id字段和name字段以及相关的getter和setter方法。
关系映射
关联Category和Product两个实体,首先在Category类中创建一个products属性:
<span>//</span><span> src/AppBundle/Entity/Category.php // ...</span> <span>use</span><span> Doctrine\Common\Collections\ArrayCollection; </span><span>class</span><span> Category { </span><span>//</span><span> ...</span> <span>/*</span><span>* * @ORM\OneToMany(targetEntity="Product", mappedBy="category") </span><span>*/</span> <span>protected</span> <span>$products</span><span>; </span><span>public</span> <span>function</span><span> __construct() { </span><span>$this</span>->products = <span>new</span><span> ArrayCollection(); } }</span>
首先,由于一个Category对象将涉及到多个Product对象,一个products数组属性被添加到Category类保存这些 Product对象。其次,这不是因为Doctrine需要它,而是因为在应用程序中为每一个Category来保存一个Product数组非常有用。
代码中__construct()方法非常重要,因为Doctrine需要$products属性成为一个ArrayCollection对象,它跟数组非常类似,但会灵活一些。如果这让你感觉不舒服,不用担心。试想他是一个数组,你会欣然接受它。
上面注释所用的targetEntity 的值可以使用合法的命名空间引用任何实体,而不仅仅是定义在同一个类中的实体。 如果要关系一个定义在不同的类或者bundle中的实体则需要输入完全的命名空间作为目标实体。
接下来,因为每个Product类可以关联一个Category对象,所有添加一个$category属性到Product类:
<span>//</span><span> src/AppBundle/Entity/Product.php // ...</span> <span>class</span><span> Product { </span><span>//</span><span> ...</span> <span>/*</span><span>* * @ORM\ManyToOne(targetEntity="Category", inversedBy="products") * @ORM\JoinColumn(name="category_id", referencedColumnName="id") </span><span>*/</span> <span>protected</span> <span>$category</span><span>; }</span>
到现在为止,我们添加了两个新属性到Category和Product类。现在告诉Doctrine来为它们生成getter和setter方法。
$ 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列以及新的外键。
$ php app/console doctrine:schema:update --force
保存相关实体
现在让我们来看看Controller内的代码如何处理:
<span>//</span><span> ...</span> <span>use</span><span> AppBundle\Entity\Category; </span><span>use</span><span> AppBundle\Entity\Product; </span><span>use</span><span> Symfony\Component\HttpFoundation\Response; </span><span>class</span> DefaultController <span>extends</span><span> Controller { </span><span>public</span> <span>function</span><span> createProductAction() { </span><span>$category</span> = <span>new</span><span> Category(); </span><span>$category</span>->setName('Main Products'<span>); </span><span>$product</span> = <span>new</span><span> Product(); </span><span>$product</span>->setName('Foo'<span>); </span><span>$product</span>->setPrice(19.99<span>); </span><span>$product</span>->setDescription('Lorem ipsum dolor'<span>); </span><span>//</span><span> relate this product to the category</span> <span>$product</span>->setCategory(<span>$category</span><span>); </span><span>$em</span> = <span>$this</span>->getDoctrine()-><span>getManager(); </span><span>$em</span>->persist(<span>$category</span><span>); </span><span>$em</span>->persist(<span>$product</span><span>); </span><span>$em</span>-><span>flush</span><span>(); </span><span>return</span> <span>new</span><span> Response( </span>'Created product id: '.<span>$product</span>-><span>getId() </span>.' and category id: '.<span>$category</span>-><span>getId() ); } }</span>
现在,一个单独的行被添加到category和product表中。新产品的product.categroy_id列被设置为新category表中的id的值。Doctrine会为你管理这些持久化关系。
获取相关对象
当你需要获取相关的对象时,你的工作流跟以前一样。首先获取$product对象,然后访问它的相关Category
<span>public</span> <span>function</span> showAction(<span>$id</span><span>) { </span><span>$product</span> = <span>$this</span>-><span>getDoctrine() </span>->getRepository('AppBundle:Product'<span>) </span>->find(<span>$id</span><span>); </span><span>$categoryName</span> = <span>$product</span>->getCategory()-><span>getName(); </span><span>//</span><span> ...</span> }
在这个例子中,你首先基于产品id查询一个Product对象。他仅仅查询产品数据并把数据给$product对象。接下来,当你调 用$product->getCategory()->getName() 时,Doctrine默默的为你执行了第二次查询,查找一个与该产品相关的category,它生成一个$category对象返回给你。
重要的是你很容易的访问到了product的相关category对象。但是category的数据并不会被取出来而直到你请求category的时候。这就是延迟加载。
你也可以从其它方向进行查询:
<span>public</span> <span>function</span> showProductsAction(<span>$id</span><span>) { </span><span>$category</span> = <span>$this</span>-><span>getDoctrine() </span>->getRepository('AppBundle:Category'<span>) </span>->find(<span>$id</span><span>); </span><span>$products</span> = <span>$category</span>-><span>getProducts(); </span><span>//</span><span> ...</span> }
在这种情况下,同样的事情发生了。你首先查查一个category对象,然后Doctrine制造了第二次查询来获取与之相关联的所有Product对 象。只有在你调用->getProducts()时才会执行一次。 $products变量是一个通过它的category_id的值跟给定的category对象相关联的所有Product对象的集合。
关系和代理类
“延迟加载”成为可能,是因为Doctrine返回一个代理对象来代替真正的对象:
<span>$product</span> = <span>$this</span>-><span>getDoctrine() </span>->getRepository('AppBundle:Product'<span>) </span>->find(<span>$id</span><span>); </span><span>$category</span> = <span>$product</span>-><span>getCategory(); </span><span>//</span><span> prints "Proxies\AppBundleEntityCategoryProxy"</span> <span>echo</span> <span>get_class</span>(<span>$category</span>);
该代理对象继承了Category对象,从外表到行为都非常像category对象。所不同的是,通过这个代理对象,Doctrine可以延迟查询真正的Category对象数据,直到真正需要它时(调用$category->getName())。
Doctrine生成了代理对象并把它存储到cache目录中,尽管你可能从来没有发现过它。记住它这一点很重要。
我们可以通过join连接来一次性取出product和category数据。这时Doctrine将会返回真正的Category对象,因为不需要延迟加载。
join相关记录
在之前的我们的查询中,会产生两次查询操作,一次是获取原对象,一次是获取关联对象。
请记住,你可以通过网页调试工具查看请求的所有查询。
当然,如果你想一次访问两个对象,你可以通过一个join连接来避免二次查询。把下面的方法添加到ProductRepository类中:
<span>//</span><span> src/AppBundle/Entity/ProductRepository.php</span> <span>public</span> <span>function</span> findOneByIdJoinedToCategory(<span>$id</span><span>) { </span><span>$query</span> = <span>$this</span>-><span>getEntityManager() </span>-><span>createQuery( </span>'<span>SELECT p, c FROM AppBundle:Product p JOIN p.category c WHERE p.id = :id</span>'<span> )</span>->setParameter('id', <span>$id</span><span>); </span><span>try</span><span> { </span><span>return</span> <span>$query</span>-><span>getSingleResult(); } </span><span>catch</span> (\Doctrine\ORM\NoResultException <span>$e</span><span>) { </span><span>return</span> <span>null</span><span>; } }</span>
现在你就可以在你的controller中一次性查询一个产品对象和它关联的category对象信息了。
<span>public</span> <span>function</span> showAction(<span>$id</span><span>) { </span><span>$product</span> = <span>$this</span>-><span>getDoctrine() </span>->getRepository('AppBundle:Product'<span>) </span>->findOneByIdJoinedToCategory(<span>$id</span><span>); </span><span>$category</span> = <span>$product</span>-><span>getCategory(); </span><span>//</span><span> ...</span> }
更多关联信息
本节中已经介绍了一个普通的实体关联,一对多关系。对于更高级的关联和如何使用其他的关联(例如 一对一,多对一),请参见 doctrine 的Association Mapping Documentation.
如果你使用注释,你需要预先在所有注释加ORM\(如ORM\OneToMany),这些在doctrine官方文档里没有。你还需要声明use Doctrine\ORM\Mapping as ORM;才能使用annotations的ORM。
配置
Doctrine是高度可配置的,但是你可能永远不用关心他们。要想了解更多关于Doctrine的配置信息,请查看config reference。
生命周期回调
有时候你可能需要在一个实体被创建,更新或者删除的前后执行一些操作。这些操作方法处在一个实体不同的生命周期阶段,所以这些行为被称为”生命周期回调“。
如果你用annotations方式,开启一个生命周期回调,需要如下设置:(如果你不喜欢你也可以使用yaml和xml方式)
<span>/*</span><span>* * @ORM\Entity() * @ORM\HasLifecycleCallbacks() </span><span>*/</span> <span>class</span><span> Product { </span><span>//</span><span> ...</span> }
现在你可以告诉Doctrine在任何可用的生命周期事件上来执行一个方法了。比如,假设你想在一个新的实体第一次被创建时设置创建日期列(created)为当前日期。
<span>//</span><span> src/AppBundle/Entity/Product.php</span> <span>/*</span><span>* * @ORM\PrePersist </span><span>*/</span> <span>public</span> <span>function</span><span> setCreatedAtValue() { </span><span>$this</span>->createdAt = <span>new</span><span> \DateTime(); }</span>
上面的例子假设你已经创建了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围绕着一个简单的概念发展而来,但是它不可思议的强大。允许你创建复杂的查询和订阅事件,通过订阅事件你可以在整个持久化过程中执行一些不同的行为。