Symfony2學習筆記之資料庫操作

huidaoli發表於2014-08-06



資料庫和Doctrine
讓我們來面對這個對於任何應用程式來說最為普遍最具挑戰性的任務,從資料庫中讀取和持久化資料資訊。幸運的是,Symfony和Doctrine進行了整合,Doctrine類庫全部目標就是給你一個強大的工具,讓你的工作更加容易。

Doctrine是完全解耦與Symfony的,所以並不一定要使用它。

一個簡單例子:一個產品,我們首先來配置資料庫,建立一個Product物件,持久化它到資料庫並把它讀回來。

首先我們需要建立一個bundle:

$php app/console generate:bundle --namespace=Acme/StoreBundle

 

配置資料庫
在開始之前,首先需要配置資料庫連線資訊。根據慣例,這些資訊通常會配置在app/config/parameters.ini 檔案中。

複製程式碼
;app/config/parameters.ini
[parameters]
    database_driver   = pdo_mysql
    database_host     = localhost
    database_name     = test_project
    database_user     = root
    database_password = password
複製程式碼

將配置資訊定義到parameters.ini檔案中也是一個常用的做法。定義在該檔案中的配置資訊將會被主配置檔案在安裝Doctrine時引用。

複製程式碼
doctrine:
    dbal:
        driver:   %database_driver%
        host:     %database_host%
        dbname:   %database_name%
        user:     %database_user%
        password: %database_password%
複製程式碼

通過把資料庫資訊分離到一個特定的檔案中,你可以很容易的為每個伺服器儲存不同的版本。現在Doctrine知道你的資料庫配置了,你可以用它來建立一個資料庫了。

$php app/console doctrine:database:create

 

建立一個實體類:
假設你建立一個應用程式,其中有些產品需要展示。即時不考慮Doctrine或者資料庫,你也應該知道你需要一個Product物件來表現這些產品。在你的AcmeStoreBundle的Entity目錄下建立一個類。

複製程式碼
// src/Acme/StoreBundle/Entity/Product.php
namespace Acme\StoreBundle\Entity;

class Product
{
    protected $name;

    protected $price;

    protected $description;
}
複製程式碼

這樣的類經常被稱為“Entity",意味著一個基礎類儲存資料。它們簡單來滿足你應用程式的業務需要。不過現在它還不能被儲存到資料庫中,因為現在它只不過還是個簡單的PHP類。一旦你學習了Doctrine背後的概念,你可以讓Doctrine來為你建立實體類。

$php app/console doctrine:generate:entity --entity="AcmeStoreBundle:Product" --fields="name:string(255) price:float description:text"

 


新增對映資訊
Doctrine允許你使用一種更加有趣的方式對資料庫進行操作,而不是隻是獲取基於列表的行到陣列中。Doctrine允許你儲存整個物件到資料庫或者把物件從資料庫中取出。這些都是通過對映PHP類到一個資料庫表,PHP類的屬性對應資料庫表的列來實現的。

因為Doctrine能夠做這些,所以你僅僅只需要建立一個meatdata,或者配置告訴DoctrineProduct類和它的屬性應該如何對映到資料庫。這些metadata可以被定義成各種格式,包括YAML,XML或者通過宣告直接定義到Product類中。

一個bundle只可以接受一種metadata定義格式。比如,不能把YAML定義的metadata和宣告PHP實體類一起混用。

複製程式碼
use Doctrine\ORM\Mapping as ORM;

/**
 * @ORM\Entity
 * @ORM\Table(name="product")
 */
