Дата обновления перевода: 2020-01-19

Базы данных и Doctrine ORM

Symfony предоставляет все инструменты, которые вам нужны для использования баз данных в вашем приложении благодаря Doctrine, лучшему набору PHP библиотек для работы с базами данных. Эти инструменты поддерживают реляционные базы данных такие как MySQL и PostgreSQL, а также NoSQL базы данных такие как MonogDB

Базы данных - это широкая тема, поэтому документация разделена на три статьи:

  • Эта статья объясняет рекомендованый способ работы с реляционными базами данных в приложениях Symfony;
  • Прочитайте о DBAL если вам нужен низкоуровневый доступ для выполнения напрямую SQL запросов в реляционные базы данных (похоже на PDO в PHP);
  • Прочитайте документацию DoctrineMongoDBBundle if you are working with MongoDB databases.

Установка Doctrine

Сначала установите поддержку Doctrine через orm Symfony pack, вместе с MakerBundle, которая поможет генерировать код:

1
2
$ composer require symfony/orm-pack
$ composer require --dev symfony/maker-bundle

Конфигурация базы данных

Информация соединения DB хранится в переменной окружения DATABASE_URL. Для разработки вы можете найти и установить её внутри .env:

1
2
3
4
5
6
7
# .env (или переопределите DATABASE_URL в .env.local чтобы не добавлять ваши изменения в репозиторий)

# настройте эту строчку!
DATABASE_URL="mysql://db_user:[email protected]:3306/db_name"

# для использования sqlite:
# DATABASE_URL="sqlite:///%kernel.project_dir%/var/app.db"

Caution

