数据库和Doctrine ORM
对于任何应用程序来说,一个最常见和最具挑战的任务,就是从数据库中读取和持久化数据信息。尽管symfony框架并未整合任何需要使用数据库的组件,但是却紧密集成了一个名为 Doctrine 的三方类库。Doctrine的主要目标是为你提供一个强有力的工具,令数据库互动更加轻松和灵活。
在本章,你将学习如何在Symfony项目中利用doctrine来提供丰富的数据库互动。
Doctrine与symfony是完全解耦的,使用与否是可选的。本章讲的全部是Doctrine ORM,目的是让你把对象映射到关系型数据库中(如 MySQL, PostgreSQL 和 Microsoft SQL)。如果你倾向于使用数据库的原始查询,这很简单,可参考 如何使用Doctrine DBAL 一文的讲解。
你也可以使用Doctrine ODM类库将数据持久化到 MongoDB。参考 DoctrineMongoDBBundle 以了解更多信息。
简单例子:一件产品(Product) ¶
要了解Doctrine是如何工作的,最简单的方式就是看一个实际应用。在本节,你需要配置你的数据库,创建一个 Product
对象,把它持久化到数据库,再取回它。
配置数据库 ¶
真正开始之前,你需要配置你的数据库连接信息。按照惯例,这部分信息通常配置在 app/config/parameters.yml
文件中:
# app/config/parameters.ymlparameters: database_host: localhost database_name: test_project database_user: root database_password: password # ...
通过 parameters.yml
来定义配置,只是一个惯例。配置Doctrine时,定义在那个文件中的参数,将被主配置文件引用:
# app/config/config.ymldoctrine: dbal: driver: pdo_mysql host: "%database_host%" dbname: "%database_name%" user: "%database_user%" password: "%database_password%"
<!-- app/config/config.xml --><?xml version="1.0" encoding="UTF-8" ?><container xmlns="http://symfony.com/schema/dic/services" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:doctrine="http://symfony.com/schema/dic/doctrine" xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd http://symfony.com/schema/dic/doctrine http://symfony.com/schema/dic/doctrine/doctrine-1.0.xsd"> <doctrine:config> <doctrine:dbal driver="pdo_mysql" host="%database_host%" dbname="%database_name%" user="%database_user%" password="%database_password%" /> </doctrine:config></container>
// app/config/config.php$configuration->loadFromExtension('doctrine', array( 'dbal' => array( 'driver' => 'pdo_mysql', 'host' => '%database_host%', 'dbname' => '%database_name%', 'user' => '%database_user%', 'password' => '%database_password%', ),));
通过把数据库信息分离到一个单独文件中,你可以很容易地为每个服务器保存不同的版本。你还可以在项目外轻松存储数据库配置(或任何敏感信息),举例来说,就和apache中的配置信息一样。参考 服务容器外部参数如何设置 以了解更多。
现在Doctrine可以连接你的数据库了,下面的命令可以自动生成一个空的 test_project
数据库:
$ php bin/console doctrine:database:create
如果你要用SQLite作为数据库,在path选项中设置你的数据库路径:
# app/config/config.ymldoctrine: dbal: driver: pdo_sqlite path: "%kernel.root_dir%/sqlite.db" charset: UTF8
<!-- app/config/config.xml --><?xml version="1.0" encoding="UTF-8" ?><container xmlns="http://symfony.com/schema/dic/services" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:doctrine="http://symfony.com/schema/dic/doctrine" xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd http://symfony.com/schema/dic/doctrine http://symfony.com/schema/dic/doctrine/doctrine-1.0.xsd"> <doctrine:config> <doctrine:dbal driver="pdo_sqlite" path="%kernel.root_dir%/sqlite.db" charset="UTF-8" /> </doctrine:config></container>
// app/config/config.php$container->loadFromExtension('doctrine', array( 'dbal' => array( 'driver' => 'pdo_sqlite', 'path' => '%kernel.root_dir%/sqlite.db', 'charset' => 'UTF-8', ),));
创建一个Entity类 ¶
假设你正构建一套程序,其中有些产品需要展示。即使不考虑Doctrine或者数据库,你也已经知道你需要一个 Product
对象来呈现这些产品。在你AppBundle的 Entity
目录下创建这个类:
// src/AppBundle/Entity/Product.phpnamespace AppBundle\Entity; class Product{ private $name; private $price; private $description;}
这个类——常被称作一个“Entity”,表示 一个保存着数据的基本类 ——它很简单,可以满足程序中所需产品的业务需求。这个类还不能被保存到数据库中——它只是个简单的PHP类。
一旦你学习了Doctrine背后的概念,你可以让Doctrine为你创建entity类。它将问你一些互动问题来帮你创建任意的entity:
$ php bin/console doctrine:generate:entity
添加映射信息 ¶
Doctrine允许你以一种更加有趣的方式来使用数据库,而不只是把标量数据的行(rows)取出到数组中。Doctrine允许你从数据库中取出整个 对象,同时持久化整个对象到数据库中。对Doctrine来说要实现这些,你必须 映射 数据表到特定的PHP类中,那些表的列(columns)必须被映射为相应PHP类的特定属性。
你要以“元数据(meatdata)”形式来提供这些映射信息,有一组规则可以准确告之Doctrine Product
类及其属性应该如何 映射到 一个特定的数据表。这个metadata可以通过不同的格式来指定,包括YAML,XML或者通过DocBlock注释(译注:annotations)直接定义到 Product
类中:
// src/AppBundle/Entity/Product.phpnamespace 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") */ private $id; /** * @ORM\Column(type="string", length=100) */ private $name; /** * @ORM\Column(type="decimal", scale=2) */ private $price; /** * @ORM\Column(type="text") */ private $description;
# src/AppBundle/Resources/config/doctrine/Product.orm.ymlAppBundle\Entity\Product: type: entity table: product id: id: type: integer generator: { strategy: AUTO } fields: name: type: string length: 100 price: type: decimal scale: 2 description: type: text
<!-- 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" table="product"> <id name="id" type="integer"> <generator strategy="AUTO" /> </id> <field name="name" type="string" length="100" /> <field name="price" type="decimal" scale="2" /> <field name="description" type="text" /> </entity></doctrine-mapping>
一个bundle只可以接受一种metadata的定义格式。比如,不能把YAML的metadata定义和添加了注释(annotation)的PHP entity类混用。
表名是可选的,如果省略,将自动取决于entity类的名称。
Doctrine允许你选择广泛的字段类型,每一种都有自己的配置。可用字段类型的信息,参考 Doctrine字段类型参考。
你也可以查看Doctrine官方文档 Basic Mapping Documentation 以了解关于映射的所有细节信息。如果你使用annotation,你需要为所有annotation加挂 ORM\
(例如 ORM\Column(...)
),这在Doctrine文档中并未写明。你还需要去包容 use Doctrine\ORM\Mapping as ORM;
声明,它可以 import(导入) ORM
annotation前缀。
小心Entity类名(或者其属性)同时也是一个SQL保留的关键字(如 group
和 user
)。例如,如果你的entity类名称为 Group
,那么,默认时,你的表名将会是group,这在一些数据库引擎中可能导致SQL错误。参考 Reserved SQL keywords documentation 以了解如何正确规避这些名称。可选地,你可以任意选择数据库的schema,轻松映射成不同的表名或列名。参考 Creating Classes for the Database 和 Property Mapping文档。
当使用其他一些“使用了annotations”的类库或者程序(如Doxygen)时,你应该把 @IgnoreAnnotation
注释添加到类中,来指示Symfony应该忽略哪个annotation。
例如,要避免 @fn
annotation抛出异常,添加下列注释:
/** * @IgnoreAnnotation("fn") */class Product// ...
创建entity之后,你应该使用以下命令来验证映射(mappings):
$ php bin/console doctrine:schema:validate
生成Getters和Setters ¶
尽管Doctrine现在知道了如何持久化 Product
对象到数据库,但是类本身还不具备真正用途。因为 Product
仅仅是一个带有 private
属性的常规PHP类,你需要创建 public
的getter和setter方法(比如 getName()
, setName($name)
)以便在程序其他部分来访问它的属性(其属性是protected)。幸运的是,下面的命令可以自动生成这些模板化的方法:
$ php bin/console doctrine:generate:entities AppBundle/Entity/Product
该命令可以确保 Product
类所有的getter和setter都被生成。这是一个安全的命令行——你可以多次运行它,它只会生成那些不存在的getters和setters(即,不会替换已有的方法)。
重要提示下面这句话极其深刻,乃是活用Doctrine的关键。大家一定照做。
记得,doctrine entity generator生成的是简单的getters/setters。你应该复审那些已生成的方法,在必要时,添加逻辑进去,以满足你的程序之需求。
你也可以为一个bundle或者一个entity命名空间内的所有已知实体(任何包含Doctrine映射信息的PHP类)来生成getter和setter:
# generates all entities in the AppBundle# 生成AppBundle下的全部entities$ php bin/console doctrine:generate:entities AppBundle # generates all entities of bundles in the Acme namespace # 生成Acme命名空间下的bundles的全部entities $ php bin/console doctrine:generate:entities Acme
创建数据表/Schema ¶
现在你有了一个包含映射信息的可用 Product
类,因此Doctrine确切地知道如何持久化它。当然,你还没有相应的 product
数据表在库中。幸运的是,Doctrine可以自动创建所有的数据表。要这么做,运行以下命令:
$ php bin/console doctrine:schema:update --force
说真的,这条命令出奇的强大。它会比较你的数据库 理论上应该是 什么样子的(基于你的entities的映射信息)以及 实际上 它应该是什么样,然后执行所需的SQl语句来将数据库的schema 更新到 它所应有的样子。换句话说,如果你添加了一个包含“映射元数据”(mapping metadata)的新属性到 Product
并运行此任务,它将执行所需的 "ALTER TABLE" 语句,向已经存在的 product
表添加那个新列。
一个利用此功能之优势的更佳方式是通过 migrations,它允许你生成这些SQL语句,并把它们并存储到migration类中,这些类能够有序运行在你的生产环境中,进而安全可靠地更新和追踪数据库的schema改变。
不管你是否利用了数据库迁移,doctrine:schema:update
命令只适合在开发环境中使用。它不应该被用于生产环境。
现在你的数据库中有了一个全功能的product表,它的列与你指定的元数据相匹配。
持久化对象到数据库 ¶
现在你有了一个Product实体和与之映射的product数据库表。你可以把数据持久化到数据库里。在Controller内,它非常简单。添加下面的方法到bundle的DefaultController中。
现在你已经把 Product
entity 映射到与之对应的 product
表中,你已经准备好把 Product
对象持久化到数据库中。在控制器里面,这极其简单。向bundle的 DefaultController
添加以下方法:
// src/AppBundle/Controller/DefaultController.php // ...use AppBundle\Entity\Product;use Symfony\Component\HttpFoundation\Response; // ...public function createAction(){ $product = new Product(); $product->setName('Keyboard'); $product->setPrice(19.99); $product->setDescription('Ergonomic and stylish!'); $em = $this->getDoctrine()->getManager(); // tells Doctrine you want to (eventually) save the Product (no queries yet) // 告诉Doctrine你希望(最终)存储Product对象(还没有语句执行) $em->persist($product); // actually executes the queries (i.e. the INSERT query) // 真正执行语句(如,INSERT 查询) $em->flush(); return new Response('Saved new product with id '.$product->getId());}
如果你正在跟进本例程,需要创建一个路由,并指向这个action,才能看到它运行。
本例展示了在控制器中使用Doctrine的 getDoctrine() 方法。这是取出 doctrine
服务的快捷方法。若你在服务中注入此服务,即可在任意地方使用doctrine。参考 服务容器 以了解更多创建服务之内容。
深入分析一下前面的例子:
- 10-13行 在此处实例化,并且像其他常规PHP对象一样去使用
$product
对象。 - 15行 这一行取出了Doctrine的 entity manager 对象,它负责处理数据库的持久化(译注:写入)和取出对象的过程。
- 18行
persist($product)
调用,告诉Doctrine去 "管理"$product
对象。它 没有 引发对数据库的请求。 - 21行 当
flush()
方法被调用时,Doctrine会遍历它管理的所有对象以确定是否需要被持久化到数据库。本例中,$product
对象的数据在库中并不存在,因此entity manager要执行INSERT
请求,在product
表中创建一个新行。
事实上,由于Doctrine了解你的全部被管理的实体,当你调用 flush()
方法时,它会计算出所有的变更集合(changeset),并按正确顺序执行语句。它利用准备好的缓存语句以略微提高性能。比如,你要持久化总数为100的 Product
对象,然后调用 flush()
方法,Doctrine将用一个单一的prepare语法对象,来执行100次 INSERT
请求。
如果 flush()
调用失败,一个 Doctrine\ORM\ORMException
异常会被抛出。参考 Transactions and Concurrency(处理和并发)。
在创建和更新对象时,工作流是相同的。在下一小节你将看到,如果记录已经存在于数据库中,Doctrine是如何聪明地自动发出一个 Update
语句的。
Doctrine提供了一个类库,允许你程序化地加载测试数据到你的项目中(即,"fixture data",固定的数据)。参考 DoctrineFixturesBundle 以了解更多。
从数据库中获取对象 ¶
从数据库中取回对象就更简单了,举个例子,假如你配置了一个路由,基于产品的 id
来显示特定的 Product
对象:
public function showAction($productId){ $product = $this->getDoctrine() ->getRepository('AppBundle:Product') ->find($productId); if (!$product) { throw $this->createNotFoundException( 'No product found for id '.$productId ); } // ... do something, like pass the $product object into a template // ... 做一些事,比如把 $product 对象传入模板}
你可以使用 @ParamConverter
快捷注释,毋须编写任何代码即可实现同样的功能。参考 FrameworkExtraBundle 以了解更多。
当你要查询某个特定类型的对象时,你总是要使用它的”respository”(宝库)。你可以认为Respository是一个PHP类,它的唯一工作就是帮助你从那个特定的类中取出entity。对于一个entity类,要访问其宝库,通过:
$repository = $this->getDoctrine() ->getRepository('AppBundle:Product');
appBundle:Product
是快捷写法,你可以在Doctrine里随处使用,以替代entity类的FQCN类名(如 AppBundle\Entity\Product
)。只要你的entity存放在bundle的 Entity
命名空间下,它就会工作。
一旦有了Repository对象,你就可以访问它的全部有用的方法了。
$repository = $this->getDoctrine()->getRepository('AppBundle:Product'); // query for a single product by its primary key (usually "id")// 通过主键(通常是id)查询一件产品 $product = $repository->find($productId); // dynamic method names to find a single product based on a column value// 动态方法名称,基于字段的值来找到一件产品$product = $repository->findOneById($productId);$product = $repository->findOneByName('Keyboard'); // dynamic method names to find a group of products based on a column value // 动态方法名称,基于字段值来找出一组产品$products = $repository->findByPrice(19.99); // find *all* products / 查出 *全部* 产品$products = $repository->findAll();
当然,你也可以使用复杂的查询,参考 对象查询 小节 。
你也可以有效利用 findBy
和 findOneBy
方法,基于多个条件来轻松获取对象:
$repository = $this->getDoctrine()->getRepository('AppBundle:Product'); // query for a single product matching the given name and price// 查询一件产品,要匹配给定的名称和价格$product = $repository->findOneBy( array('name' => 'Keyboard', 'price' => 19.99)); // query for multiple products matching the given name, ordered by price// 查询多件产品,要匹配给定的名称和价格$products = $repository->findBy( array('name' => 'Keyboard'), array('price' => 'ASC'));
渲染任何页面时,你可以在除错工具条(web debug toolbar)的右下角看到许多查询。
如果你点击图标,分析器(profiler)将会打开,显示出所产生的精确查询。
如果你的页面查询超过了50个,图标会变成黄色。这表明某些地方不大对劲。
对象更新 ¶
一旦从Doctrine中获取了一个对象,更新它就很容易了。假设你有一个路由,把一个产品id映射到controller的updateaction:
public function updateAction($productId){ $em = $this->getDoctrine()->getManager(); $product = $em->getRepository('AppBundle:Product')->find($productId); if (!$product) { throw $this->createNotFoundException( 'No product found for id '.$productId ); } $product->setName('New product name!'); $em->flush(); return $this->redirectToRoute('homepage');}
更新一个对象包括三步:
- 从Doctrine中取出对象;
- 修改对象;
- 调用entity manager的
flush()
方法。
注意调用 $em->persist($product)
是不必要的。回想一下,这个方法只是告诉Doctrine去管理或者“观察” $product
对象。此处,因为你已经取到了 $product
对象了,它已经被管理了。
删除对象 ¶
删除一个对象十分类似,但需要从entity manager调用 remove()
方法:
$em->remove($product);$em->flush();
你可能已经预期,remove()
方法通知Doctrine你想从数据库中删除指定的entity。真正的 DELETE
查询不会被真正执行,直到 flush()
方法被调用。
对象查询 ¶
你已经看到repository对象是如何让你执行一些基本查询而毋须做任何工作了:
$repository = $this->getDoctrine()->getRepository('AppBundle:Product'); $product = $repository->find($productId);$product = $repository->findOneByName('Keyboard');
当然,Doctrine 也允许你使用Doctrine Query Language(DQL)来写一些复杂的查询,DQL类似于SQL,只是它用于查询一个或者多个entity类的对象(如 product
),而SQL则是查询一个数据表中的行(如 product
)。
在Doctrine中查询时,你有两个主要选择:编写纯正的Doctrine查询(DQL) 或者 使用Doctrine的Query Builder。
使用DQL进行对象查询 ¶
假设你要查询价格高于 19.99
的产品,并且按价格从低到高排列。你可以使用DQL,Doctrine中类似原生SQL的语法,来构造一个用于此场景的查询:
$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也会非常自然。它们之间最大的不同就是你需要就“select PHP对象”来进行思考,而不是数据表的行。正因为如此,你要 从 AppBundle:Product
这个 entity (可选的一个AppBundle\Entity\Product
类的快捷写法)来select,然后给entity一个 p
的别名。
注意 setParameter()
方法。当使用Doctrine时,通过“占位符”来设置任意的外部值(上面例子的 :price
),是一个好办法,因为它可以防止SQL注入攻击。
getResult()
方法返回一个结果数组。要得到一个结果,可以使用getSingleResult()
(这个方法在没有结果时会抛出一个异常)或者 getOneOrNullResult()
:
$product = $query->setMaxResults(1)->getOneOrNullResult();
DQL语法强大到令人难以置信,允许轻松地在entity之间进行join(稍后会覆盖relations)和group等。参考 Doctrine Query Language 文档以了解更多。
使用Doctrine's Query Builder进行对象查询 ¶
不去写DQL的大字符串,你可以使用一个非常有用的QueryBuilder对象,来构建那个字符串。当你的查询取决于动态条件时,这很有用,因为随着你的连接字符串不断增加,DQL代码会越来越难以阅读:
$repository = $this->getDoctrine() ->getRepository('AppBundle:Product'); // createQueryBuilder() automatically selects FROM AppBundle:Product// and aliases it to "p"// createQueryBuilder() 自动从 AppBundle:Product 进行 select 并赋予 p 假名$query = $repository->createQueryBuilder('p') ->where('p.price > :price') ->setParameter('price', '19.99') ->orderBy('p.price', 'ASC') ->getQuery(); $products = $query->getResult();// to get just one result: / 要得到一个结果:// $product = $query->setMaxResults(1)->getOneOrNullResult();
QueryBuilder对象包含了创建查询时的所有必要方法。通过调用getQuery()方法,query builder将返回一个标准的Query对象,可用于取得请求的结果集。
Query Builder更多信息,参考Doctrine的 Query Builder 文档。
把自定义查询组织到Repository类中 ¶
前面所有的查询是直接写在你的控制器中的。但对于程序的组织来说,Doctrine提供了一个专门的repository类,它允许你保存所有查询逻辑到一个中心位置。
参考 如何创建自定义Repository类 以了解更多。
配置 ¶
Doctrine是高度可配置的,虽然你可能永远不会去关心那些选项。要了解Doctrine的配置信息,参考 config reference。
Doctrine字段类型参考 ¶
Doctrine配备了大量可用的字段类型。每一个都能把PHP数据类型映射到特定的字段类型中,无论你使用什么数据库。对于每一个字段类型, Column
都可以被进一步配置,可以设置 length
、nullable
行为,name
或者其他选项。可用字段类型的列表,参考 Mapping Types documentation。
Associations(关系) 和 Relations(关联) ¶
Doctrine 提供了你所需要的管理数据库关系(也被称为关联-associations)的所有的功能。更多信息,参考 如何使用Doctrine Associations / Relations。
总结 ¶
有了Doctrine,你可以集中精力到你的 对象 以及 如何把它应用到程序中,而数据库持久化则是第二位。这是因为Doctrine允许你使用任何的PHP对象来保存你的数据,并且依靠“元数据映射”信息来把一个对象的数据映射到一个特定的数据表之中。
Doctrine有很多强大的功能等着你去学习,像是relationships(关联),复杂查询和事件监听。