Бази даних і Doctrine (частина 1)

24/02/2016 0 symfony, налаштування, конфігурація

Зберігання та зчитування інформації з бази даних, є звичним завданням і водночас містить виклик для будь-якого додатку. Хоча повністю укомплектований Symfony Framework не включає об’єктно-реляційного відображення за замовчуванням, Symfony Standard Edition, найчастіше використовуваний дистрибутив, містить інтегровану бібліотеку Doctrine, яка має на меті дати вам потужні інструменти для полегшення роботи. У цьому розділі ви ознайомитесь з базовою філософією бібліотеки Doctrine, і побачите наскільки простою може бути робота з базами даних.

Doctrine повністю відокремлена від Symfony, тому ви можете її і не використовувати. Цей розділ повністю присвячений Doctrine ORM (об’єктно-реляційне відображення), який дозволяє відображати об’єкти у реляційній базі даних (таких як MySQL, PostgreSQL, або Microsoft SQL). Якщо ви надаєте перевагу користуванню необробленими запитами, це напрочуд легко. Пояснення ви можете знайти у статті Довідника "How to Use Doctrine DBAL". Ви також можете зберігати дані у MongoDB, використовуючи бібліотеку Doctrine ORM. більше інформації стосовно цього питання читайте у документації: "DoctrineMongoDBBundle".

Простий приклад: об’єкт Product

Щоб зрозуміти як працює бібліотека Doctrine, найкраще побачити це в дії. У цьому підрозділі ми налаштуємо базу даних, створимо об’єкт Product, збережем його у базі даних та отримаємо його назад.

Налаштування бази даних

Перш ніж розпочати, налаштуємо інформацію щодо підключення бази даних. За звичай, ця інформація налаштовується у файлі app/config/parameters.yml:

 # app/config/parameters.yml
parameters:
    database_driver:    pdo_mysql
    database_host:      localhost
    database_name:      test_project
    database_user:      root
    database_password:  password

# ...

YAML

# app/config/config.yml
doctrine:
    dbal:
        driver:   "%database_driver%"
        host:     "%database_host%"
        dbname:   "%database_name%"
        user:     "%database_user%"
        password: "%database_password%"

XML

<!-- app/config/config.xml -->
<?xml version="1.0" encoding="UTF-8" ?>
<container xmlns="http://symfony.com/schema/dic/services"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:doctrine="http://symfony.com/schema/dic/doctrine"
    xsi:schemaLocation="http://symfony.com/schema/dic/services
        http://symfony.com/schema/dic/services/services-1.0.xsd
        http://symfony.com/schema/dic/doctrine
        http://symfony.com/schema/dic/doctrine/doctrine-1.0.xsd">

    <doctrine:config>
        <doctrine:dbal
            driver="%database_driver%"
            host="%database_host%"
            dbname="%database_name%"
            user="%database_user%"
            password="%database_password%" />
    </doctrine:config>
</container>

PHP

// app/config/config.php
$configuration->loadFromExtension('doctrine', array(
    'dbal' => array(
        'driver'   => '%database_driver%',
        'host'     => '%database_host%',
        'dbname'   => '%database_name%',
        'user'     => '%database_user%',
        'password' => '%database_password%',
    ),
));

Відділяючи інформацію, що знаходиться у базі даних до іншого файлу, ви легко можете мати різні версії файлів на кожному сервері. Також, зберігайте налаштування бази даних (або будь-яку іншу інформацію) за межами вашого проекту, наприклад у налаштуваннях Apach. Щоб дізнатися більше, читайте розділ у Довіднику: “Як налаштувати зовнішні параметри у сервісі контейнером”.

Тепер, коли бібліотека Doctrine знає про вашу базу даних, вона може створити вам її:

$ php app/console doctrine:database:create

Налаштування кодування бази даних: UTF8

Помилкою, яку роблять навіть досвідчені розробники починаючи проекти на Symfony, є те, що вони забувають налаштувати кодування за замовчуванням, і порядок зіставлення та сортування символів у базі даних. Це все закінчується тим, що у базі даних так і залишається латинське кодування, так як воно налаштоване за замовчуванням майже в усіх базах даних. Вони пам’ятають про це при налаштуванні перший раз, але забувають, що після запуску відносно звичної всім команди під час розробки, ці налаштування анулюються:

$ php app/console doctrine:database:drop --force
$ php app/console doctrine:database:create