class Product
{
    /**
     * @ORM\Id
     * @ORM\Column(type="integer")
     * @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;
}
複製程式碼

YAML格式metadata定義:

複製程式碼
# src/Acme/StoreBundle/Resources/config/doctrine/Product.orm.yml
Acme\StoreBundle\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
複製程式碼

XML格式metadata定義:

複製程式碼
<!-- src/Acme/StoreBundle/Resources/config/doctrine/Product.orm.xml -->
<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="Acme\StoreBundle\Entity\Product" table="product">
        <id name="id" type="integer" column="id">
            <generator strategy="AUTO" />
        </id>
        <field name="name" column="name" type="string" length="100" />
        <field name="price" column="price" type="decimal" scale="2" />
        <field name="description" column="description" type="text" />
    </entity>
</doctrine-mapping>
複製程式碼

表名稱是可選的,可以忽略;如果忽略將會自動的根據entity類名對應。

如果使用在類中宣告metadata需要首先使用

use Doctrine\ORM\Mapping as ORM;

匯入ORM宣告字首。然後在每個宣告前使用 ORM\ 比如:

@ORM\Column(...);

注意:你的類名稱和屬性不能對映到SQL受保護的關鍵字(比如:group 或者 user)。如果你的實體類名是Group,預設情況下你的表面也將是group,這會引起SQL錯誤。當使用另外的類庫或者程式,它們使用了宣告,你應該把@IgnoreAnnotation宣告新增到該類上來告訴Symfony忽略它們。比如我們要阻止@fn 宣告丟擲異常,可以這樣:

/**
* @IgnoreAnnotation("fn")
*/
class Product

 

生成Getters和Setters
儘管Doctrine現在知道了如何值就花Product物件到資料庫,但是類本身還是無法使用。因為Product僅僅是一個標準的PHP類,你需要建立getter和setter方法(比如getName(),setName())來訪問它的屬性(因為它的屬性是protected),幸運的是Doctrine可以為我們做這些:

$php app/console doctrine:generate:entites Acme/StoreBundle/Entity/Product

該命令可以確認Product類所有的getter和setter都被生成。這是一個安全的命令列,你可以多次執行它,它只會生成那些不存在的getters和setters,而不會替換已有的。

關於doctrine:generate:entities命令
        用它你可以生成getters和setters。
        用它在配置@ORM\Entity(repositoryClass="...")宣告的情況下,生成repository類。
        用它可以為1:n或者n:m生成合適的構造器。
該命令會儲存一個原來Product.php檔案的備份Product.php~。 有些時候可也能夠會造成“不能重新宣告類”錯誤,你可以放心的刪除它,來消除錯誤。當然你沒有必要依賴於該命令列,Doctrine不依賴於程式碼生成,想標準的PHP類,你只需要保證它的protected/private屬性擁有getter和setter方法即可。你也可以為一個bundle或者整個實體名稱空間內的所有已知實體(任何包含Doctrine對映宣告的PHP類)來生成getter和setter:

$php app/console doctrine:generate:entities AcmeStoreBundle
$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/Acme/StoreBundle/Controller/DefaultController.php
use Acme\StoreBundle\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()->getEntityManager();
    $em->persist($product);
    $em->flush();

    return new Response('Created product id '.$product->getId());
}
複製程式碼

事實上,Doctrine瞭解你所有的被管理的實體,當你呼叫flush()方法時,它會計算出所有的變化,並執行最有效的查詢可能。 比如,你要持久化總是為100的產品物件,然後呼叫flush()方法。Doctrine會建立一個唯一的預備語句並重復使用它插入。 這種模式成為Unit of work。


在建立和更新物件是,工作流是相同的。Doctrine提供了一個類庫允許你通過程式設計載入測試資料到你的專案。該類庫為DoctrineFixturesBundle(http://symfony.com/doc/current/bundles/DoctrineFixturesBundle/index.html)


從資料庫中讀取物件
從資料庫中獲取物件更容易,舉個例子,加入你配置了一個路由來基於它的ID顯示特定的product。

複製程式碼
public function showAction($id)
{
       $product = $this->getDoctrine()
                 ->getRepository('AcmeStoreBundle:Product'))
                 ->find($id);
        if(!$product){
             throw $this->createNotFoundException('No product found for id ' .$id);
        }
       //do something,想把$product物件傳遞給一個template等。
}
複製程式碼

當你查詢某個特定的產品是,你總是需要使用它的"respository"。你可以認為Respository是一個PHP類,它的唯一工作就是幫助你從某個特定類哪裡獲取實體。你可以為一個實體物件訪問一個repository物件,如下:

$repository = $this->getDoctrine()
        ->getRepository('AcmeStoreBundle:Product');

其中AcmeStoreBundle:Product是簡潔寫法,你可以在Doctrine中任意使用它來替代實體類的全限定名稱。

Acme\StoreBung\Entity\Product

你一旦有了Repository,你就可以訪問其所有分類的幫助方法了。

複製程式碼
//通過主鍵查詢(一般為"id")
$product=$repository->find($id);

//動態方法名基於列值查詢
$product=$repository->findOneById($id);
$product=$repository->findOneByName('foo');

//查詢所有產品
$products=$repository->findAall();
//基於任意列值查詢一組產品
$products = $repository->findByPrice(19.99);
複製程式碼

你也可以發揮findBy和findOneBy方法的優勢很容易的基於多個條件來獲取物件。

