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

Symfony не предоставляет компонента для работы с БД, но предоставляет тесную интеграцию со сторонней библиотекой под названием Doctrine.

Note

Эта статья полностью описывает использование Doctrine ORM. сли вы предпочитаете использовать чистые запросы БД, см. статью "How to Use Doctrine DBAL".

Вы также можете сохранять данные в MongoDB, используя библиотеку Doctrine ODM. См. документацию "DoctrineMongoDBBundle".

Установка Doctrine

Для начала, установите Doctrine, а также MakerBundle, который поможет сгенерировать код:

1
composer require doctrine maker

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

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

1
2
3
4
5
6
7
# .env

# настройте эту строчку!
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 для их шифрования.

Теперь, когда ваши параметры соединения установлены, Doctrine может создать для вас БД 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
$ php bin/console make:entity Product

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
// 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;

    // добавьте ваши собственные поля
}

Этот класс называется "сущность", И вскоре вы сможете сохранять и запрашивать объекты Product в таблице product в вашей базе данных.

Отображение большего количества полей / колонок

Каждое свойство в сущности Product может быть связано с колонкой в таблице product. Добавляя конфигурацию отображения, Doctrine сможет сохранять объект Товар в таблицу product и запрашивать их таблицы product и превращать эти данные в объекты Product:

_images/mapping_single_entity.png

Давайте дадим классу сущности Product ещё три свойства и свяжем их с колоками в БД. Это обычно делается с помощью аннотаций:

  • Annotations
     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
    // src/Entity/Product.php
    // ...
    
    // это утверждение использования необходимо для аннотаций
    use Doctrine\ORM\Mapping as ORM;
    
    class Product
    {
        /**
         * @ORM\Id
         * @ORM\GeneratedValue
         * @ORM\Column(type="integer")
         */
        private $id;
    
        /**
         * @ORM\Column(type="string", length=100)
         */
        private $name;
    
        /**
         * @ORM\Column(type="decimal", scale=2, nullable=true)
         */
        private $price;
    }
    
  • YAML
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    # config/doctrine/Product.orm.yml
    App\Entity\Product:
        type: entity
        id:
            id:
                type: integer
                generator: { strategy: AUTO }
        fields:
            name:
                type: string
                length: 100
            price:
                type: decimal
                scale: 2
                nullable: true
    
  • XML
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    <!-- config/doctrine/Product.orm.xml -->
    <?xml version="1.0" encoding="UTF-8" ?>
    <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="App\Entity\Product">
            <id name="id" type="integer">
                <generator strategy="AUTO" />
            </id>
            <field name="name" type="string" length="100" />
            <field name="price" type="decimal" scale="2" nullable="true" />
        </entity>
    </doctrine-mapping>
    

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

Caution

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

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

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

1
$ php bin/console doctrine:migrations:diff

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

Сгенерирован новый класс миграции в "/path/to/project/doctrine/src/Migrations/Version20171122151511.php" из различий схемы.

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

1
$ php bin/console doctrine:migrations:migrate

Эта команда выполняет все файлы миграции, которые ещё не были запущены в вашей БД.

Миграции и добавление полей

Но что, если вам нужно добавить новое свойство поля в Product, например, description?

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

class Product
{
    // ...

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

Новое свойство отображено, но ещё не существует в таблице product. Не проблема! Просто сгенерируйте миграцию:

1
$ php bin/console doctrine:migrations:diff

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

1
ALTER TABLE product ADD description LONGTEXT NOT NULL

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

1
$ php bin/console doctrine:migrations:migrate

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

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

Создание геттеров и сеттеров

Doctrine теперь знает, как сохранять объект Product в БД. Но сам класс ещё бесполезен. Все свойства - private, поэтому увидеть данные в них невозможно!

По этой причине, вам нужно создать публичные геттеры и сеттеры для всех полей, которые вам нужно изменить снаружи класса. Если вы используете IDE вроде PhpStorm, то он может сгенерировать их за вас. В PhpStorm, поместите ваш курсор где-либо в классе, а потом перейдите в Код -> Сгенерировать меню и выберите "Геттеры и сеттеры":

 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/Entity/Product
// ...

class Product
{
    // all of your properties

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

    public function getName()
    {
        return $this->name;
    }

    public function setName($name)
    {
        $this->name = $name;
    }

