Статьи

Первый взгляд на Atlas — ORM, который обеспечивает

Эта статья была рецензирована Полом Джонсом и Скоттом Молинари . Спасибо всем рецензентам SitePoint за то, что сделали контент SitePoint как можно лучше!

« AtlasORM — это реализация картографа данных для вашей модели персистентности (а не модели вашего домена)

По определению Data Mapper перемещает данные между объектами и базой данных и изолирует их друг от друга. При использовании Data Mapper объектам в памяти даже не нужно знать, что база данных существует. Ему не нужно знать интерфейс SQL или схему базы данных; ему даже не нужен доменный слой, чтобы знать, что он существует!

Это может привести нас к мысли, что в Atlas постоянный уровень полностью отключен от базы данных, но это не совсем то, что происходит. Атлас использует термин Record чтобы указать, что его объекты не являются объектами домена. Атлас Рекорд пассивен; не активная запись. В отличие от большинства ORM, его объекты представляют модель постоянства, а не модель предметной области. Думайте об этом как о представлении того, как хранятся данные, а не как о реальных представлениях

Иллюстрация бога Атласа, несущего большой глобус

В чем причина этого?

Создатель «Атласа» Пол Джонс основал его на статье Мехди Халили . Идея заключалась в том, чтобы создать альтернативу шаблону Active Record (объект, который переносит как данные, так и поведение, и по своей сути связан с базой данных).

Ссылаясь на автора:

«Я хотел альтернативу Active Record, которая позволила бы вам начать работу с Active Record так же легко, как [с] Active Record для вашей персистентной модели, а затем реорганизовать более легкий подход к более богатой модели домена».

У Altas есть свой набор характеристик, который отличает его от других.

  • Нет аннотаций — при условии, что код должен быть в коде, а не в комментариях.
  • Никакой миграции или логики модификации базы данных — поскольку Atlas отделен от базы данных (не полностью, пакет должен взаимодействовать с базой данных в целом, чтобы иметь возможность перемещать данные назад и вперед), имеет смысл, что он действует только как модель схемы и не имеет создателя и менеджера этого.
  • Не ленивая загрузка — Создатель считал ленивую загрузку полезной, но в конечном итоге слишком много проблем для того, чего она стоит.
  • Нет абстракций типов данных — типы базы данных доступны и доступны, где это возможно.
  • Поддержка составных ключей — Atlas поддерживает как составные первичные ключи, так и составные внешние ключи.

Теперь, когда у нас есть общее представление об Atlas и о том, что он обозначает, давайте посмотрим на его использование.

Установка

Поскольку на момент написания этой статьи Atlas все еще находится в стадии разработки, мы установим самую последнюю версию. Также мы установим пакет CLI для простоты разработки. Это оставляет нам следующий файл composer.json :

 { "require": { "atlas/orm": "@dev" }, "require-dev": { "atlas/cli": "@dev" }, "autoload": { "psr-4": { "App\\": "src/App/" } } } 

У нас также есть немного PSR-4 для автозагрузки, который мы будем использовать в дальнейшем.

Настройка его

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

Мы будем использовать очень простой дизайн базы данных. У нас будут продукты и категории в отдельных таблицах базы данных. Используя MySQL , давайте создадим нашу базу данных:

 CREATE DATABASE atlasorm; USE DATABASE atlasORM; CREATE TABLE `category` (`category_id` int(11) NOT NULL, `name` varchar(255) NOT NULL) ENGINE=InnoDB DEFAULT CHARSET=latin1; CREATE TABLE `products` (`product_id` int(11) NOT NULL, `name` varchar(255) NOT NULL, `category_id` int(11) NOT NULL) ENGINE=InnoDB DEFAULT CHARSET=latin1; ALTER TABLE `category` ADD PRIMARY KEY (`category_id`); ALTER TABLE `products` ADD PRIMARY KEY (`product_id`); ALTER TABLE `category` MODIFY `category_id` int(11) NOT NULL AUTO_INCREMENT; ALTER TABLE `products` MODIFY `product_id` int(11) NOT NULL AUTO_INCREMENT; 

После этого мы добавляем некоторые значения в обе таблицы:

 INSERT INTO `category` (`category_id`, `name`) VALUES (1, 'Steel'), (2, 'Wood'), (3, 'Leather'); INSERT INTO `products` (`product_id`, `name`, `category_id`) VALUES (1, 'Nails', 1), (2, 'Pipe', 1), (3, 'Chair', 2), (4, 'Screwdriver', 1); 