複製程式碼
//按照名字和價格來獲取一個匹配的物件
$product=$repository->findOneBy(array('name'=>'foo','price'=>19.99));

//查詢匹配名字的所有產品並按照價格排序
$products = $repository->findBy(
        array('name'=> 'foo'),
        array('price'=>'ASC')
);
複製程式碼

 


更新物件
一旦你從Doctrine中獲取了一個物件,那麼更新它就變得很容易了。假設你有一個路由對映一個產品id到一個controller的更新行為。

複製程式碼
public function updateAction($id)
{
    $em = $this->getDoctrine()->getEntityManager();
    $product = $em->getRepository('AcmeStoreBundle:Product')->find($id);

    if (!$product) {
        throw $this->createNotFoundException('No product found for id '.$id);
    }

    $product->setName('New product name!');
    $em->flush();

    return $this->redirect($this->generateUrl('homepage'));
}
複製程式碼

更新一個物件包括三步:

       1.從Doctrine取出物件
       2.修改物件
       3.在實體管理者上呼叫flush()方法

注意呼叫 $em->persist($product) 在這裡沒有必要。我們回想一下,呼叫該方法的目的主要是告訴Doctrine來管理或者“watch"$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的查詢建立器。

用DQL查詢物件:
假設你想查詢產品,需要返回價格高於19.99的產品,並且要求按價格從低到高排列。我們可以在相應的Controller裡進行如下操作:

$em = $this->getDoctrine()->getEntityManager();
$query = $em->createQuery(
    'SELECT p FROM AcmeStoreBundle:Product p WHERE p.price > :price ORDER BY p.price ASC'
)->setParameter('price', '19.99');

$products = $query->getResult();

如果你習慣了寫SQL,那麼對於DQL也應該不會感到陌生。它們之間最大的不同就是你需要思考物件,而不是資料庫錶行。正因為如此,所以你從AcmeStoreBundle:Product選擇並給它定義別名p。getResult()方法返回一個結果陣列。如果你只需要一個物件,你可以使用getSingleResult()方法。

$product = $query->getSingleResult();

如果沒有符合要求的結果,getSingleResult()方法會丟擲一個 Doctrine\ORM\NoResultException 異常和如果不只有一個結果返回那麼就會
丟擲一個Doctrine\ORM\NonUniqueResultException 異常。所以,如果你要使用該方法的話,需要把它包裹在一個try-catch塊內,以確保只有
一個結果被返回。

複製程式碼
$query = $em->createQuery('SELECT ....')
    ->setMaxResults(1);

try {
    $product = $query->getSingleResult();
} catch (\Doctrine\Orm\NoResultException $e) {
    $product = null;
}
// ...
複製程式碼

DQL的語法難以置信的強大,允許你很容易的倆和多個實體,分組等進行查詢。

 

設定引數:
注意setParameter()方法,在使用Doctrine時,把任何的外部值設定成佔位符是一個非常好的做法。 
比如上例中 ...WHERE p.price>:price ...
這樣你可以通過呼叫setParameter()方法為price佔位符設定具體值。
->setParameter('price', '19.99')


使用引數而不是直接把具體在插入到查詢字串中是為了放置SQL隱碼攻擊,所以必須始終這麼做。如果你使用了多個引數,你可以使用setParameters()方法一次性設定他們的值。

->setParameters(array(
    'price'=>'19.99',
    'name' =>'foo',
))

 


使用Doctrine的查詢建立器
除了直接編寫查詢以外,你可以使用Doctrine的QueryBuilder來做相同的工作。面前物件介面。如果你使用IDE,你還可以獲取自動編譯檢查的好處。

複製程式碼
$repository = $this->getDoctrine()
    ->getRepository('AcmeStoreBundle:Product');

$query = $repository->createQueryBuilder('p')
    ->where('p.price > :price')
    ->setParameter('price', '19.99')
    ->orderBy('p.price', 'ASC')
    ->getQuery();

$products = $query->getResult();
複製程式碼

QueryBuilder物件包含了建立查詢的所有必須的方法。通過呼叫getQuery()方法,查詢建立器將返回一個標準的Query物件。它跟我們直接寫查詢物件效果相同。

 

自定義Repository類
在上面你已經開始在controller中建立和使用負責的查詢了。為了隔離,比阿育測試和重用這些查詢,一個好的辦法是為你的實體建立一個自定義的repository類並新增相關邏輯查詢方法。要定義repository類,首先需要在你的對映定義中新增repository類的宣告:

