Базы данных и ORM Doctrine в Symfony.

Из «коробки» Symfony 4 не предоставляет компонент для работы с базой данных, но она имеет отличную тесную интеграцию с прекрасной сторонней библиотекой называемой Doctrine.

Эта статья посвящена использованию ORM Doctrine. Если вы предпочитаете работать с «необработанными» запросами к БД, то Вам следует познакомиться с Doctrine DBAL.

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


Установка Doctrine.

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

composer require symfony/orm-pack  
composer require --dev symfony/maker-bundle

Конфигурация Базы Данных

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

# .env (или переопределите DATABASE_URL в .env.local, чтобы избежать попадания ваших изменений в историю коммитов git)
# настройте эту строку
DATABASE_URL="mysql://db_user:db_password@127.0.0.1:3306/db_name"
# для использования sqlite:
# DATABASE_URL="sqlite:///%kernel.project_dir%/var/app.db"

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

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

 php bin/console doctrine:database:create

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

Создание Entity Class

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

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

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)

Отлично! Теперь у Вас в проекте появится новый файл src/Entity/Product.php :

// 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;
    }
    // ... остальные методы (гетеры и сетеры)
}

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

Если вы используете базу данных SQLite, вы увидите следующую ошибку: PDOException: SQLSTATE[HY000]: General error: 1 Cannot add a NOT NULL column with default value NULL. Добавьте параметр nullable = true в свойство description, чтобы устранить проблему.


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

Этот класс называется «entity»сущность. И вскоре вы сможете сохранять и запрашивать объекты Product в таблице продуктов в вашей базе данных. Каждое свойство в сущности Product может быть сопоставлено со столбцом в этой таблице. Обычно это делается с помощью аннотаций: это комментарии @ORM\…, которые вы видите над каждым свойством:

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

Doctrine поддерживает широкий спектр типов полей, каждый со своими опциями. Чтобы увидеть полный список, ознакомьтесь с документацией Doctrine’s Mapping Types.

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

Миграции: создание таблиц БД и структуры БД

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

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, необходимый для обновления вашей базы данных! Чтобы запустить этот SQL, требуется применить ваши миграции, с помощью консольной команды:

 php bin/console doctrine:migrations:migrate

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

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

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

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():

// src/Entity/Product.php
// ...
class Product
{
    // ...
+     /**
+      * @ORM\Column(type="text")
+      */
+     private $description;
    // getDescription() & setDescription() were also added
}

Новое свойство сопоставлено, но оно еще не существует в таблице продукта. Нет проблем! Создайте новую миграцию с помощью уже ранее используемой команды:

 php bin/console make:migration

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

ALTER TABLE product ADD description LONGTEXT NOT NULL

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

 php bin/console doctrine:migrations:migrate

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

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

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

php bin/console make:entity —regenerate

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

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

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

 php bin/console make:controller ProductController

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

// 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="product")
     */
    public function index()
    {
        // you can fetch the EntityManager via $this->getDoctrine()
        // or you can add an argument to your action: index(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());
    }
}

Активируйте сохранение сущности в БД, перейдя по роуту

http://localhost:8000/product

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

 php bin/console doctrine:query:sql 'SELECT * FROM product'

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

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

Если вызов метода flush() завершитсянеудачей, то будет сгенерировано исключение Doctrine\ORM\ORMException.

Независимо от того, создаете ли вы или обновляете объекты, рабочий процесс всегда один и тот же: Doctrine достаточно умен, чтобы знать, следует ли ему ВСТАВИТЬ или ОБНОВИТЬ вашу сущность.

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

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

// 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());
     // или рендерим в шаблоне
    // в шаблоне выводим нужные строки, с помощью {{ product.name }}
    // return $this->render('product/show.html.twig', ['product' => $product]);
}

Попробуйте, перейдя по адресу http://localhost:8000/product/1

Когда вы запрашиваете объект определенного типа, вы всегда используете то, что известно как его «репозиторий». Вы можете думать о репозитории как о классе PHP, единственная задача которого — помочь вам выбрать объекты определенного класса.

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

$repository = $this->getDoctrine()->getRepository(Product::class);
// ищем один продукт по его первичному ключу (обычно «id»)
$product = $repository->find($id);
// ищем один продукт по названию
$product = $repository->findOneBy(['name' => 'Keyboard']);
// или найти по названию и цене
$product = $repository->findOneBy([
    'name' => 'Keyboard',
    'price' => 1999,
]);
// ищем несколько объектов Product, соответствующих названию, упорядоченных по цене
$products = $repository->findBy(
    ['name' => 'Keyboard'],
    ['price' => 'ASC']
);
// ищем *все* объекты Product
$products = $repository->findAll()