Немає жодного способу, щоб налаштувати ці значення за замовчуванням всередині Doctrine, так як цей додаток має тенденцію бути якомога більш “агностичним”, в плані  налаштування середовища. Одним із способів вирішення проблеми є налаштування значення за замовчуванням на рівні серверу. Налаштувати UTF8 для MySQL так само просто як і додати декілька рядків до конфігураційного файлу (типового my.cnf):

 [mysqld]
# Version 5.5.3 introduced "utf8mb4", which is recommended
collation-server     = utf8mb4_general_ci # Replaces utf8_general_ci
character-set-server = utf8mb4            # Replaces utf8

Ми рекомендуємо вам використовувати новий набір символів для кодування - utf8mb4, замість звичного для MySQL UTF8, тому що він не підтримує 4-рьох байтних символів шифрування, а рядки, які містять таке шифрування будуть скорочуватися.

Якщо ви хочете використовувати SQLite, як вашу базу даних, вам слід написати команду і вказати де повинен знаходитися файл з базою даних:

YAML

# app/config/config.yml
doctrine:
    dbal:
        driver: pdo_sqlite
        path: "%kernel.root_dir%/sqlite.db"
        charset: UTF8

XML

<!-- app/config/config.xml -->
<?xml version="1.0" encoding="UTF-8" ?>
<container xmlns="http://symfony.com/schema/dic/services"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:doctrine="http://symfony.com/schema/dic/doctrine"
    xsi:schemaLocation="http://symfony.com/schema/dic/services
        http://symfony.com/schema/dic/services/services-1.0.xsd
        http://symfony.com/schema/dic/doctrine
        http://symfony.com/schema/dic/doctrine/doctrine-1.0.xsd">

    <doctrine:config>
        <doctrine:dbal
            driver="pdo_sqlite"
            path="%kernel.root_dir%/sqlite.db"
            charset="UTF-8" />
    </doctrine:config>
</container>

PHP

 // app/config/config.php
$container->loadFromExtension('doctrine', array(
    'dbal' => array(
        'driver'  => 'pdo_sqlite',
        'path'    => '%kernel.root_dir%/sqlite.db',
        'charset' => 'UTF-8',
    ),
));

Створюємо клас-сутність 

Припустимо, що ви ствоюєте додаток, у якому повинні відображатися продукти. Навіть без роздумів про бази даних чи Doctrine, ви вже знаєте, що вам потрібен об’єкт Product, який і покаже ці продукти. Давайте створимо цей клас у директорії Entity вашого AppBundle:

 // src/AppBundle/Entity/Product.php
namespace AppBundle\Entity;

class Product
{
    protected $name;
    protected $price;
    protected $description;
}

Клас, як його ще часто називають “сутність”, є звичайним базовим класом що містить певні дані. Будучи простим, він стає у нагоді при виконанні вимог потрібного продукту у вашому додатку. Поки цей клас не може бути збереженим у базі даних, так як це простий клас PHP.

Після того, як ви ознайомитесь з певними поняттями бібліотеки Doctrine, ви зможете створювати потрібні класи з її допомогою. Інтерактивні питання допоможуть вам збудувати будь-який клас:

$ php app/console doctrine:generate:entity

Додаємо інформацію про відображення

Doctrine дозволяє працювати з базами даних значно цікавішим способом ніж просто робити вибірку рядків з таблиці до масиву. Натомість, з Doctrine ви можете зберігати цілі об’єкти до бази даних та робити вибірку цих самих об’єктів. Це спрацює при відображенні PHP класу до таблиці бази даних, та властивостей цього класу PHP до колонок в таблиці.

 ../_images/doctrine_image_1.png

 

Для того, аби бібліотека Doctrine могла таке зробити, вам необхідно створити “метадані”, або такі налаштування які “повідомлять”  Doctrine, як клас Product і його властивості повинні відповідати базі даних. Ці метадані можуть бути у різних форматах, включаючи YAML і  XML, або знаходитися прямо всередині класу Product  через анотації:

Aнотації

// src/AppBundle/Entity/Product.php
namespace AppBundle\Entity;

use Doctrine\ORM\Mapping as ORM;