Располагая этой информацией, мы теперь можем настроить наш ORM. У Atlas очень мало генерации кода. Чтобы сгенерировать каркасные классы (и класс Table, и класс Mapper), нам сначала нужно будет создать файл .php котором у нас будет информация о подключении к нашей базе данных. Таким образом, генератор будет знать, какие свойства он должен генерировать.

 <?php // connection.php return ['mysql:dbname=testdb;host=localhost', 'username', 'password']; 

Затем, давайте запустим генератор atlas-skeleton . Генератор atlas-skeleton — это инструмент CLI, который будет использовать информацию о connection.php с базой данных в файле connection.php , имя класса и заданное пространство имен для генерации классов скелета. Обратите внимание, что мы должны запустить генератор для каждой отдельной таблицы в базе данных.

 ./vendor/bin/atlas-skeleton.php --dir=./src/App/DataSource --conn=/path/to/connection.php --table=products App\\DataSource\\Products ./vendor/bin/atlas-skeleton.php --dir=./src/App/DataSource --conn=/path/to/connection.php --table=category App\\DataSource\\Category 

После запуска команды atlas-skeleton мы можем проверить папку /src/App/Datasource и увидеть, что скелеты для products и category были созданы. Оба этих скелета содержат два файла. Файл Mapper , который почти пуст, и класс Table , который содержит описание таблицы базы данных. Это все настройки, которые нам нужны.

Теперь давайте наконец увидим это в действии!

CRUD Операции

Когда все настроено, давайте сосредоточимся на использовании нашего ORM. Для этого нам нужен экземпляр Atlas , и мы создаем его, используя класс AtlasContainer .

 <?php require_once __DIR__ . '/vendor/autoload.php'; use Atlas\Orm\Mapper\Mapper; use Atlas\Orm\AtlasContainer; use App\DataSource\Category\CategoryMapper; use App\DataSource\Products\ProductsMapper; $atlasContainer = new AtlasContainer('mysql:host=host;dbname=atlasorm','username', 'password' ); $atlasContainer->setMappers([ProductsMapper::CLASS, CategoryMapper::CLASS]); $atlas = $atlasContainer->getAtlas(); 

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

чтение

Чтобы прочитать Record или RecordSet из нашей базы данных, мы можем использовать следующее:

 $categoryRecord = $atlas->fetchRecord(CategoryMapper::CLASS, '2'); 

Это вернет одну запись с идентификатором 2. Для набора записей:

 $categoryRecordSet = $atlas ->select(CategoryMapper::CLASS) ->orderBy(['category_id DESC']) ->limit(10) ->fetchRecordSet(); 

Это приведет к получению RecordSet из последних 10 категорий, упорядоченных по category_id .

Создание, обновление и удаление

Для создания, обновления и удаления Atlas использует тот же принцип. Получение записи и манипулирование ею:

 // start insert $newCategory = $atlas->newRecord(CategoryMapper::CLASS); $newCategory->name = "Bone"; // create a transaction $transaction = $atlas->newTransaction(); $transaction->insert($newCategory); // execute the transaction $ok = $transaction->exec(); if ($ok) { echo "Transaction success."; } else { echo "Transaction failure. "; } 

Как мы видим, создание довольно просто. Для обновления и удаления мы можем использовать тот же принцип:

 //Start updating $newCategory = $atlas->fetchRecord(CategoryMapper::CLASS, '2'); $newCategory->name = "Wood"; // create a transaction $transaction = $atlas->newTransaction(); // plan work for the transaction $transaction->update($newCategory); //$transaction->delete($newCategory); // execute the transaction plan $ok = $transaction->exec(); if ($ok) { echo "Transaction success."; } else { echo "Transaction failure. "; } //End updating 

В этом случае мы можем использовать либо update вместе с новыми значениями таблицы, чтобы обновить запись, либо delete чтобы удалить эту же запись.

Краткое примечание об отношениях

Atlas поддерживает все четыре вида отношений: OneToMany , ManyToOne , OneToOne и MantToMany . Чтобы добавить отношения, мы должны добавить их в классы Mapper.

В нашем конкретном случае давайте представим, что в каждой категории может быть много товаров. Отношения один ко многим. Наш CategoryMapper будет выглядеть так:

 <?php namespace App\DataSource\Category; use Atlas\Orm\Mapper\AbstractMapper; use App\DataSource\Products\ProductsMapper; /** * @inheritdoc */ class CategoryMapper extends AbstractMapper { /** * @inheritdoc */ protected function setRelated() { $this->oneToMany('products', ProductsMapper::CLASS); } } 

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