在實體類中宣告方式:

複製程式碼
// src/Acme/StoreBundle/Entity/Product.php
namespace Acme\StoreBundle\Entity;

use Doctrine\ORM\Mapping as ORM;

/**
 * @ORM\Entity(repositoryClass="Acme\StoreBundle\Repository\ProductRepository")
 */
class Product
{
    //...
}
複製程式碼

YAML格式:

# src/Acme/StoreBundle/Resources/config/doctrine/Product.orm.yml
Acme\StoreBundle\Entity\Product:
    type: entity
    repositoryClass: Acme\StoreBundle\Repository\ProductRepository
    # ...

XML格式:

複製程式碼
<!-- src/Acme/StoreBundle/Resources/config/doctrine/Product.orm.xml -->
<!-- ... -->
<doctrine-mapping>

    <entity name="Acme\StoreBundle\Entity\Product"
            repository-class="Acme\StoreBundle\Repository\ProductRepository">
            <!-- ... -->
    </entity>
</doctrine-mapping>
複製程式碼

然後通過執行跟之前生成丟失的getter和setter方法同樣的命令列,Doctrine會為你自動生成repository類。

$php app/console doctrine:generate:entities Acme

接下來,新增一個新方法findAllOrderedByName() 到新生成的repository類。該方法將查詢所有的Product實體,並按照字元順序排列。

複製程式碼
// src/Acme/StoreBundle/Repository/ProductRepository.php
namespace Acme\StoreBundle\Repository;

use Doctrine\ORM\EntityRepository;

class ProductRepository extends EntityRepository
{
    public function findAllOrderedByName()
    {
        return $this->getEntityManager()
            ->createQuery('SELECT p FROM AcmeStoreBundle:Product p ORDER BY p.name ASC')
            ->getResult();
    }
}
複製程式碼

注意在Repository類中可以通過$this->getEntityManager()方法類獲取實體管理者。如此一來你就可以像使用預設的方法一樣使用這個新定義的方法了:

$em = $this->getDoctrine()->getEntityManager();
$products = $em->getRepository('AcmeStoreBundle:Product')
            ->findAllOrderedByName();

在使用自定義的repository類時,你依然可以訪問原有的預設查詢方法,比如find() 和findAll()等。


實體關係/關聯
假設你應用程式中的產品屬於一確定的分類。這時你需要一個分類物件和一種把Product和Category物件聯絡在一起的方式。首先我們建立Category實體,我們最終要通過Doctrine來對其進行持久化,所以我們這裡讓Doctrine來幫我們建立這個類。

$php app/console doctrine:generate:entity --entity="AcmeStoreBundle:Category" --fields="name:string(255)"

該命令列為你生成一個Category實體,包含id欄位和name欄位以及相關的getter和setter方法。


關係對映後設資料:
聯絡Category和Product兩個實體,首先在Category類中建立一個products屬性:
Category類中宣告格式:

複製程式碼
// src/Acme/StoreBundle/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();
    }
}
複製程式碼

YAML定義格式:

複製程式碼
# src/Acme/StoreBundle/Resources/config/doctrine/Category.orm.yml
Acme\StoreBundle\Entity\Category:
    type: entity
    # ...
    oneToMany:
        products:
            targetEntity: Product
            mappedBy: category
    # 不要忘記在實體的 __construct() 方法中初始化集合
複製程式碼

首先,因為一個Category物件將關係到多個Product物件,一個products陣列屬性被新增到Category類儲存Product物件。

其次,這裡沒有被做因為Doctrine需要它,但在應用程式中為每一個Category來儲存一個Product陣列非常有用。

程式碼中__construct()方法非常重要,因為Doctrine需要$products熟悉成為一個ArrayCollection物件,它跟陣列非常類似。targetEntity 的值可以使用合法的名稱空間引用任何實體,而不僅僅是定義在同一個類中的實體。 如果要關係一個定義在不同的類或者bundle中的實體則需要輸入完全的名稱空間作為目標實體。


接下來,因為每個Product類可以關聯一個Category物件,所有新增一個$category屬性到Product類:
在Product類宣告中定義:

複製程式碼
// src/Acme/StoreBundle/Entity/Product.php
// ...

class Product
{
    // ...

    /**
     * @ORM\ManyToOne(targetEntity="Category", inversedBy="products")
     * @ORM\JoinColumn(name="category_id", referencedColumnName="id")
     */
    protected $category;
}
複製程式碼

YAML定義格式:

