數據庫和Doctrine ORM
對于任何應用程序來說,一個最常見和最具挑戰(zhàn)的任務,就是從數據庫中讀取和持久化數據信息。盡管symfony框架并未整合任何需要使用數據庫的組件,但是卻緊密集成了一個名為 Doctrine 的三方類庫。Doctrine的主要目標是為你提供一個強有力的工具,令數據庫互動更加輕松和靈活。
在本章,你將學習如何在Symfony項目中利用doctrine來提供豐富的數據庫互動。
Doctrine與symfony是完全解耦的,使用與否是可選的。本章講的全部是Doctrine ORM,目的是讓你把對象映射到關系型數據庫中(如 MySQL, PostgreSQL 和 Microsoft SQL)。如果你傾向于使用數據庫的原始查詢,這很簡單,可參考 如何使用Doctrine DBAL 一文的講解。
你也可以使用Doctrine ODM類庫將數據持久化到 MongoDB。參考 DoctrineMongoDBBundle 以了解更多信息。
簡單例子:一件產品(Product) ?
要了解Doctrine是如何工作的,最簡單的方式就是看一個實際應用。在本節(jié),你需要配置你的數據庫,創(chuàng)建一個 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', ),));
創(chuàng)建一個Entity類 ?
假設你正構建一套程序,其中有些產品需要展示。即使不考慮Doctrine或者數據庫,你也已經知道你需要一個 Product
對象來呈現這些產品。在你AppBundle的 Entity
目錄下創(chuàng)建這個類:
// src/AppBundle/Entity/Product.phpnamespace AppBundle\Entity; class Product{ private $name; private $price; private $description;}
這個類——常被稱作一個“Entity”,表示 一個保存著數據的基本類 ——它很簡單,可以滿足程序中所需產品的業(yè)務需求。這個類還不能被保存到數據庫中——它只是個簡單的PHP類。
一旦你學習了Doctrine背后的概念,你可以讓Doctrine為你創(chuàng)建entity類。它將問你一些互動問題來幫你創(chuàng)建任意的entity:
$ php bin/console doctrine:generate:entity
添加映射信息 ?
Doctrine允許你以一種更加有趣的方式來使用數據庫,而不只是把標量數據的行(rows)取出到數組中。Doctrine允許你從數據庫中取出整個 對象,同時持久化整個對象到數據庫中。對Doctrine來說要實現這些,你必須 映射 數據表到特定的PHP類中,那些表的列(columns)必須被映射為相應PHP類的特定屬性。
你要以“元數據(meatdata)”形式來提供這些映射信息,有一組規(guī)則可以準確告之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 以了解關于映射的所有細節(jié)信息。如果你使用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 以了解如何正確規(guī)避這些名稱。可選地,你可以任意選擇數據庫的schema,輕松映射成不同的表名或列名。參考 Creating Classes for the Database 和 Property Mapping文檔。
當使用其他一些“使用了annotations”的類庫或者程序(如Doxygen)時,你應該把 @IgnoreAnnotation
注釋添加到類中,來指示Symfony應該忽略哪個annotation。
例如,要避免 @fn
annotation拋出異常,添加下列注釋:
/** * @IgnoreAnnotation("fn") */class Product// ...
創(chuàng)建entity之后,你應該使用以下命令來驗證映射(mappings):
$ php bin/console doctrine:schema:validate
生成Getters和Setters ?
盡管Doctrine現在知道了如何持久化 Product
對象到數據庫,但是類本身還不具備真正用途。因為 Product
僅僅是一個帶有 private
屬性的常規(guī)PHP類,你需要創(chuàng)建 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
創(chuàng)建數據表/Schema ?
現在你有了一個包含映射信息的可用 Product
類,因此Doctrine確切地知道如何持久化它。當然,你還沒有相應的 product
數據表在庫中。幸運的是,Doctrine可以自動創(chuàng)建所有的數據表。要這么做,運行以下命令:
$ php bin/console doctrine:schema:update --force
說真的,這條命令出奇的強大。它會比較你的數據庫 理論上應該是 什么樣子的(基于你的entities的映射信息)以及 實際上 它應該是什么樣,然后執(zhí)行所需的SQl語句來將數據庫的schema 更新到 它所應有的樣子。換句話說,如果你添加了一個包含“映射元數據”(mapping metadata)的新屬性到 Product
并運行此任務,它將執(zhí)行所需的 "ALTER TABLE" 語句,向已經存在的 product
表添加那個新列。
一個利用此功能之優(yōu)勢的更佳方式是通過 migrations,它允許你生成這些SQL語句,并把它們并存儲到migration類中,這些類能夠有序運行在你的生產環(huán)境中,進而安全可靠地更新和追蹤數據庫的schema改變。
不管你是否利用了數據庫遷移,doctrine:schema:update
命令只適合在開發(fā)環(huán)境中使用。它不應該被用于生產環(huán)境。
現在你的數據庫中有了一個全功能的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對象(還沒有語句執(zhí)行) $em->persist($product); // actually executes the queries (i.e. the INSERT query) // 真正執(zhí)行語句(如,INSERT 查詢) $em->flush(); return new Response('Saved new product with id '.$product->getId());}
如果你正在跟進本例程,需要創(chuàng)建一個路由,并指向這個action,才能看到它運行。
本例展示了在控制器中使用Doctrine的 getDoctrine() 方法。這是取出 doctrine
服務的快捷方法。若你在服務中注入此服務,即可在任意地方使用doctrine。參考 服務容器 以了解更多創(chuàng)建服務之內容。
深入分析一下前面的例子:
- 10-13行 在此處實例化,并且像其他常規(guī)PHP對象一樣去使用
$product
對象。 - 15行 這一行取出了Doctrine的 entity manager 對象,它負責處理數據庫的持久化(譯注:寫入)和取出對象的過程。
- 18行
persist($product)
調用,告訴Doctrine去 "管理"$product
對象。它 沒有 引發(fā)對數據庫的請求。 - 21行 當
flush()
方法被調用時,Doctrine會遍歷它管理的所有對象以確定是否需要被持久化到數據庫。本例中,$product
對象的數據在庫中并不存在,因此entity manager要執(zhí)行INSERT
請求,在product
表中創(chuàng)建一個新行。
事實上,由于Doctrine了解你的全部被管理的實體,當你調用 flush()
方法時,它會計算出所有的變更集合(changeset),并按正確順序執(zhí)行語句。它利用準備好的緩存語句以略微提高性能。比如,你要持久化總數為100的 Product
對象,然后調用 flush()
方法,Doctrine將用一個單一的prepare語法對象,來執(zhí)行100次 INSERT
請求。
如果 flush()
調用失敗,一個 Doctrine\ORM\ORMException
異常會被拋出。參考 Transactions and Concurrency(處理和并發(fā))。
在創(chuàng)建和更新對象時,工作流是相同的。在下一小節(jié)你將看到,如果記錄已經存在于數據庫中,Doctrine是如何聰明地自動發(fā)出一個 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// 動態(tài)方法名稱,基于字段的值來找到一件產品$product = $repository->findOneById($productId);$product = $repository->findOneByName('Keyboard'); // dynamic method names to find a group of products based on a column value // 動態(tài)方法名稱,基于字段值來找出一組產品$products = $repository->findByPrice(19.99); // find *all* products / 查出 *全部* 產品$products = $repository->findAll();
當然,你也可以使用復雜的查詢,參考 對象查詢 小節(jié) 。
你也可以有效利用 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
查詢不會被真正執(zhí)行,直到 flush()
方法被調用。
對象查詢 ?
你已經看到repository對象是如何讓你執(zhí)行一些基本查詢而毋須做任何工作了:
$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對象,來構建那個字符串。當你的查詢取決于動態(tài)條件時,這很有用,因為隨著你的連接字符串不斷增加,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對象包含了創(chuàng)建查詢時的所有必要方法。通過調用getQuery()方法,query builder將返回一個標準的Query對象,可用于取得請求的結果集。
Query Builder更多信息,參考Doctrine的 Query Builder 文檔。
把自定義查詢組織到Repository類中 ?
前面所有的查詢是直接寫在你的控制器中的。但對于程序的組織來說,Doctrine提供了一個專門的repository類,它允許你保存所有查詢邏輯到一個中心位置。
參考 如何創(chuàng)建自定義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(關聯),復雜查詢和事件監(jiān)聽。