Вы также можете добавить в класс Репозитория собственные методы для более сложных запросов! Подробнее об этом в разделе «Запросы объектов: секция репозиторий».

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


Если количество запросов к базе данных слишком велико, значок станет желтым, чтобы указать, что что-то может быть не правильно. Нажмите на значок, чтобы открыть Symfony Profiler и увидеть точные запросы, которые были выполнены. Если вы не видите веб-панель инструментов для отладки, попробуйте запустить composer, для ее установки:

composer require —dev symfony/profiler-pack

Автоматическая выборка объектов (ParamConverter).

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

 composer require sensio/framework-extra-bundle

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

// src/Controller/ProductController.php
use App\Entity\Product;
/**
 * @Route("/product/{id}", name="product_show")
 */
public function show(Product $product)
{
    // Используйте объект Product !
    // ...
}

Это нужный объект с данными! Пакет использует {id} из маршрута для запроса продукта по столбцу id. Если он не найден, создается страница 404.

Есть еще много вариантов, которые вы можете использовать. Узнайте больше о ParamConverter в документации.

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

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

/**
 * @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 для редактирования существующего продукта состоит из трех этапов:

  • извлечение объекта из Doctrine;
  • модификация объекта;
  • вызов метода flush () для менеджера сущностей.

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

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

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

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

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

Запросы для объектов через Репозиторий.

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

// from inside a controller
$repository = $this->getDoctrine()->getRepository(Product::class);
$product = $repository->find($id);

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

// 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, которая была сгенерирована в верхней части вашего класса сущностей Product.

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

// 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’s Query Builder: очень мощный и удобный способ написания пользовательских запросов. Теперь вы можете вызвать этот метод через объект репозитория:

// фрагмент кода из контроллера
$minPrice = 1000;
$products = $this->getDoctrine()
    ->getRepository(Product::class)
    ->findAllGreaterThanPrice($minPrice);
// ...

Если вы внедряете Сервисы в Services/Config, то вы можете указать тайп-хинт класс ProductRepository и внедрить его как обычно.

Для получения дополнительной информации ознакомьтесь с документацией Doctrine Query Builder.

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

В дополнение к построителю запросов Query Builder вы также можете выполнить запрос с помощью Doctrine Query Language:

// src/Repository/ProductRepository.php
// ...
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);
    // возвращает массив объектов класса Product
    return $query->execute();
}

Или напрямую с SQL, если вам нужно:

// 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]);
    //  возвращает массив массивов (т.е. набор необработанных "сырых" данных)
    return $stmt->fetchAll();
}

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

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

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

Для получения детальной информации, смотрите в документации: «Как работать с отношениями в Doctrine»

Фиктивные данные

Doctrine предоставляет библиотеку, которая позволяет вам программно загружать данные тестирования в ваш проект (т. е. «Данные фикстуры» в оригинале «fixture data»). Установите его с помощью:

 composer require --dev doctrine/doctrine-fixtures-bundle

Затем с помощью команды make:fixtures создайте пустой класс fixture:

 php bin/console make:fixtures

The class name of the fixtures to create (e.g. AppFixtures):
> ProductFixture

Настройте новый класс для загрузки объектов Product в Doctrine:

// src/DataFixtures/ProductFixture.php
namespace App\DataFixtures;
use Doctrine\Bundle\FixturesBundle\Fixture;
use Doctrine\Common\Persistence\ObjectManager;
class ProductFixture extends Fixture
{
    public function load(ObjectManager $manager)
    {
        $product = new Product();
        $product->setName('Priceless widget!');
        $product->setPrice(14.50);
        $product->setDescription('Ok, I guess it *does* have a price');
        $manager->persist($product);
        // add more products
        $manager->flush();
    }
}

Очистите базу данных и перезагрузите все классы фикстур:

 php bin/console doctrine:fixtures:load

Для более подробной информации о теме фейковых данных, ознакомьтесь в документации с темой «DoctrineFixturesBundle«.

Базы данных и ORM Doctrine в Symfony.: 2 комментария

  1. У меня в одной из фикстур в проекте не сохраняются данные в базу. Причем сообщений об ошибках нет. При этом в других блоках кода и даже в фикстурах других бандлов тот же код работает без проблем. Не работает код только в фикстурах одного конкретного бандла. Как такое возможно?

    1. Евгений, я не совсем понимаю ваш вопрос, но возможно Вам есть смысл применить фистуры вербозно
      [cc lang=»php»]
      bin/console doctrine:fixture:load -vvv
      [/cc]

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *