数据库和Doctrine
对于任何应用程序来说,一个最常见和最具挑战的任务,就是从数据库中读取和持久化数据信息。尽管symfony框架并未整合任何需要使用数据库的组件,但是却紧密集成了一个名为 Doctrine 的三方类库。Doctrine的主要目标是为你提供一个强有力的工具,令数据库互动更加轻松和灵活。
在本章,你将学习如何在Symfony项目中利用doctrine来提供丰富的数据库互动。
Note
Doctrine与symfony是完全解耦的,使用与否是可选的。本章讲的全部是Doctrine ORM,目的是让你把对象映射到关系型数据库中(如 MySQL, postgresql 和 Microsoft SQL)。如果你倾向于使用数据库的原始查询,这很简单,可参考 如何使用Doctrine DBAL 一文的讲解。
你也可以使用Doctrine ODM类库将数据持久化到 Mongodb。参考 DoctrineMongoDBBundle 以了解更多信息。
简单例子:一件产品(Product) ¶
要了解Doctrine是如何工作的,最简单的方式就是看一个实际应用。在本节,你需要配置你的数据库,创建一个 Product
对象,把它持久化到数据库,再取回它。
配置数据库 ¶
真正开始之前,你需要配置你的数据库连接信息。按照惯例,这部分信息通常配置在 app/config/parameters.yml
文件中:
1 2 3 4 5 6 7 8 |
# app/config/parameters.yml
parameters:
database_host: localhost
database_name: test_project
database_user: root
database_password: password
# ... |
Note
通过 parameters.yml
来定义配置,只是一个惯例。配置Doctrine时,定义在那个文件中的参数,将被主配置文件引用:
1 2 3 4 5 6 7 8 |
# app/config/config.yml
doctrine:
dbal:
driver: pdo_mysql
host: "%database_host%"
dbname: "%database_name%"
user: "%database_user%"
password: "%database_password%" |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
<!-- 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> |
1 2 3 4 5 6 7 8 9 10 |
// 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
数据库:
1 |
$ php bin/console doctrine:database:create |
Note
如果你要用SQLite作为数据库,在path选项中设置你的数据库路径:
1 2 3 4 5 6 |
# app/config/config.yml
doctrine:
dbal:
driver: pdo_sqlite
path: "%kernel.root_dir%/sqlite.db"
charset: UTF8 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
<!-- 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> |
1 2 3 4 5 6 7 8 |
// 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
目录下创建这个类:
1 2 3 4 5 6 7 8 9 |
// src/AppBundle/Entity/Product.php
namespace AppBundle\Entity;
class Product
{
private $name;
private $price;
private $description;
} |
这个类——常被称作一个“Entity”,表示 一个保存着数据的基本类 ——它很简单,可以满足程序中所需产品的业务需求。这个类还不能被保存到数据库中——它只是个简单的PHP类。
Tip
一旦你学习了Doctrine背后的概念,你可以让Doctrine为你创建entity类。它将问你一些互动问题来帮你创建任意的entity:
1 |
$ php bin/console doctrine:generate:entity |
添加映射信息 ¶
Doctrine允许你以一种更加有趣的方式来使用数据库,而不只是把标量数据的行(rows)取出到数组中。Doctrine允许你从数据库中取出整个 对象,同时持久化整个对象到数据库中。对Doctrine来说要实现这些,你必须 映射 数据表到特定的PHP类中,那些表的列(columns)必须被映射为相应PHP类的特定属性。
你要以“元数据(meatdata)”形式来提供这些映射信息,有一组规则可以准确告之Doctrine Product
类及其属性应该如何 映射到 一个特定的数据表。这个metadata可以通过不同的格式来指定,包括YAML,XML或者通过DocBlock注释(译注:annotations)直接定义到 Product
类中:
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")
*/
private $id;
/**
* @ORM\Column(type="string", length=100)
*/
private $name;
/**
* @ORM\Column(type="decimal", scale=2)
*/
private $price;
/**
* @ORM\Column(type="text")
*/
private $description;
} |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
# src/AppBundle/Resources/config/doctrine/Product.orm.yml
AppBundle\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 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
<!-- 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> |
Note
一个bundle只可以接受一种metadata的定义格式。比如,不能把YAML的metadata定义和添加了注释(annotation)的PHP entity类混用。
Tip
表名是可选的,如果省略,将自动取决于entity类的名称。
Doctrine允许你选择广泛的字段类型,每一种都有自己的配置。可用字段类型的信息,参考 Doctrine字段类型参考。
Seealso
你也可以查看Doctrine官方文档 Basic Mapping Documentation 以了解关于映射的所有细节信息。如果你使用annotation,你需要为所有annotation加挂 ORM\
(例如 ORM\Column(...)
),这在Doctrine文档中并未写明。你还需要去包容 use Doctrine\ORM\Mapping as ORM;
声明,它可以 import(导入) ORM
annotation前缀。
Caution
小心Entity类名(或者其属性)同时也是一个SQL保留的关键字(如 group
和 user
)。例如,如果你的entity类名称为 Group
,那么,默认时,你的表名将会是group,这在一些数据库引擎中可能导致SQL错误。参考 Reserved SQL keywords documentation 以了解如何正确规避这些名称。可选地,你可以任意选择数据库的schema,轻松映射成不同的表名或列名。参考 Creating Classes for the Database 和 Property Mapping文档。
Note
当使用其他一些“使用了annotations”的类库或者程序(如Doxygen)时,你应该把 @IgnoreAnnotation
注释添加到类中,来指示Symfony应该忽略哪个annotation。
例如,要避免 @fn
annotation抛出异常,添加下列注释:
1 2 3 4 5 |
/**
* @IgnoreAnnotation("fn")
*/
class Product
// ... |
Tip
创建entity之后,你应该使用以下命令来验证映射(mappings):
1 |
$ php bin/console doctrine:schema:validate |
生成Getters和Setters ¶
尽管Doctrine现在知道了如何持久化 Product
对象到数据库,但是类本身还不具备真正用途。因为 Product
仅仅是一个带有 private
属性的常规PHP类,你需要创建 public
的getter和setter方法(比如 getName()
, setName($name)
)以便在程序其他部分来访问它的属性(其属性是protected)。幸运的是,下面的命令可以自动生成这些模板化的方法:
1 |
$ php bin/console doctrine:generate:entities AppBundle/Entity/Product |
该命令可以确保 Product
类所有的getter和setter都被生成。这是一个安全的命令行——你可以多次运行它,它只会生成那些不存在的getters和setters(即,不会替换已有的方法)。
Caution
重要提示 下面这句话极其深刻,乃是活用Doctrine的关键。大家一定照做。
记得,doctrine entity generator生成的是简单的getters/setters。你应该复审那些已生成的方法,在必要时,添加逻辑进去,以满足你的程序之需求。
你也可以为一个bundle或者一个entity命名空间内的所有已知实体(任何包含Doctrine映射信息的PHP类)来生成getter和setter:
1 2 3 4 5 6 7 |
# 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可以自动创建所有的数据表。要这么做,运行以下命令:
1 |
$ php bin/console doctrine:schema:update --force |
Tip
说真的,这条命令出奇的强大。它会比较你的数据库 理论上应该是 什么样子的(基于你的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
添加以下方法:
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 |
// 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());
} |
Note
如果你正在跟进本例程,需要创建一个路由,并指向这个action,才能看到它运行。
Tip
本例展示了在控制器中使用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
表中创建一个新行。
Note
事实上,由于Doctrine了解你的全部被管理的实体,当你调用 flush()
方法时,它会计算出所有的变更集合(changeset),并按正确顺序执行语句。它利用准备好的缓存语句以略微提高性能。比如,你要持久化总数为100的 Product
对象,然后调用 flush()
方法,Doctrine将用一个单一的prepare语法对象,来执行100次 INSERT
请求。
Note
如果 flush()
调用失败,一个 Doctrine\ORM\ORMException
异常会被抛出。参考 Transactions and Concurrency(处理和并发)。
在创建和更新对象时,工作流是相同的。在下一小节你将看到,如果记录已经存在于数据库中,Doctrine是如何聪明地自动发出一个 Update
语句的。
Tip
Doctrine提供了一个类库,允许你程序化地加载测试数据到你的项目中(即,"fixture data",固定的数据)。参考 DoctrineFixturesBundle 以了解更多。
从数据库中获取对象 ¶
从数据库中取回对象就更简单了,举个例子,假如你配置了一个路由,基于产品的 id
来显示特定的 Product
对象:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
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 对象传入模板
} |
Tip
你可以使用 @ParamConverter
快捷注释,毋须编写任何代码即可实现同样的功能。参考 FrameworkExtraBundle 以了解更多。
当你要查询某个特定类型的对象时,你总是要使用它的”respository”(宝库)。你可以认为Respository是一个PHP类,它的唯一工作就是帮助你从那个特定的类中取出entity。对于一个entity类,要访问其宝库,通过:
1 2 |
$repository = $this->getDoctrine()
->getRepository('AppBundle:Product'); |
Note
appBundle:Product
是快捷写法,你可以在Doctrine里随处使用,以替代entity类的FQCN类名(如 AppBundle\Entity\Product
)。只要你的entity存放在bundle的 Entity
命名空间下,它就会工作。
一旦有了Repository对象,你就可以访问它的全部有用的方法了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
$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(); |
Note
当然,你也可以使用复杂的查询,参考 对象查询 小节 。
你也可以有效利用 findBy
和 findOneBy
方法,基于多个条件来轻松获取对象:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
$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')
); |
Tip
渲染任何页面时,你可以在除错工具条(web debug toolbar)的右下角看到许多查询。
如果你点击图标,分析器(profiler)将会打开,显示出所产生的精确查询。
如果你的页面查询超过了50个,图标会变成黄色。这表明某些地方不大对劲。
对象更新 ¶
一旦从Doctrine中获取了一个对象,更新它就很容易了。假设你有一个路由,把一个产品id映射到controller的updateaction:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
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()
方法:
1 2 |
$em->remove($product);
$em->flush(); |
你可能已经预期,remove()
方法通知Doctrine你想从数据库中删除指定的entity。真正的 DELETE
查询不会被真正执行,直到 flush()
方法被调用。
对象查询 ¶
你已经看到repository对象是如何让你执行一些基本查询而毋须做任何工作了:
1 2 3 4 |
$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的语法,来构造一个用于此场景的查询:
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也会非常自然。它们之间最大的不同就是你需要就“select PHP对象”来进行思考,而不是数据表的行。正因为如此,你要 从 AppBundle:Product
这个 entity (可选的一个AppBundle\Entity\Product
类的快捷写法)来select,然后给entity一个 p
的别名。
Tip
注意 setParameter()
方法。当使用Doctrine时,通过“占位符”来设置任意的外部值(上面例子的 :price
),是一个好办法,因为它可以防止SQL注入攻击。
getResult()
方法返回一个结果数组。要得到一个结果,可以使用getSingleResult()
(这个方法在没有结果时会抛出一个异常)或者 getOneOrNullResult()
:
1 |
$product = $query->setMaxResults(1)->getOneOrNullResult(); |
DQL语法强大到令人难以置信,允许轻松地在entity之间进行join(稍后会覆盖relations)和group等。参考 Doctrine Query Language 文档以了解更多。
使用Doctrine's Query Builder进行对象查询 ¶
不去写DQL的大字符串,你可以使用一个非常有用的QueryBuilder对象,来构建那个字符串。当你的查询取决于动态条件时,这很有用,因为随着你的连接字符串不断增加,DQL代码会越来越难以阅读:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
$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(关联),复杂查询和事件监听。