Symfony2学习笔记之数据库操作



数据库和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

posted @ 2014-08-06 14:08  huidaoli  阅读(884)  评论(0编辑  收藏  举报