Schema Generator - generujte svoji databázi z entit

4. června 2018

Schema Generator je nástroj, který dokáže zanalyzovat vaše entity a vytvořit z nich strukturu databáze. V současné chvíli podporuje Lean Mapper a MySQL, ale generátor je navržen univerzálně, takže časem může přibýt i podpora jiných knihoven a databází.

Jak generátor pracuje si ukážeme na Quick Startu z Lean Mapperu. Quick Start a Schema Generator nainstalujeme pomocí Composeru:

composer create-project leanmapper/quickstart
cd quickstart
composer require inlm/schema-generator

Teď si vytvoříme skript, pomocí kterého budeme generátor spouštět. Pojmenujeme si ho třeba schema-generator.php:

require __DIR__ . '/vendor/autoload.php';

$integration = new Inlm\SchemaGenerator\Integrations\LeanMapperIntegration(
    $schemaFile = __DIR__ . '/schema.neon',
    $migrationsDirectory = __DIR__ . '/migrations/structures',
    $entityDirectories = __DIR__ . '/app/Model/Entity',
    $options = NULL,
    $customTypes = NULL,
    $ignoredTables = array('migrations'),
    $connection = new LeanMapper\Connection(...),
    $mapper = new LeanMapper\DefaultMapper
);

$integration->createMigration();

Ve skriptu vytváříme tzv. integraci – jedná se o objekt, který zjednodušuje „standardní způsob použití“. Pokud chceme něco trochu jiného, můžeme generátor vytvořit a nakonfigurovat ručně – není to nic složitého. Pojďme si vysvětlit význam jednotlivých voleb:

  • $schemaFile – jedná se o soubor, ve kterém si bude generátor ukládat informace o struktuře databáze a nastaveních, aby mohl generovat změnové SQL příkazy. Není to jediný způsob, v případě potřeby můžeme strukturu načítat přímo z databáze (viz např. DibiAdapter)
  • $migrationsDirectory – složka, ve které budou uloženy soubory s SQL migracemi. Generované migrace jsou kompatibilní s nextras/migra­tions
  • $entityDirectories – jedna či více složek, ve kterých bude generátor hledat entity
  • $options – jedná se o výchozí volby, které budou přiřazeny každé vytvářené tabulce. Jde např. o ENGINE, COLLATE, apod.
  • $customTypes – mapování mezi vlastními datovými typy a databází. Pro MySQL je v generátoru předpřipraven např. typ money, který se přeloží jako DECIMAL(15, 4). Můžete uvést libovolné vlastní typy včetně value objektů.
  • $ignoredTables – seznam ignorovaných tabulek v databázi, např. nextras/migrations si v databázi vytváří tabulku migrations a ukládá do ní seznam aplikovaných migrací
  • $connection – připojení k databázi
  • $mapper – aplikací používaný mapper – bez něj nelze dodržet správné jmenné konvence

Na konci voláme metodu createMigration() – ta zajistí spuštění generátoru a vygenerování SQL příkazů do souboru.

Po spuštění (php -f schema-generator.php) skript vypíše:

Generating schema
Generating diff
Generating migrations
 - created table borrower
 - created table author
 - created table book
 - created table tag
 - created table borrowing
 - created table book_tag
Saving schema
Done.

Pokud vše proběhlo bez chyby, na disku se nám objeví:

  • složka migrations/structures a v ní SQL soubor s aktuální datumem a časem (např. 2018/06/2018-06-02-145118.sql)
  • soubor schema.neon a v něm aktuální databázová struktura a výchozí nastavení

Oba soubory můžete commitnout do vašeho projektu.

Vygenerovaný SQL soubor ve složce migrations/structures má následující obsah:

SET foreign_key_checks = 1;
SET time_zone = "SYSTEM";
SET sql_mode = "TRADITIONAL";

CREATE TABLE `borrower` (
    `id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
    `name` TEXT NOT NULL,
    PRIMARY KEY (`id`)
)
ENGINE=InnoDB
CHARACTER SET=utf8mb4
COLLATE=utf8mb4_czech_ci;

CREATE TABLE `author` (
    `id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
    `name` TEXT NOT NULL,
    `web` TEXT NULL,
    PRIMARY KEY (`id`)
)
ENGINE=InnoDB
CHARACTER SET=utf8mb4
COLLATE=utf8mb4_czech_ci;

CREATE TABLE `book` (
    `id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
    `author_id` INT UNSIGNED NOT NULL,
    `reviewer_id` INT UNSIGNED NULL,
    `pubdate` TEXT NOT NULL,
    `name` TEXT NOT NULL,
    `description` TEXT NULL,
    `website` TEXT NULL,
    `available` TINYINT(1) UNSIGNED NOT NULL,
    KEY `book_fk_author_id` (`author_id`),
    KEY `book_fk_reviewer_id` (`reviewer_id`),
    PRIMARY KEY (`id`),
    CONSTRAINT `book_fk_author_id` FOREIGN KEY (`author_id`) REFERENCES `author` (`id`) ON DELETE RESTRICT ON UPDATE RESTRICT,
    CONSTRAINT `book_fk_reviewer_id` FOREIGN KEY (`reviewer_id`) REFERENCES `author` (`id`) ON DELETE RESTRICT ON UPDATE RESTRICT
)
ENGINE=InnoDB
CHARACTER SET=utf8mb4
COLLATE=utf8mb4_czech_ci;

CREATE TABLE `tag` (
    `id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
    `name` TEXT NOT NULL,
    PRIMARY KEY (`id`)
)
ENGINE=InnoDB
CHARACTER SET=utf8mb4
COLLATE=utf8mb4_czech_ci;