/**
* @ORM\Entity
* @ORM\Table(name="product")
*/
class Product
{
   /**
    * @ORM\Column(type="integer")
    * @ORM\Id
    * @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
# src/AppBundle/Resources/config/doctrine/Product.orm.yml
AppBundle\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

<!-- src/AppBundle/Resources/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="AppBundle\Entity\Product" table="product">
        <id name="id" type="integer">
            <generator strategy="AUTO" />
        </id>
        <field name="name" type="string" length="100" />
        <field name="price" type="decimal" scale="2" />
        <field name="description" type="text" />
    </entity>
</doctrine-mapping>

Пакет може прийняти лише один формат дефініцій метаданих. Наприклад, неможливо перемішати дефініції метаданих YAML з дефініціями анотованого PHP класу.

Ім’я таблиці не є обов’язковим, тому якщо воно не вказане, воно буде визначене автоматично, базуючись на імені класу об’єкта.

Doctrine дозволяє обирати будь-який тип поля з власними опціями. Для того щоб дізнатися більше інформації про доступні типи полів, читайте у підрозділі цого ж розділу: Типи полів у Doctrine.

Також, перегляньте базову документацію відображення Doctrine (Basic Mapping Documentation), для того щоб дізнатися деталі про відображення інформації. Якщо ви користуєтеся анотаціями, тоді вам слід обумовити їх усі з ORM\ (наприклад, ORM\Column(...)), що не показано у документації Doctrine. Вам також потрібно включити вираз use Doctrine\ORM\Mapping as ORM;, який імпортуватиме префікс анотації ORM.

Слідкуйте за тим, щоб ім’я вашого класу та властивостей не співпадало з ключовим словом SQL (group або user). Наприклад, якщо ім’я вашого класу-сутності Group, тоді, відповідно, за замовчуванням ім’я вашої таблиці буде group, що може спричинити помилку SQL на деяких движках. Якщо ви прагнете навчитися як уникати імен такого типу, читайте у документації: Reserved SQL keywords documentation. Проте, є ще одна альтернатива. Якщо ви вільно почуваєтеся у виборі схеми бази даних, тоді просто зробіть відображення до іншого імені таблиці, чи імені колонки. Детальніше читайте у документації: Creating Classes for the Database та Property Mapping.

При використанні іншої бібліотеки або програми (наприклад, Doxygen), яка використовує анотації, слід помістити анотацію @IgnoreAnnotation у клас, для того щоб вказати які саме анотації Symfony повинна ігнорувати. Наприклад, щоб не дозволити анотації @fn вивести на екран виняток, додайте наступне:

 /**
 * @IgnoreAnnotation("fn")
 */
class Product
// ...

Створення геттерів і сеттерів (Getters and Setters)

Не дивлячись на те, що Doctrine тепер знає як зберегти об’єкт Product у базі даних, у самий по собі клас не приносить користі. Так як Product є простим, регулярним класом у PHP, нам потрібно створити геттери і сеттери, наприклад, getName(), setName(). Ми це робимо для того, щоб отримати доступ до їхніх властивостей, так як вони є захищеними (protected). На щастя, Doctrine зробить це для вас, запустивши ось такий код:

$ php app/console doctrine:generate:entities AppBundle/Entity/Product

Ця команда “хоче переконатися”, що усі геттери і сеттери є згенерованими для класу Product. Це досить безпечна команда - ви можете запускати її знову і знову: так як вона генерує лише ті геттери і сеттери, які взагалі не існують (тобто, ця команда не замінить уже існуючих методів).

Запам’ятайте, що генератор об’єктів у Doctrine створює лише прості геттери і сеттери. Тому, вам слід перевіряти уже згенеровані об’єкти та пристосовувати код геттерів і сеттерів до ваших потреб.

Детальніше про команду doctrine:generate:entities

За допомогою команди doctrine:generate:entities ви зможете:

1) генерувати геттери і сеттери;

2) генерувати класи-сховища налаштовані за анотацією @ORM\Entity(repositoryClass="...");

3) генерувати відповідний конструктор у відношенні 1:m та n:m.

Команда doctrine:generate:entities зберігає копію Product.php з назвою Product.php~. У деяких випадках наявність даного файлу може спричинити таку помилку: “Клас не можна перевизначити” (англ. “Cannot redeclare class”). Але її можна позбутися.   Скористайтеся опцією --no-backup, щоб запобігти створенню такого типу копій.

Зверніть увагу, що вам не потрібно використовувати цю команду. Doctrine не покладається на генерацію коду. Як і зі звичайним PHP класом, просто переконайтеся, що захищені (приватні) властивості містять геттери і сеттери. Цю команду було створено для полегшення роботи, так як це досить банальна річ яку слід робити при використанні бібліотеки Doctrine.

Ви також можете створювати загальновідомі класи (такі як клас PHP з інформацією про відображення бібліотеки Doctrine) пакета або цілого заповнювача.

