對於任何應用程序來說最為普遍最具挑戰性的任務,就是從數據庫中 讀取和持久化數據信息。盡管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文件中:
# 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時引用。
# app/config/config.yml doctrine: 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知道你的數據庫配置了,你可以用它來創建一個數據庫了。
$ php app/console doctrine:database:create
設置數據庫為UTF8
即便對於經驗豐富的程序員來說,一個常犯的錯誤是,在Symfony項目開始後,忘記設置他們的數據庫默認字符集和校對規則,僅把大部分數據庫給出的latin類型的校對作為默認。他們也許在第一次操作時會記得,但到了後面敲打兩行相關的常規命令之後,就完全忘掉了。
$ php app/console doctrine:database:drop --force
$ php app/console doctrine:database:create
在Doctrine裡直接指派默認字符集是不可能的,因為doctrine會根據環境配置,盡可能多地去適應各種“不可知”情形。解決辦法之一,是去配置“服務器級別”的默認信息。
設置UTF8為MySql的默認字符集是非常簡單的,只要在數據庫配置文件中加幾行代碼就可以了(一般是my.cnf文件)
[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
我們推薦避免使用Mysql的uft8字符集,因為它並不兼容4-byte unicode字符,如果字符串中有這種字符會被清空。不過這種情況被修復了,參考《新型utf8mb4字符集》
---------------------------------------------新建數據庫這段還不如直接在mysql裡面操作更方便-----------------------------------------------
如果你想要使用SQLite作為數據庫,你需要設置path為你的數據庫路徑
# app/config/config.yml doctrine: dbal: driver: pdo_sqlite path: "%kernel.root_dir%/sqlite.db" charset: UTF8
創建一個實體類
假設你創建一個應用程序,其中有些產品需要展示。即時不考慮Doctrine或者數據庫,你也應該知道你需要一個Product對象來表現這些產品。在你的AppBundle的Entity目錄下創建一個類。
// src/AppBundle/Entity/Product.php namespace AppBundle\Entity; class Product { protected $name; protected $price; protected $description; }
這樣的類經常被稱為“Entity”,意味著一個基礎類保存數據。它們簡單來滿足你應用程序的業務需要。不過現在它還不能被保存到數據庫中,因為現在它只不過還是個簡單的PHP類。
一旦你學習了Doctrine背後的概念,你可以讓Doctrine來為你創建實體類。他會問你一些問題來創建entity:
$ php app/console doctrine:generate:entity
添加映射信息
Doctrine允許你使用一種更加有趣的方式對數據庫進行操作,而不是只是獲取基於列表的行到數組中。Doctrine允許你保存整個對象到數據庫或者把對象從數據庫中取出。這些都是通過映射PHP類到一個數據庫表,PHP類的屬性對應數據庫表的列來實現的。
因為Doctrine能夠做這些,所以你僅僅只需要創建一個meatdata,或者配置告訴Doctrine的Product類和它的屬性應該如何映射到數據庫。這些metadata可以被定義成各種格式,包括YAML,XML或者通過聲明直接定義到Product類中。
annotations: // 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 聲明拋出異常,可以這樣:
/** * @IgnoreAnnotation("fn") */ class Product // ...
生產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:
# 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可以自動創建所有的數據庫表。
$ php app/console doctrine:schema:update --force
說真的,這條命令是出奇的強大。它會基於你的entities的映射信息,來比較現在的數據庫,並生成所需要的新數據庫的更新SQl語句。換句話說,如 果你想添加一個新的屬性映射元數據到Product並運行該任務,它將生成一個alert table 語句來添加新的列到已經存在的product表中。
一個更好的發揮這一優勢的功能是通過migrations,它允許你生成這些SQL語句。並存儲到一個遷移類,並能有組織的運行在你的生產環境中,系統為了安全可靠地跟蹤和遷移數據庫。
現在你的數據庫中有了一個全功能的product表,它的每個列都會被映射到你指定的元數據。
持久化對象到數據庫
現在我們有了一個Product實體和與之映射的product數據庫表。你可以把數據持久化到數據庫裡。在Controller內,它非常簡單。添加下面的方法到bundle的DefaultController中。
// 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。
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對象,如下:
$repository = $this->getDoctrine() ->getRepository('AppBundle:Product');
其中appBundle:Product是簡潔寫法,你可以在Doctrine中任意使用它來替代實體類的全限定名稱(例如AppBundle\Entity\Product)。只要你的entity在你的bundle的Entity命名空間下它就會工作。你一旦有了Repository,你就可以訪問其所有分類的幫助方法了。
// 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方法的優勢,很容易的基於多個條件來獲取對象。
// 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') );
當你去渲染頁面,你可以在網頁調試工具的右下角看到許多的查詢。
doctrine_web_debug_toolbar
如果你單機該圖標,分析頁面將打開,顯示你的精確查詢。
如果你的頁面查詢超過了50個它會變成黃色。這可能表明你的程序有問題。
更新對象
一旦你從Doctrine中獲取了一個對象,那麼更新它就變得很容易了。假設你有一個路由映射一個產品id到一個controller的updateaction。
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()方法。
$em->remove($product); $em->flush();
正如你想的那樣,remove()方法告訴Doctrine你想從數據庫中移除指定的實體。真正的刪除查詢沒有被真正的執行,直到flush()方法被調用。
查詢對象
你已經看到了repository對象允許你執行一些基本的查詢而不需要你做任何的工作。
$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:
$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():
$product = $query->getOneOrNullResult();
更多Doctrine’s Query Builder的信息請閱讀Query Builder。
使用DQL查詢對象
不愛使用QueryBuilder,你還可以直接使用DQL查詢:
$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類的聲明:
// 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類。
$ php app/console doctrine:generate:entities AppBundle
下面,添加一個新方法findAllOrderedByName() 到新生成的repository類。該方法將查詢所有的Product實體,並按照字符順序排序。
// 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管理。
你就可以像使用默認的方法一樣使用這個新定義的方法了:
$em = $this->getDoctrine()->getManager(); $products = $em->getRepository('AppBundle:Product') ->findAllOrderedByName();
當使用一個自定義的repository類時,你依然可以訪問原有的默認查找方法,比如find() 和findAll()等。
實體的關系/關聯
假設你應用程序中的產品屬於一確定的分類。這時你需要一個分類對象和一種把Product和Category對象聯系在一起的方式。首先我們創建Category實體,我們最終要通過Doctrine來對其進行持久化,所以我們這裡讓Doctrine來幫我們創建這個類。
$ php app/console doctrine:generate:entity \ --entity="AppBundle:Category" \ --fields="name:string(255)"
該命令行為你生成一個Category實體,包含id字段和name字段以及相關的getter和setter方法。
關系映射
關聯Category和Product兩個實體,首先在Category類中創建一個products屬性:
// 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類:
// 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方法。
$ 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內的代碼如何處理:
// ... 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
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的時候。這就是延遲加載。
你也可以從其它方向進行查詢:
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返回一個代理對象來代替真正的對象:
$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類中:
// 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對象信息了。
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方式)
/** * @ORM\Entity() * @ORM\HasLifecycleCallbacks() */ class Product { // ... }
現在你可以告訴Doctrine在任何可用的生命周期事件上來執行一個方法了。比如,假設你想在一個新的實體第一次被創建時設置創建日期列(created)為當前日期。
// 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圍繞著一個簡單的概念發展而來,但是它不可思議的強大。允許你創建復雜的查詢和訂閱事件,通過訂閱事件你可以在整個持久化過程中執行一些不同的行為。