CREATE TABLE `borrowing` (
    `id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
    `book_id` INT UNSIGNED NOT NULL,
    `borrower_id` INT UNSIGNED NOT NULL,
    `date` TEXT NOT NULL,
    KEY `borrowing_fk_book_id` (`book_id`),
    KEY `borrowing_fk_borrower_id` (`borrower_id`),
    PRIMARY KEY (`id`),
    CONSTRAINT `borrowing_fk_book_id` FOREIGN KEY (`book_id`) REFERENCES `book` (`id`) ON DELETE RESTRICT ON UPDATE RESTRICT,
    CONSTRAINT `borrowing_fk_borrower_id` FOREIGN KEY (`borrower_id`) REFERENCES `borrower` (`id`) ON DELETE RESTRICT ON UPDATE RESTRICT
)
ENGINE=InnoDB
CHARACTER SET=utf8mb4
COLLATE=utf8mb4_czech_ci;

CREATE TABLE `book_tag` (
    `book_id` INT UNSIGNED NOT NULL,
    `tag_id` INT UNSIGNED NOT NULL,
    PRIMARY KEY (`book_id`, `tag_id`),
    KEY `tag_id` (`tag_id`),
    CONSTRAINT `book_tag_fk_book_id` FOREIGN KEY (`book_id`) REFERENCES `book` (`id`) ON DELETE RESTRICT ON UPDATE RESTRICT,
    CONSTRAINT `book_tag_fk_tag_id` FOREIGN KEY (`tag_id`) REFERENCES `tag` (`id`) ON DELETE RESTRICT ON UPDATE RESTRICT
)
ENGINE=InnoDB
CHARACTER SET=utf8mb4
COLLATE=utf8mb4_czech_ci;

Generátor se snaží vytvářet co nejoptimálnější strukturu. Všimněte si např. sloupečku book.available – v entitě má sloupeček typ bool, do databáze ho generátor přeložil jako TINYINT(1), stejně tak je primárním klíčům typu INT automaticky doplněn příznak UNSIGNED. Generátor také automaticky vytváří spojovací tabulky (např. book_tag) a v nich složený primární klíč.

Datové typy můžete libovolně doladit přímo v entitě – ukážeme si to na entitě Tag. Položka $name má v entitě typ string a generátor ji automaticky přeložil na TEXT. To je v případě tabulky tag zbytečné plýtvání – bohatě by nám stačilo 20 znaků a typ VARCHAR. Uvedeme tedy u položky $name příznak m:schemaType:

/**
 * @property int $id
 * @property string $name m:schemaType(varchar:20)
 */
class Tag extends \LeanMapper\Entity
{
}

A spustíme znovu náš skript.

Generating schema
Generating diff
Generating migrations
 - updated column tag.name
Saving schema
Done.

Ve složce migrations/structures se nám objevil další SQL soubor:

SET foreign_key_checks = 1;
SET time_zone = "SYSTEM";
SET sql_mode = "TRADITIONAL";

ALTER TABLE `tag`
MODIFY COLUMN `name` VARCHAR(20) NOT NULL;

V příznaku m:schemaType můžete uvést jak požadovaný databázový datový typ, tak název vašeho vlastního typu např. m:schemaType(money).


Ještě si v rychlosti zodpovíme několik „často pokládaných dotazů“.

Musím při vývoji spouštět vygenerované SQL příkazy v databázi ručně?

Nemusíš. Právě z tohoto důvodu vyžaduje integrace ve svém konstruktoru objekt LeanMapper\Connection. Stačí tedy jen místo $integration->createMigration() zavolat $integration->updateDevelopmentDatabase() a generátor sám databázi upraví.

Můžu si před vytvořením migrace zobrazit seznam změn?

Ano. Integrace disponuje metodou showDiff().

Měl bych generátor spouštět na produkčním serveru?

To nedoporučuji, generátor by měl být spouštěn výhradně jen lokálně. Předpokládaný způsob použití je následující:

  1. vývojář začně pracovat na požadované feature
  2. v průběhu vývoje upravuje entity a spouští $integration->updateDevelopmentDatabase(), aby si zaktualizoval svou testovací databázi
  3. po dokončení featury spustí $integration->createMigration() a vygenerovanou SQL migraci s upraveným souborem schema.neon a ostatními změnami commitne a pošle k začlenění

Mám existující projekt, na který chci generátor nasadit. Jak mám postupovat?

Nejprve musíš vytvořit soubor schema.neon s otiskem aktuální databáze. Od verze 0.6.2 můžeš použít $integration->initFromDatabase() – to vytvoří i prvotní SQL migraci. Alternativně můžeš generátor sestavit ručně. Jak na to lze najít v příkladech.

Následně budeš muset upravit položky entit (a případně i výchozí $options a $customTypes) tak, aby přesně odpovídaly struktuře databáze. Pro ověření, zda existují nějaké rozdíly můžeš použít $integration->showDiff().

Existují kromě příznaku m:schemaType i nějaké další?

Ano, seznam všech podporovaných příznaků a anotací najdeš v dokumentaci.

Jak do m:schemaType zapsat value objekt?

V případě, že máš v anotaci jako typ položky nějaký value objekt, není potřeba příznak m:schemaType zapisovat, stačí jen value objekt uvést v poli $customTypes, které se předává do integrace:

use App\Model\HexColor;

$customTypes = array(
    HexColor::class => 'char(6)'
);

/**
 * @property int $id
 * @property HexColor $color
 */
class Product extends LeanMapper\Entity
{
    // ...
}

V současné době je generátor ve verzi 0.6.x, při používání tedy můžete narazit na chyby a některé věci se do vydání verze 1.0 mohou ještě výrazně změnit.

Dotazy rád zodpovím na GitHubu nebo na fóru.

Líbí se vám tato stránka?