複製程式碼
# src/Acme/StoreBundle/Resources/config/doctrine/Product.orm.yml
Acme\StoreBundle\Entity\Product:
    type: entity
    # ...
    manyToOne:
        category:
            targetEntity: Category
            inversedBy: products
            joinColumn:
                name: category_id
                referencedColumnName: id
複製程式碼

最後,到現在為止,我們新增了兩個新屬性到Category和Product類。現在告訴Doctrine來為它們生成getter和setter方法。

$php app/console doctrine:generate:entities Acme

我們先不看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 Acme\StoreBundle\Entity\Category;
use Acme\StoreBundle\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);
        // relate this product to the category
        $product->setCategory($category);

        $em = $this->getDoctrine()->getEntityManager();
        $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('AcmeStoreBundle:Product')
        ->find($id);

    $categoryName = $product->getCategory()->getName();

    // ...
}
複製程式碼

在這個例子中,你首先基於產品id查詢一個Product物件,接下來當你呼叫$product->getCategory()->getName() 時,Doctrine默默的為你執行了第二次查詢,查詢一個與該產品相關的category,它生成一個$category物件返回給你。

重要的是你很容易的訪問到了product的關聯物件category。但是category的資料並不會被取出來而直到你請求category的時候。這就是延遲載入。你也可以從其它方向進行查詢:

複製程式碼
public function showProductAction($id)
{
    $category = $this->getDoctrine()
        ->getRepository('AcmeStoreBundle:Category')
        ->find($id);

    $products = $category->getProducts();

    // ...
}
複製程式碼

在這種情況下,同樣的事情發生了。你首先查查一個category物件,然後Doctrine製造了第二次查詢來獲取與之相關聯的Product物件們。只有在你呼叫->getProducts()時才會執行一次。 $products變數是一個通過它的category_id的值跟給定的category物件相關聯的所有Product物件的集合。


關係和代理類:
延遲載入成為可能是因為Doctrine返回一個代理物件來代替真正的物件:

複製程式碼
$product = $this->getDoctrine()
     ->getRepository('AcmeStoreBundle:Product')
     ->find($id);

$category = $product->getCategory();

// 輸出結果 "Proxies\AcmeStoreBundleEntityCategoryProxy"
echo get_class($category);
複製程式碼

該代理物件繼承了Category物件,從外表到行為都非常像category物件。通過這個代理物件,Doctrine可以延遲查詢真正的Category物件資料,直到真正需要它時(呼叫$category->getName())。Doctrine生成了代理物件並把它儲存到cache目錄中,儘管你可能從來沒有發現過它。記住它這一點很重要。

我們可以通過join連線來一次性取出product和category資料。這時Doctrine將會返回真正的Category物件,因為不需要延遲載入。


連線相關記錄:
在之前的我們的查詢中,會產生兩次查詢操作,一次是獲取原物件,一次是獲取關聯物件。當然,如果你想一次訪問兩個物件,你可以通過一個join連線來避免二次查詢。把下面的方法新增到ProductRepository類中:

複製程式碼
// src/Acme/StoreBundle/Repository/ProductRepository.php

