# 快速入门

对于任何应用程序来说，一个最常见和最具挑战的任务，就是从数据库中读取和持久化数据信息。在 Ecshopx 中我们采用 Doctrine ORM 替代 Laravel 自带的 Eloquent ORM ，前者更灵活，且自带 [Repository 模式](https://espier.gitbook.io/espier/kuang-jia/jia-gou-si-xiang/architecture-repository)。本节将为大家介绍如何使用 Doctrine ORM。

## 几个概念

### 什么是 Doctrine ORM ？

Doctrine ORM 是 PHP 7.1+ 的对象关系映射器（ORM），可以将PHP对象持久化到数据库中。它以Data Mapper模式为核心，旨在将您的业务逻辑与数据库中的持久化完全分离。

### 什么是 Entities ？

实体是简单的PHP对象，其含可持久化的属性，可持久属性是实体的一个实例变量，通过Doctrine的数据映射功能可以将其保存到数据库中或从数据库中检索出来。实体类不需要扩展任何抽象基类或接口。

实体类不能为final，尽管它可以包含final方法。

### 什么是 DQL ？

DQL 是 Doctrine Query Language 的缩写，代表 Doctrine 查询语言，并且是对象查询语言（Object Query Language）的派生类，它与 Hibernate 查询语言（HQL）或Java持久化查询语言（JPQL）非常相似。

{% hint style="info" %}
对于初学者来说，常见的错误是将DQL误认为只是某种形式的SQL，因此尝试在查询中使用表名和列名或将任意表连接在一起。您需要考虑DQL作为对象模型而不是关系模式的查询语言。
{% endhint %}

## 定义数据库 Schema

在 ECOS 中可以在 dbschema 文件夹下，使用数组定义数据库 Schema，laravel 中可以书写数据库迁移（database migration）文件，而在 ecshopx 中，则需要定义 Doctrine ORM 的实体类来完成数据库 Schema定义。

我们从最简单的实体 `Product` 开始。创建一个 `src/DemoBundle/Entities/Product.php`包含 Product 实体定义的文件：

```php
<?php
// src/DemoBundle/Entities/Product.php
class Product
{
    /**
     * @var int
     */
    private $id;
    /**
     * @var string
     */
    private $name;
}
```

创建实体类时，所有字段都应为 `private`。 这个类仅仅是一个普通的 php 类，还不能称之为 Entity 类。我们需要通过为这个类添加注解使之成为 Entity 类。

```php
<?php
// src/DemoBundle/Entities/Product.php
use Doctrine\ORM\Mapping as ORM;

/**
 * @ORM\Entity
 * @ORM\Table(name="products")
 */
class Product
{
    /** 
     * @ORM\Id
     * @ORM\Column(type="integer")
     * @ORM\GeneratedValue
     */
    protected $id;
    /** 
     * @ORM\Column(type="string") 
     */
    protected $name;
}
```

在添加完注解之后，可以通过以下命令，生成 getter 和 setter 方法：

```
php artisan doctrine:generate:entities --filter=DemoBundle
```

生成之后，一个实体类就完整了，完整的实体类如下：

```php
// src/DemoBundle/Entities/Product.php
<?php
namespace DemoBundle\Entities;

use Doctrine\ORM\Mapping as ORM;
/**
 * @ORM\Entity
 * @ORM\Table(name="products")
 */
class Product
{
    /** 
     * @ORM\Id
     * @ORM\Column(type="integer")
     * @ORM\GeneratedValue
     */
    protected $id;
    /** 
     * @ORM\Column(type="string") 
     */
    protected $name;

    /**
     * Get id.
     *
     * @return int
     */
    public function getId()
    {
        return $this->id;
    }

    /**
     * Set name.
     *
     * @param string $name
     *
     * @return Product
     */
    public function setName($name)
    {
        $this->name = $name;

        return $this;
    }

    /**
     * Get name.
     *
     * @return string
     */
    public function getName()
    {
        return $this->name;
    }
}
```

## 生成数据表

一个实体类定义了一张数据表，如何将数据表同步到数据库呢？ 首先需要先运行：

```
php artisan doctrine:migrations:diff
```

运行以上命令成功后，会生成数据库迁移文件：

```
Generated new migration class to "Version20191225161708" from schema differences.
```

文件的路径在`database/migrations/Version20191225161708.php` 生成之后，即可运行一下命令，将数据表写入到数据库：

```
 php artisan doctrine:migrations:migrate
```

## 持久化对象到数据库

现在我们有了Product实体和与之映射的product数据库表。接下里我们演示如何把数据持久化到数据库里。 为了方便演示，我们采用测试用例的方式来演示此功能：

```php
//src/DemoBundle/Tests/ProductTest.php
<?php
namespace DemoBundle\Tests;

use EspierBundle\Services\TestBaseService;
use DemoBundle\Entities\Product;

class RepositoryFactoryTest extends TestBaseService
{
    /**
     * A basic test example.
     *
     * @return void
     */
    public function testCreateProduct()
    {
        $product = new Product();
        $product->setName("EcshopX");

        //这一行取出了Doctrine的 entity manager 对象，它负责处理数据库的持久化（写入）和取出对象的过程。
        $em = app('registry')->getManager('default');

        // 告诉Doctrine你希望（最终）存储Product对象（还没有语句执行）
        $em->persist($product);

        // 真正执行语句（如，INSERT 查询）
        $em->flush();

        $this->assertNotNull($product->getId());
    }
}
```

当 flush() 方法被调用时，Doctrine会遍历它管理的所有对象以确定是否需要被持久化到数据库。本例中， $product 对象的数据在库中并不存在，因此entity manager要执行 INSERT 请求，在 product 表中创建一个新行。

## 从数据库中获取对象

从数据库中取回对象就更简单了：

```php
//src/DemoBundle/Tests/ProductTest.php
public function testGetProduct()
{
    $productId = 1;
    //这一行取出了Doctrine的 entity manager 对象，它负责处理数据库的持久化（写入）和取出对象的过程。
    $em = app('registry')->getManager('default');

    $productRepository = $em->getRepository(Product::class);

    $product = $productRepository->find($productId);


    $this->assertEquals($productId, $product->getId());
}
```

当你要查询某个特定类型的对象时，你总是要使用它的”respository”。你可以认为Respository是一个PHP类，它的唯一工作就是帮助你从那个特定的类中取出entity。对于一个entity类，要访问其宝库，通过：

```php
app('registry')->getManager('default')->getRepository(Product::class);
```

一旦有了Repository对象，你就可以访问它的全部有用的方法了。

```php
$repository = app('registry')->getManager('default')->getRepository(Product::class);
 // 通过主键（通常是id）查询一件产品
$product = $repository->find($productId);

// 动态方法名称，基于字段的值来找到一件产品
$product = $repository->findOneById($productId);
$product = $repository->findOneByName('Keyboard');

// 动态方法名称，基于字段值来找出一组产品
$products = $repository->findByPrice(19.99);

// find *all* products / 查出 *全部* 产品
$products = $repository->findAll();
```

你也可以有效利用 `findBy` 和 `findOneBy` 方法，基于多个条件来轻松获取对象：

```php
$repository = app('registry')->getManager('default')->getRepository(Product::class);

// 查询一件产品，要匹配给定的名称和价格
$product = $repository->findOneBy(
    array('name' => 'Keyboard', 'price' => 19.99)
);

// 查询多件产品，要匹配给定的名称和价格
$products = $repository->findBy(
    array('name' => 'Keyboard'),
    array('price' => 'ASC')
);
```

## 对象更新

一旦从Doctrine中获取了一个对象，更新它就很容易了：

```php
public function testUpdateProduct()
{
    $productId = 1;
    //这一行取出了Doctrine的 entity manager 对象，它负责处理数据库的持久化（写入）和取出对象的过程。
    $em = app('registry')->getManager('default');
    $productRepository = $em->getRepository(Product::class);
    $product = $productRepository->find($productId);

    $newName = "Ecshopx 2.0";

    $product->setName($newName);
    $em->flush();

    //判断数据是否更新成功
    $newProduct = $productRepository->find($productId);
    $this->assertEquals($newName, $newProduct->getName());
}
```

更新一个对象包括三步：

* 1、从Doctrine中取出对象;
* 2、修改对象;
* 3、调用entity manager的 flush() 方法。

注意调用 $em->persist($product) 是不必要的。回想一下，这个方法只是告诉Doctrine去管理或者“观察” $product 对象。此处，因为你已经取到了 $product 对象了，它已经被管理了。

## 删除对象

删除一个对象十分类似，但需要从entity manager调用 remove() 方法：

```php
$em->remove($product);
$em->flush();
```

你可能已经预期，`remove()` 方法通知Doctrine你想从数据库中删除指定的entity。真正的 DELETE 查询不会被真正执行，直到 `flush()` 方法被调用。

## 对象查询

你已经看到 repository 对象是如何让你执行一些基本查询而毋须做任何工作了：

```php
$repository = app('registry')->getManager('default')->getRepository(Product::class);;

$product = $repository->find($productId);
$product = $repository->findOneByName('Keyboard');
```

当然，Doctrine 也允许你使用Doctrine Query Language(DQL)来写一些复杂的查询，DQL类似于SQL，只是它用于查询一个或者多个entity类的对象(如 product)，而SQL则是查询一个数据表中的行（如 product ）。

在Doctrine中查询时，你有两个主要选择：

* 编写纯正的Doctrine查询（DQL）&#x20;
* 使用Doctrine的Query Builder

## 使用DQL进行对象查询

假设你要查询价格高于 19.99 的产品，并且按价格从低到高排列。你可以使用DQL，Doctrine中类似原生SQL的语法，来构造一个用于此场景的查询:

```php
$em = $this->getDoctrine()->getManager();
$query = $em->createQuery(
    'SELECT p
    FROM DemoBundle\Entities\Product p
    WHERE p.price > :price
    ORDER BY p.price ASC'
)->setParameter('price', 19.99);

$products = $query->getResult();
```

如果你习惯了写SQL，那么对于DQL也会非常自然。它们之间最大的不同就是你需要就“select PHP对象”来进行思考，而不是数据表的行。正因为如此，你要 从 Product 这个 entity 来select，然后给entity一个 p 的别名。

> 注意 \`setParameter()\`\` 方法。当使用 Doctrine 时，通过“占位符”来设置任意的外部值（上面例子的 :price），是一个好办法，因为它可以防止SQL注入攻击。

getResult() 方法返回一个结果数组。要得到一个结果，可以使用getSingleResult()（这个方法在没有结果时会抛出一个异常）或者 getOneOrNullResult() ：

```php
$product = $query->setMaxResults(1)->getOneOrNullResult();
```

DQL语法强大到令人难以置信，允许轻松地在entity之间进行join（稍后会覆盖relations）和group等。参考 [Doctrine Query Language](http://docs.doctrine-project.org/projects/doctrine-orm/en/latest/reference/dql-doctrine-query-language.html) 文档以了解更多。

## 使用Doctrine's Query Builder进行对象查询

除了去写DQL，你还可以使用一个非常有用的QueryBuilder对象，来构建查询 SQL。当你的查询取决于动态条件时，这很有用，因为随着你的连接字符串不断增加，DQL代码会越来越难以阅读：

```php
$em = app('registry')->getManager('default');
$productRepository = $em->getRepository(Product::class);

// createQueryBuilder() 自动从 AppBundle:Product 进行 select 并赋予 p 假名
$query = $productRepository->createQueryBuilder('p')
    ->where('p.price > :price')
    ->setParameter('price', '19.99')
    ->orderBy('p.price', 'ASC')
    ->getQuery();

$products = $query->getResult();
//  要得到一个结果：
$product = $query->setMaxResults(1)->getOneOrNullResult();
```

QueryBuilder对象包含了创建查询时的所有必要方法。通过调用getQuery()方法，query builder将返回一个标准的Query对象，可用于取得请求的结果集。

Query Builder更多信息，参考 Doctrine 的 [Query Builder](http://docs.doctrine-project.org/projects/doctrine-orm/en/latest/reference/query-builder.html)文档。