    // ... getters & setters for price & description
}

Tip

Обычано вам не нужен метод setId(): Doctrine установит его за вас автоматически.

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

Пора сохранить объект 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
// src/Controller/ProductController.php
namespace App\Controller;

// ...
use App\Entity\Product;

class ProductController extends Controller
{
    /**
     * @Route("/product", name="product")
     */
    public function index()
    {
        // вы можете извлечь EntityManager через $this->getDoctrine()
        // или вы можете добавить аргумент в ваше действие: index(EntityManagerInterface $em)
        $em = $this->getDoctrine()->getManager();

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

        // скажите Doctrine, что вы (в итоге) хотите сохранить Товар (пока без запросов)
        $em->persist($product);

        // на самом деле выполнить запросы (т.е. запрос INSERT)
        $em->flush();

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

Попробуйте!

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

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

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

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

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

Note

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

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

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

Извлечение объекта обратно из БД ещё проще. Представьте, что вы хотите перейти в /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 showAction($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());

    // или отобразить шаблон
    // в шаблоне, отобразить всё с {{ product.name }}
    // вернуть $this->render('product/show.html.twig', ['product' => $product]);
}

Попробуйте!

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

Когда у вас есть объект хранилища, у вас появляется множество методов помощника:

 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);

// искать один Товар по его основному ключу (обычно "id")
$product = $repository->find($id);

// искать один Товар по имени
$product = $repository->findOneBy(['name' => 'Keyboard']);
// или найти по имени и цене
$product = $repository->findOneBy([
    'name' => 'Keyboard',
    'price' => 19.99,
]);

// искать несколько объектов Товар, соответствующих имени, упорядоченные по цене
$products = $repository->findBy(
    ['name' => 'Keyboard'],
    ['price' => 'ASC']
);

// искат *все* объекты Товар
$products = $repository->findAll();

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

Tip

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

_images/doctrine_web_debug_toolbar.png

Если количество запросов в БД слишком велико, иконка станет жёлтой, чтобы показать, что что-то может быть не так. Нажмите на иконку, чтобы открыть Профилировщик Symfony и увидеть, какие именно запросы были выполнены. Если вы не видите панели инструментов веб-отладки, попробуйте запустить composer require profiler, чтобы установить её.

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

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

1
$ composer require annotations

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
// src/Controller/ProductController.php

use App\Entity\Product;
// ...

/**
 * @Route("/product/{id}", name="product_show")
 */
public function showAction(Product $product)
{
    // использовать Товар!
    // ...
}

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

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

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

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

 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 updateAction($id)
{
    $em = $this->getDoctrine()->getManager();
    $product = $em->getRepository(Product::class)->find($id);

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

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

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

Обновление объекта включает в себя всего три шага:

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

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

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

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

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

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

Запрашивание объектов: Хранилище

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

1
2
3
4
// изнутри контроллера
$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 Symfony\Bridge\Doctrine\RegistryInterface;

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

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

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

 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
// src/Repository/ProductRepository.php

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

    /**
     * @param $price
     * @return Product[]
     */
    public function findAllGreaterThanPrice($price): array
    {
        // автоматически знает, что надо выбрать Товары
        // "p" - это дополнительное имя, которое вы будете использовать далее в запросе
        $qb = $this->createQueryBuilder('p')
            ->andWhere('p.price > :price')
            ->setParameter('price', $price)
            ->orderBy('p.price', 'ASC')
            ->getQuery();

        return $qb->execute();

        // чтобы получить только один результат:
        // $product = $qb->setMaxResults(1)->getOneOrNullResult();
    }
}

Тут используется Конструктор запросов Doctrine: очень мощный и дружелюбный способ написания пользовательских запросов. Теперь, вы можете вызать этот метод в хранилише:

1
2
3
4
5
6
7
8
// изнутри контроллера
$minPrice = 10;

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

// ...

Если вы в Injecting Services/Config into a Service, то вы можете типизировать класс ProductRepository и внедрить его, как обычно.

Чтобы узнать больше, см. документацию Конструктора запросов от Doctrine.

Запрашивание объектов через DQL или SQL

В дополенение к конструктору запросов, вы также можете делать запросы с помощью Языка запросов Doctrine:

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

public function findAllGreaterThanPrice($price): array
{
    $em = $this->getEntityManager();

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

    // возвращает массив объектов Товар
    return $query->execute();
}

Или напрямую с помощью 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' => 10]);

    // возвращает массив массивов (т.е. набор чистых данных)
    return $stmt->fetchAll();
}

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

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

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

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

Коррекция пустых данных

Doctrine предоставляет библиотеку, которая позваляет вам программно загружать данные тестирования в ваш проект (т.е. "данные коррекции"). Чтобы узнать больше, см. документацию "DoctrineFixturesBundle".

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