public function findOneByIdJoinedToCategory($id)
{
    $query = $this->getEntityManager()
        ->createQuery('
            SELECT p, c FROM AcmeStoreBundle: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('AcmeStoreBundle:Product')
        ->findOneByIdJoinedToCategory($id);

    $category = $product->getCategory();

    // ...
}
複製程式碼

 

 

生命週期回撥:
有時候你可能需要在一個實體被建立,更新或者刪除的前後執行一些行為。因為它們回撥的方法處在一個實體不同的生命週期階段,所以這些行為被稱為"生命週期回撥“。如果你用宣告後設資料方式,開啟一個生命週期回撥,需要如下設定:

複製程式碼
/**
 * @ORM\Entity()
 * @ORM\HasLifecycleCallbacks()
 */
class Product
{
    // ...
}
複製程式碼

如果你選擇YAML或者XML格式為你定義對映,則不需要它。現在你可以告訴Doctrine在任何可用的生命週期事件上來執行一個方法了。比如,假設你想在一個新的實體第一次被建立時設定設定建立日期列(created)為當前日期。

宣告式定義:

複製程式碼
/**
* @ORM\PrePersist
*/
public function setCreatedValue()
{
       $this->created = new \DateTime();
}
複製程式碼

YAML格式定義:

# src/Acme/StoreBundle/Resources/config/doctrine/Product.orm.yml
Acme\StoreBundle\Entity\Product:
    type: entity
    # ...
    lifecycleCallbacks:
        prePersist: [ setCreatedValue ]

XML格式定義:

複製程式碼
<!-- src/Acme/StoreBundle/Resources/config/doctrine/Product.orm.xml -->
<!-- ... -->
<doctrine-mapping>

    <entity name="Acme\StoreBundle\Entity\Product">
            <!-- ... -->
            <lifecycle-callbacks>
                <lifecycle-callback type="prePersist" method="setCreatedValue" />
            </lifecycle-callbacks>
    </entity>
</doctrine-mapping>
複製程式碼

現在在實體第一次被儲存時,Doctrine會自動呼叫這個方法使created日期自動設定為當前日期。還有其它生命週期事件可用:

preRemove
postRemove
prePersist
postPersist
preUpdate
postUpdate
postLoad
loadClassMetadata

 

生命週期回撥和事件監聽:
注意到setCreatedValue()方法不需要接收任何引數。這是生命週期回撥通常的做法和慣例。生命週期回撥應該方法簡單,更關注於實體內部傳輸資料。比如設定一個建立/更新欄位,生成一個定量值等。如果你需要一些比較大的行為活動,像執行日誌或者傳送郵件,你應該註冊一個擴充套件類作為事件監聽器或接收器給它賦予訪問所需資源的權利。

 

Doctrine擴充套件:Timestampable, Sluggable
Doctrine非常靈活,許多第三方擴充套件可以使用,讓你很容易在你的實體上執行一些重複和通用的任務。包括Sluggable,Timestampable,Loggable,Translatable 和 Tree。


Doctrine欄位型別參考:
Doctrine配備了大量可用的欄位型別。它們每一個都能對映PHP資料型別到特定的列型別,無論你使用什麼資料庫。下面是Doctrine支援的資料型別:

字串:
         string 短字串
         text 大型字串
數字:
         integer
         smallint
         bigint
         decimal
         float
日期和時間:
         date
         time
         datetime
其它型別:
         boolean
         object(序列化並儲存到CLOB欄位)
         array(序列化並儲存到CLOB欄位)


可選欄位:
每個欄位都有一些可選項。包括type(預設string),name,length,unique 和nullable。
比如:

複製程式碼
/**
 *字串欄位長度為255 不能為空
 * (影響預設值的 "type", "length" 和 *nullable* 可選)
 *
 * @ORM\Column()
 */
protected $name;

/**
 * 字串欄位長度 150儲存到 "email_address" 列
 * 並且有一個唯一索引.
 *
 * @ORM\Column(name="email_address", unique=true, length=150)
 */
protected $email;
複製程式碼

YAML格式:

複製程式碼
fields:
    #字串長度為 255 不能為空l
    # (影響預設值的 "length" 和 *nullable* 可選)
    # type 屬性在yml中是必須定義的
    name:
        type: string

    # 字串長度為150持久化一個 "email_address" 列
    # 並有一個唯一索引.
    email:
        type: string
        column: email_address
        length: 150
        unique: true
複製程式碼

 


控制檯命令:
Doctrine2 ORM繼承官方的多個控制檯命令在doctrine名稱空間下。你可以通過如下命令檢視:

$php app/console

一個可用的命令列列表將會被列印出來。有許多是以doctrine:開頭的。你可通過執行help命令來檢視它們的詳細,比如,檢視doctrine:database:create 任務,則需要執行:

$php app/console help doctrine:database:create

另外一些可用的有趣的命令列任務包括:

doctrine:ensure-production-settings 用來檢視當前還將配置是否對產品有效。這個一般在prod環境下執行:

$php app/console doctrine:ensure-production-settings --env=prod

 

doctrine:mapping:import 允許Doctrine自己檢查一個已有的資料庫並建立對映資訊。

doctrine:query:dql 和 doctrine:query:sql 允許你直接在命令列執行DQL或者SQL查詢。


總結思考:
有了Doctrine,你可以集中精力到你的物件以及怎樣把它應用於你的應用程式中,而不必擔心資料庫持久化。因為Doctrine允許你使用任何的PHP物件儲存你的資料並依靠對映後設資料資訊來聯絡一個物件到特定的資料庫表。

儘管Doctrine圍繞著一個簡單的概念發展而來,但是它不可思議的強大。允許你建立複雜的查詢和訂閱事件,通過訂閱事件你可以在整個持久化過程中執行一些不同的行為。

 

參考URL:http://symfony.com/doc/current/book/doctrine.html

相關文章