Если имя пользователя, пароль, хост или название базы данных содержат один из символов, которые считаются специальными в URI (такие как +, @, $, #, /, :, *, !), вы должны их экранировать. См. RFC 3986 для полного списка зарезервированных символов или используйте функцию urlencode для их экранирования. В этом случае вам нужно удалить префикс resolve: в config/packages/doctrine.yaml для избежания ошибок: url: '%env(resolve:DATABASE_URL)%'

Теперь, когда ваши параметры соединения настроены, Doctrine может создать для вас DB db_name:

1
$ php bin/console doctrine:database:create

Существует больше опций в config/packages/doctrine.yaml, которые вы можете настроить, включая вашу server_version (например, 5.7, если вы используете MySQL 5.7), которые могут повлиять на то, как функционирует Doctrine.

Tip

Существует много других команд Doctrine. Запустите php bin/console list doctrine, чтобы увидеть полный список.

Создание класса сущности (Entity)

Предположим, что вы создаёте приложение, в котором необходимо отображать товары. Даже не задумываясь о Doctrine или базах данных, вы уже знаете, что

вам необходим объект Product для представления этих товаров.

Используйте команду make:entity, чтобы создать этот класс и все поля, которые вам нужны. Команда задаст несколько вопросов - ответьте на них как в примере ниже:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
$ php bin/console make:entity

Class name of the entity to create or update:
> Product

New property name (press <return> to stop adding fields):
> name

Field type (enter ? to see all types) [string]:
> string

Field length [255]:
> 255

Can this field be null in the database (nullable) (yes/no) [no]:
> no

New property name (press <return> to stop adding fields):
> price

Field type (enter ? to see all types) [string]:
> integer

Can this field be null in the database (nullable) (yes/no) [no]:
> no

New property name (press <return> to stop adding fields):
>
(press enter again to finish)

New in version 1.3: Интерактивное поведение команды make:entity добавилось в MakerBundle 1.3.

Теперь у вас есть новый файл src/Entity/Product.php:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// src/Entity/Product.php
namespace App\Entity;

use Doctrine\ORM\Mapping as ORM;

/**
 * @ORM\Entity(repositoryClass="App\Repository\ProductRepository")
 */
class Product
{
    /**
     * @ORM\Id
     * @ORM\GeneratedValue
     * @ORM\Column(type="integer")
     */
    private $id;

    /**
     * @ORM\Column(type="string", length=255)
     */
    private $name;

    /**
     * @ORM\Column(type="integer")
     */
    private $price;

    public function getId()
    {
        return $this->id;
    }

    // ... getter and setter methods
}

Note

Удивлены почему цена это целое число? Не переживайте: это лишь пример. Но, если хранить цены как целые числа (например 100 = $1) можно избежать проблем с округлением.

Caution

There is a лимит в 767 байт для индекса, когда используются таблицы InnoDB в MySQL 5.6 или более ранние версии. Строковые колонки с длиной 255 символов и кодировкой utf8mb4 превышают этот лимит. Это значит, что любая колонка типа string и unique=true должна иметь максимальную length 190. Иначе отобразится ошибка: "[PDOException] SQLSTATE[42000]: Syntax error or access violation: 1071 Specified key was too long; max key length is 767 bytes".

Этот класс называется "сущность" (Entity), И вскоре вы сможете сохранять и запрашивать объекты Product в таблице product в вашей базе данных. Каждое свойство в сущности Product может быть связано с колонкой в этой таблице. Это обычно делается аннотациями: комментарии @ORM\..., которые вы видите над каждым свойством:

_images/mapping_single_entity.png

Команда make:entity - это инструмент упрощающий жизнь. Но это ваш код: добавляйте/удаляйте поля, добавляйте/удаляйте методы или обновляйте конфигурацию.

Doctrine поддержвивет множество типов полей, каждое со своими настройками. Весь список можно посмотреть в Doctrine's Mapping Types documentation. Если вы хотите использовать XML вместо аннотаций, добавьте type: xml и dir: '%kernel.project_dir%/config/doctrine' к маппингу сущностей в вашем файле config/packages/doctrine.yaml`.

Caution

Будьте осторожны и не используйте зарезервированые ключевые слова SQL для названия таблиц или столбцов (например, GROUP or USER). Смотрите документацю Doctrine Зарезериврованные ключевые слова SQL для деталей или как экранировать их. Или измените название таблицы @ORM\Table(name="groups") над классом или настройте название столбца в настройке name="group_name".

Миграции: Создание таблиц / схемы базы данных

Класс Product полностью сконфигурирован и готов к сохранению в таблицу product. Если вы только что создали класс, ваша база данных ещё не имеет таблицы product. Чтобы добавить её, можете использовать предустановленную DoctrineMigrationsBundle:

1
$ php bin/console make:migration

Если всё сработало, то вы должны увидеть что-то вроде:

SUCCESS!

Next: Review the new migration "src/Migrations/Version20180207231217.php" Then: Run the migration with php bin/console doctrine:migrations:migrate

Если вы откроете этот файл, то увидите, что он содержит SQL, необходимый для обновленя вашей DB! Чтобы запустить этот SQL, выполните ваши миграции:

1
$ php bin/console doctrine:migrations:migrate

Эта команда выполняет все файлы миграции, которые ещё не были запущены в вашей базе данных. Вы должны запускать эту команду в production, когда будете разворачивать приложение, чтобы держать вашу production базу данных обновлённой.

Миграции и добавление дополнительных полей

Но что, если вам нужно добавить новое свойство поля в Product, например, description`? Вы можете отредактировать класс и добавить новое свойство. Но можете также запустить снова make:entity:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
$ php bin/console make:entity

Class name of the entity to create or update
> Product

New property name (press <return> to stop adding fields):
> description

Field type (enter ? to see all types) [string]:
> text

Can this field be null in the database (nullable) (yes/no) [no]:
> no

New property name (press <return> to stop adding fields):
>
(press enter again to finish)

Это также добавит новое свойство description и методы getDescription() и setDescription():

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
// src/Entity/Product.php
// ...

class Product
{
    // ...

+     /**
+      * @ORM\Column(type="text")
+      */
+     private $description;

    // getDescription() & setDescription() were also added
}

Новое свойство связано с базой данных, но оно ещё не существует в таблице product. Сгенерируйте новую миграцию:

1
$ php bin/console make:migration

В этот раз SQL в сгенерированном файле будет выглядеть так:

1
ALTER TABLE product ADD description LONGTEXT NOT NULL

Система миграций умная. Она сравнивает все ваши сущности с текущим состоянием базы данных и генерирует SQL необходимый для их синхронизации! Как и раньше, примените ваши миграции:

1
$ php bin/console doctrine:migrations:migrate

Это выполнит только один новый файл миграции, так как DoctrineMigrationsBundle знает, что первая миграция уже была выполнена ранее. За кулисами, она автоматически управляет таблицей migration_versions, чтобы следить за этим.

Каждый раз, когда вы вносите изменения в свою схему, запускайте эти две команды, чтобы сгенерировать миграцию и потом выполнить её. Не забудьте коммитить файлы миграции и запускать их выполнение при развёртывании.

Tip

Если вы предпочитаете добавлять новые свойства вручную, команда make:entity может генерировать методы геттеров и сеттеров для вас:

1
$ php bin/console make:entity --regenerate

Если мы делаете изменения и хотите перегенерировать все методы геттеров/сеттеров, также укажите --overwrite.

Сохранение объектов в базе данных

Пора сохранить объект Product в базу данных! Давайте создадим новый контроллер для экспериментов:

1
$ php bin/console make:controller ProductController

Внутри контроллера, вы можете создать новый объект Product, установить данные в нём и сохранить его!:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// src/Controller/ProductController.php
namespace App\Controller;

// ...
use App\Entity\Product;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\HttpFoundation\Response;

class ProductController extends AbstractController
{
    /**
     * @Route("/product", name="create_product")
     */
    public function createProduct(): Response
    {
        // you can fetch the EntityManager via $this->getDoctrine()
        // or you can add an argument to the action: createProduct(EntityManagerInterface $entityManager)
        $entityManager = $this->getDoctrine()->getManager();

        $product = new Product();
        $product->setName('Keyboard');
        $product->setPrice(1999);
        $product->setDescription('Ergonomic and stylish!');

        // tell Doctrine you want to (eventually) save the Product (no queries yet)
        $entityManager->persist($product);

        // actually executes the queries (i.e. the INSERT query)
        $entityManager->flush();

        return new Response('Saved new product with id '.$product->getId());
    }
}

Попробуйте!

Поздравляем! Вы только что создали вашу первую строку в таблице product. Чтобы доказать это, вы можете запросить DB напрямую:

1
2
3
4
$ php bin/console doctrine:query:sql 'SELECT * FROM product'

# в системах Windows, не использующих Powershell, запустите эту команду:
# php bin/console doctrine:query:sql "SELECT * FROM product"

Рассмотрим предыдущий пример более детально:

  • Строчка 18 Метод $this->getDoctrine()->getManager() получает объект Doctrine менеджер сущностей (entity manager), который является самым важным объектом Doctrine. Он отвечает за сохранение в базу данных, и получение объектов из базы данных.
  • Строxки 20-23 В этой части вы создаёте объект $product и работаете с ним, как и с любым другим обычным PHP-объектом.
  • Строчка 26 Вызов persist($product) сообщает Doctrine, чтобы он "управлял" объектом $product. Это не создает запрос в базу данных.
  • Строка 29 Когда вызывается метод flush(), Doctrine просматривает все объекты, которыми она управляет, чтобы узнать, нужно ли их сохранять в базу данных. В этом примере, объект $product не существует в базе данных, так что менеджер сущностей выполняет запрос INSERT, создавая новую строку в таблице product.

Note

Если вызов flush() не удается, то вызывается исключение Doctrine\ORM\ORMException. См. Транзакции и параллелизм.

И для создания, и для обновления объектов, рабочий процесс всегда одинаков: Doctrine достаточно умна для того, чтобы знать, что делать с вашей сущностью: INSERT или UPDATE.

Validating Objects

Валидатор Symfony использует метаданные Doctrine для осуществления базовых заданий валидации:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// src/Controller/ProductController.php
namespace App\Controller;

use App\Entity\Product;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Validator\Validator\ValidatorInterface;
// ...

class ProductController extends AbstractController
{
    /**
     * @Route("/product", name="create_product")
     */
    public function createProduct(ValidatorInterface $validator): Response
    {
        $product = new Product();
        // This will trigger an error: the column isn't nullable in the database
        $product->setName(null);
        // This will trigger a type mismatch error: an integer is expected
        $product->setPrice('1999');

        // ...

        $errors = $validator->validate($product);
        if (count($errors) > 0) {
            return new Response((string) $errors, 400);
        }

        // ...
    }
}

Несмотря на то, что сущность Product не объявляет явной конфигурации валидации, Symfony просматривает настройки Doctrine для применения некоторых правил валидации. Например, так как свойство name не может быть null в базе данных, то ограничение NotNull автоматически добавляется к свойству (если оно уже не содержало это ограничение).

Следующая таблица описывает связь между метаданными Doctrine и соответствующими ограничениями валидации, которые автоматически добавляются Symfony:

Аттрибут Doctrine Ограничение валидации Заметки
nullable=false NotNull Требует установки компонента PropertyInfo
type Type Требует установки компонента PropertyInfo
unique=true UniqueEntity  
length Length  

Так как компонент Form также как и API Platform внутри себя используют компонент Validator, все ваши формы и web API будут также автоматически получать пользу от этих автоматических ограничений валидации.

Автоматическая валидация - это удобно и увеличивает продуктивность, но она не заменяет полностью настройку валидации. Вам всё ещё нужно добавить несколько ограничений валидации, чтобы убедиться, что данные, предоставляемые пользователем, корректны.

Извлечение объектов из базы данных

Извлечение объекта обратно из DB ещё проще. Представьте, что вы хотите перейти в /product/1, чтобы увидеть ваш новый товар:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// src/Controller/ProductController.php
// ...

/**
 * @Route("/product/{id}", name="product_show")
 */
public function show($id)
{
    $product = $this->getDoctrine()
        ->getRepository(Product::class)
        ->find($id);

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

    return new Response('Check out this great product: '.$product->getName());

    // or render a template
    // in the template, print things with {{ product.name }}
    // return $this->render('product/show.html.twig', ['product' => $product]);
}

Также можно использовать ProductRepository с autowiring Symfony и внедрить его через dependency injection container:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
// src/Controller/ProductController.php
// ...
use App\Repository\ProductRepository;

/**
 * @Route("/product/{id}", name="product_show")
 */
public function show($id, ProductRepository $productRepository)
{
    $product = $productRepository
        ->find($id);

    // ...
}

Попробуйте!

Когда вы запрашиваете определённый тип объекта, вы всегда используете то, что известно, как его "repository". Вы можете думать о repository, как о PHP-классе, единственной работой которого является помогать вам извлекать сущности определённого класса.

Когда у вас есть объект repository, у вас появляется множество helper-методов:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
$repository = $this->getDoctrine()->getRepository(Product::class);

// look for a single Product by its primary key (usually "id")
$product = $repository->find($id);

// look for a single Product by name
$product = $repository->findOneBy(['name' => 'Keyboard']);
// or find by name and price
$product = $repository->findOneBy([
    'name' => 'Keyboard',
    'price' => 1999,
]);

// look for multiple Product objects matching the name, ordered by price
$products = $repository->findBy(
    ['name' => 'Keyboard'],
    ['price' => 'ASC']
);

// look for *all* Product objects
$products = $repository->findAll();

Вы можете также добавлять пользовательские методы для более сложных запросов! Больше об этом вы узнаете позже, в разделе Querying for Objects: The Repository.

Tip

При отображении HTML-страницы, панель инструментов веб-отладки внизу страницы отобразит количество запросов, и время за которое они были выполнены:

_images/doctrine_web_debug_toolbar.png

Если количество запросов в DB слишком велико, иконка станет жёлтой, чтобы показать, что что-то может быть не так. Нажмите на иконку, чтобы открыть Symfony Profiler и посмотрите, какие именно запросы были выполнены. Если вы не видите панели инструментов веб-отладки, установите profiler Symfony pack запустив команду: composer require --dev symfony/profiler-pack.

Автоматическое извлечение объектов (ParamConverter)

Во многих случаях, вы можете использовать SensioFrameworkExtraBundle, чтобы запрос был сделан за вас автоматически! Для начала, установите пакет, если у вас его нет:

1
$ composer require sensio/framework-extra-bundle

Теперь, упростите ваш контроллер:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
// src/Controller/ProductController.php
use App\Entity\Product;

/**
 * @Route("/product/{id}", name="product_show")
 */
public function show(Product $product)
{
    // use the Product!
    // ...
}

Вот и всё! Пакет использует {id} из route, чтобы запросить Product по столбцу id. Если он не найден, генерируется страница 404.

Существует еще множество опций, которые вы можете использовать. Прочтите больше о ParamConverter.

Обновление объекта

Когда вы получили объект из Doctrine, можно взаимодействовать с ним также как и с любым другим PHP-объектом:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
/**
 * @Route("/product/edit/{id}")
 */
public function update($id)
{
    $entityManager = $this->getDoctrine()->getManager();
    $product = $entityManager->getRepository(Product::class)->find($id);

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

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

    return $this->redirectToRoute('product_show', [
        'id' => $product->getId()
    ]);
}

Используя Doctrine для изменения объекта нужно сделать три шага:

  1. получить объект из Doctrine;
  2. измененть объект;
  3. вызвать flush() в менеджере сущностей.

Вы можете вызвать $entityManager->persist($product), но в этом нет необходимости: Doctrine уже "наблюдает" за вашим объектом на предмет изменений.

Удаление объекта

Удаление объекта очень похоже, но требует вызова метода remove() в менеджере сущностей:

1
2
$entityManager->remove($product);
$entityManager->flush();

Как вы и могли ожидать, метод remove() уведомляет Doctrine о том, что вы хотите удалить указанный объект из базы данных. Тем не менее, запрос DELETE не выполняется до тех пор, пока не вызван метод flush().

Запрашивание объектов: Repository

Вы уже видели, как объект repository позволяет вам выполнять базовые запросы без каких-либо усилий:

1
2
3
4
// from inside a controller
$repository = $this->getDoctrine()->getRepository(Product::class);

$product = $repository->find($id);

Но что, если вам нужен более сложный запрос? Когда вы сгенерировали свою сущность с помощью make:entity, команда также сгенерировала класс ProductRepository:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
// src/Repository/ProductRepository.php
namespace App\Repository;

use App\Entity\Product;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Common\Persistence\ManagerRegistry;

class ProductRepository extends ServiceEntityRepository
{
    public function __construct(ManagerRegistry $registry)
    {
        parent::__construct($registry, Product::class);
    }
}

Когда вы извлекаете ваше хранилище (т.е. ->getRepository(Product::class)), оно на самом деле является экземпляром этого объекта! Это так из-за конфигурации repositoryClass, которая была создана поверх вашего класса сушности.

Представьте, что вы хотите получить все объекты Product с ценой, больше, чем заданная. Добавьте для этого в ваше хранилище новый метод:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// src/Repository/ProductRepository.php

// ...
class ProductRepository extends ServiceEntityRepository
{
    public function __construct(ManagerRegistry $registry)
    {
        parent::__construct($registry, Product::class);
    }

    /**
     * @return Product[]
     */
    public function findAllGreaterThanPrice($price): array
    {
        $entityManager = $this->getEntityManager();

        $query = $entityManager->createQuery(
            'SELECT p
            FROM App\Entity\Product p
            WHERE p.price > :price
            ORDER BY p.price ASC'
        )->setParameter('price', $price);

        // returns an array of Product objects
        return $query->getResult();
    }
}

Строка передаваемая в createQuery() может показаться похожей на SQL, но это Doctrine Query Language. Это позволяет вам создавать запросы используя популярный язык запросов, но ссылаться вместо таблиц на PHP-объекты (например в выражении FROM).

Теперь, вы можете вызать этот метод в repository:

1
2
3
4
5
6
7
8
// from inside a controller
$minPrice = 1000;

$products = $this->getDoctrine()
    ->getRepository(Product::class)
    ->findAllGreaterThanPrice($minPrice);

// ...

См. Injecting Services/Config into a Service, чтобы узнать как внедрить repository в любой сервис.

Выполнение запросов с конструктором запросов Query Builder

Doctrine также предоставляет Query Builder, объектно-ориентированный способ писать запросы. Рекомендуется использовать его, когда запросы создаются динамически (то есть, базируясь на условной логике в PHP):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// src/Repository/ProductRepository.php

// ...
public function findAllGreaterThanPrice($price, $includeUnavailableProducts = false): array
{
    // automatically knows to select Products
    // the "p" is an alias you'll use in the rest of the query
    $qb = $this->createQueryBuilder('p')
        ->where('p.price > :price')
        ->setParameter('price', $price)
        ->orderBy('p.price', 'ASC');

    if (!$includeUnavailableProducts) {
        $qb->andWhere('p.available = TRUE')
    }

    $query = $qb->getQuery();

    return $query->execute();

    // to get just one result:
    // $product = $query->setMaxResults(1)->getOneOrNullResult();
}

Запросы с помощью SQL

В дополнение, если необходимо, вы можете писать запросы напрямую с SQL:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
// src/Repository/ProductRepository.php

// ...
public function findAllGreaterThanPrice($price): array
{
    $conn = $this->getEntityManager()->getConnection();

    $sql = '
        SELECT * FROM product p
        WHERE p.price > :price
        ORDER BY p.price ASC
        ';
    $stmt = $conn->prepare($sql);
    $stmt->execute(['price' => $price]);

    // returns an array of arrays (i.e. a raw data set)
    return $stmt->fetchAll();
}

С SQL, вы получите на выходе сырые данные, а не объекты (кроме случаев, когда вы используете функциональность NativeQuery).

Отношения и ассоциации

Doctrine предоставляет все необходимые вам функции, чтобы управлять отношениями DB (также известными, как ассоциации), включая отношения ManyToOne, OneToMany, OneToOne и ManyToMany.

Чтобы узнать больше, см. How to Work with Doctrine Associations / Relations.

Тестирование DB

Читайте статью о тестировании кода, который взаимодействует с DB.

Расширения Doctrine (Timestampable, Translatable, etc.)

Сообщество Doctrine создало расширения для удовлетворения частых потребностей вроде "установить значение свойства createdAt автоматически при создании новой сущности". Читайте делальнее о доступных расширениях Doctrine и используйте StofDoctrineExtensionsBundle для из интеграции в ваше приложение.

Эта документация является переводом официальной документации Symfony и предоставляется по свободной лицензии CC BY-SA 3.0.