# generates all entities in the AppBundle
$ php app/console doctrine:generate:entities AppBundle

# generates all entities of bundles in the Acme namespace
$ php app/console doctrine:generate:entities Acme

Для Doctrine немає різниці якими є властивості: protected чи private, або чи наявна функція геттерів і сеттерів для тієї чи іншої властивості. Геттери і сеттери створюються лише для взаємодії з об’єктом PHP.

Створюємо таблиці/схему бази даних

Тепер у нас є потрібний клас Product з відображеною інформацією, і Doctrine знає як правильно зберегти його. Звичайно, у вас немає відповідної таблиці product у базі даних. На щастя, Doctrine може автоматично створити всі потрібні  для кожного класу таблиці. Для цього, виконайте команду:

$ php app/console doctrine:schema:update --force

Насправді, ця команда надзвичайно потужна. Вона порівнює те, як ваша база даних повинна виглядати (опираючись на відповідну інформацію ваших класів) з тим, як вона насправді виглядає. Також генерує вирази SQL, які потрібні для оновлення БД, для того щоб вона знаходилася там де потрібно. Іншими словами, коли ви додаєте нову властивість з відображеними метаданими до класу Product і знову запускаєте це завдання, програма виконає команду "alter table", яка використовується для додавання нової колонки у вже існуючій таблиці product.

Ще краще використати цю функцію через пакет migrations. Це дасть змогу генерувати та зберігати SQL вирази у так званих міграційних класах, які можна систематично запускати на вашому робочому сервері, для того, щоб безпечно і надійно простежувати та переміщувати схеми баз даних.

Тепер база даних містить функціональну таблицю product з колонками, що збігаються з метаданими, які ви вказали.  

Зберігаємо об’єкти до бази даних

Тепер коли клас Product відображується, і ми створили відповідну йому таблицю з таким же іменем, можемо зберегти дані у БД. З середини контролера, це дуже просто. Давайте додамо наступний метод дo DefaultController у пакеті:

// src/AppBundle/Controller/DefaultController.php

// ...
use AppBundle\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()->getManager();

    $em->persist($product);
    $em->flush();

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

Якщо ви виконали все як подано у прикладі, вам необхідно створити маршрут, який вказуватиме на цю дію, для того щоб побачити як все працює.

У цій статті розповідається про те, як працювати з бібліотекою Doctrine з середини контролера використовуючи метод конролера getDoctrine(). Цей метод є найкоротшим шляхом, за допомогою якого можна добратися до сервісу doctrine. Ви можете працювати з Doctrine будь-де вставивши цей сервіс у інший. Щоб дізнатися про це детальніше читайте розділ “Сервіс контейнером”.

Давайте розглянемо попередній приклад більш детально:

1) 10-13 рядки. Ви ілюструєте конкретним прикладом і працюєте разом з об’єктом $product, як і з будь-яким іншим звичайним об’єктом PHP.

2) 15 рядок. Ця команда добуває і вносить об’єкт Doctrine entity manager, який відповідає за проведення процесу зберігання, добування та внесення об’єктів до бази даних.

3) 16 рядок. Метод persist() дає команду Doctrine керувати об’єктом $product, але поки не відсилає запиту до бази даних.

4) 17 рядок. Коли робиться запит на виконання методу flush(), Doctrine переглядає всі об’єкти якими керує, щоби перевірити чи їх потрібно зберегти у базі даних. У цьому прикладі об’єкт $product ще поки не збережений, тому entity manager виконує запит INSERT, і тоді створюється рядок у таблиці product.

Насправді, так як Doctrine вже знає усі ваші класи, якими ви керуєте, коли ви даєте команду на виконання методу flush(), Doctrine підраховує усі зміни і тоді виконує запити у правильному порядку. Doctrine використовує вже підготовлені закешовані вирази, щоби трохи покращити продуктивність. Наприклад, якщо ви загалом зберегли 100 об’єктів Product, і потім даєте команду на виконання методу flush(), відповідно Doctrine виконає 100 запитів INSERT, використовуючи один підготовлений об’єкт.

Процес створення чи оновлення об’єктів завжди однаковий. У наступному підрозділі, ви побачите наскільки досвідченою є бібліотека Doctrine, оскільки вона автоматично виконує запит UPDATE, якщо запис вже існує у базі даних.

Doctrine надає бібліотеку, яка дозволяє програмно завантажувати тестовані дані у ваш проект,  Щоб дізнатися все детальніше читайте розділ "DoctrineFixturesBundle".

Поділитися