Итак, если мы хотим получить RecordSet наших категорий с соответствующими записями о продуктах, мы можем использовать метод with() , вот так:

 $newRecord = $atlas ->select(CategoryMapper::CLASS) ->with([ 'products' ]) ->fetchRecordSet(); 

Зная, как работают основные операции CRUD, мы можем теперь взглянуть на некоторые более практичные примеры кода с использованием Atlas.

Некоторые практические примеры

Здорово иметь такой инструмент, как Atlas, и посмотреть, как он работает. Но в конечном итоге мы хотим, чтобы он использовался в более практичных сценариях. Хотя мы не будем создавать полноценное приложение, мы рассмотрим некоторые возможные варианты использования.

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

 <?php require_once __DIR__ . '/vendor/autoload.php'; use Atlas\Orm\Mapper\Mapper; use Atlas\Orm\AtlasContainer; use App\DataSource\Category\CategoryMapper; use App\DataSource\Products\ProductsMapper; /** * This function will create and return our Atlas instance set up * with both our Mappers * * @return $atlasContainer */ function getAtlasContainer(){ $atlasContainer = new AtlasContainer('mysql:host=localhost;dbname=atlasorm','root', '' ); $atlasContainer->setMappers([ProductsMapper::CLASS, CategoryMapper::CLASS]); return $atlasContainer->getAtlas(); } /** * This function will return a RecordSet of all our products * * @return RecordSet */ function getAllProducts(){ $atlas = getAtlasContainer(); $productsRecordSet = $atlas ->select(ProductsMapper::CLASS) ->fetchRecordSet(); return $productsRecordSet; } /** * This function will return a Record of the Product with the specified id * * @param int * @return Record */ function getProductById( $id ){ $atlas = getAtlasContainer(); $productRecord = $atlas->fetchRecord(ProductsMapper::CLASS, $id); return $productRecord; } /** * This function will insert a new product Record * * @param string $product_name * @param int $category_name */ function addProduct( $product_name, $category_id ){ $atlas = getAtlasContainer(); //First we check if the category exists $categoryRecord = $atlas->fetchRecord(CategoryMapper::CLASS, $category_id); //if our category exists we will insert our product if( $categoryRecord ){ //Start insert $newProduct = $atlas->newRecord(ProductsMapper::CLASS); $newProduct->name = $product_name; $newProduct->category_id = $category_id; // create a transaction $transaction = $atlas->newTransaction(); $transaction->insert($newProduct); // execute the transaction $ok = $transaction->exec(); if ($ok) { echo "Transaction success."; } else { // get the exception that was thrown in the transaction $exception = $transaction->getException(); $work = $transaction->getFailure(); echo "Transaction failure. "; echo $work->getLabel() . ' threw ' . $exception->getMessage(); } //End insert }else{ echo 'The category is not valid.'; } } /** * This function will delete a product Record * * @param id $product_id */ function deleteProduct( $product_id ){ $atlas = getAtlasContainer(); //First, lets check if the product with $product_id exists $productRecord = $atlas->fetchRecord(ProductsMapper::CLASS, $product_id); if( $productRecord ){ //Delete the product $transaction = $atlas->newTransaction(); $transaction->delete($productRecord); // execute the transaction $ok = $transaction->exec(); if ($ok) { echo "Transaction success."; } else { // get the exception that was thrown in the transaction $exception = $transaction->getException(); $work = $transaction->getFailure(); echo "Transaction failure. "; echo $work->getLabel() . ' threw ' . $exception->getMessage(); } }else{ echo 'The product you are trying to delete does not exist.'; } } 

Очень простой каркас операций с использованием Atlas (без ООП и современных концепций программирования, потому что это всего лишь демонстрация).

Предостережения

Наконец, давайте посмотрим на два предостережения.

  • Атлас использует генерацию кода . Если вы измените таблицу базы данных, в которой уже был сгенерирован код, и повторно создадите ее, класс mapper не будет переопределен. Это особенно полезно, если вы написали собственный код в классе mapper.

  • Atlas находится в стадии разработки , 1.0.0-alpha1 был выпущен совсем недавно. Будьте очень осторожны при его использовании, особенно если вы пытаетесь использовать его в производственной среде, поскольку критические изменения могут повлиять на ваши проекты.

Вывод

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

Что вы думаете об Атласе? Вы дадите ему вращение?