Дата последнего изменения: 27.10.2020
У книги может быть несколько авторов, у автора может быть несколько книг. В таких случаях создается отдельная таблица с двумя полями AUTHOR_ID
и BOOK_ID
. В ORM не придется оформлять ее отдельной сущностью, достаточно описать отношение специальным полем ManyToMany:
//Файл bitrix/modules/main/lib/test/typography/booktable.php namespace Bitrix\Main\Test\Typography; use Bitrix\Main\ORM\Fields\Relations\ManyToMany; class BookTable extends \Bitrix\Main\ORM\Data\DataManager { public static function getMap() { return [ // ... (new ManyToMany('AUTHORS', AuthorTable::class)) ->configureTableName('b_book_author') ]; } }
//Файл bitrix/modules/main/lib/test/typography/authortable.php namespace Bitrix\Main\Test\Typography; use Bitrix\Main\ORM\Fields\Relations\ManyToMany; class AuthorTable extends \Bitrix\Main\ORM\Data\DataManager { public static function getMap() { return [ // ... (new ManyToMany('BOOKS', BookTable::class)) ->configureTableName('b_book_author') ]; } }
Необязательно описывать поле в обеих сущностях - можно только в одной, но тогда и доступ к данным будет осуществляться только через нее.
В конструктор передаются название поля и класс партнерской сущности. В случае примитивных отношений достаточно вызвать метод configureTableName с указанием имени таблицы, где хранятся связующие данные. Более сложный случай будет рассмотрен ниже, в примере отношений Книг с Магазинами.
В данном же случае в памяти автоматически создается временная сущность для работы с промежуточной таблицей. Фактически вы нигде не увидите ее следов, но для понимания происходящего и возможных дополнительных настроек приоткроем внутренности. Системная сущность промежуточной таблицы имеет примерно такой вид:
class ... extends \Bitrix\Main\ORM\Data\DataManager { public static function getTableName() { return 'b_book_author'; } public static function getMap() { return [ (new IntegerField('BOOK_ID')) ->configurePrimary(true), (new Reference('BOOK', BookTable::class, Join::on('this.BOOK_ID', 'ref.ID'))) ->configureJoinType('inner'), (new IntegerField('AUTHOR_ID')) ->configurePrimary(true), (new Reference('AUTHOR', AuthorTable::class, Join::on('this.AUTHOR_ID', 'ref.ID'))) ->configureJoinType('inner'), ]; } }
Это не более чем типовая сущность с референсами (направленными отношениями 1:N) к исходным сущностям-партнерам. Имена полей формируются на основе имен сущностей и имен их первичных ключей:
new IntegerField('BOOK_ID') - snake_case от Book + primary поле ID new Reference('BOOK') - snake_case от Book new IntegerField('AUTHOR_ID') - snake_case от Author + primary поле ID new Reference('AUTHOR') - snake_case от Author
Чтобы задать имена полей явно, особенно это актуально в сущностях с составными первичными ключами для избегания путаницы, используются конфигурирующие методы:
//Файл bitrix/modules/main/lib/test/typography/booktable.php namespace Bitrix\Main\Test\Typography; use Bitrix\Main\ORM\Fields\Relations\ManyToMany; class BookTable extends \Bitrix\Main\ORM\Data\DataManager { public static function getMap() { return [ // ... (new ManyToMany('AUTHORS', AuthorTable::class)) ->configureTableName('b_book_author') ->configureLocalPrimary('ID', 'MY_BOOK_ID') ->configureLocalReference('MY_BOOK') ->configureRemotePrimary('ID', 'MY_AUTHOR_ID') ->configureRemoteReference('MY_AUTHOR') ]; } }
//Файл bitrix/modules/main/lib/test/typography/authortable.php namespace Bitrix\Main\Test\Typography; use Bitrix\Main\ORM\Fields\Relations\ManyToMany; class AuthorTable extends \Bitrix\Main\ORM\Data\DataManager { public static function getMap() { return [ // ... (new ManyToMany('BOOKS', BookTable::class)) ->configureTableName('b_book_author') ->configureLocalPrimary('ID', 'MY_AUTHOR_ID') ->configureLocalReference('MY_AUTHOR'), ->configureRemotePrimary('ID', 'MY_BOOK_ID') ->configureRemoteReference('MY_BOOK') ]; } }
Метод configureLocalPrimary указывает, как будет называться привязка к полю из первичного ключа текущей сущности, аналогично configureRemotePrimary задает соответствие полей первичного ключа сущности-партнера. Методы configureLocalReference и configureRemoteReference задают имена референсов к исходным сущностям. При описанной выше конфигурации системная сущность отношений будет иметь примерно такой вид:
class ... extends \Bitrix\Main\ORM\Data\DataManager { public static function getTableName() { return 'b_book_author'; } public static function getMap() { return [ (new IntegerField('MY_BOOK_ID')) ->configurePrimary(true), (new Reference('MY_BOOK', BookTable::class, Join::on('this.MY_BOOK_ID', 'ref.ID'))) ->configureJoinType('inner'), (new IntegerField('MY_AUTHOR_ID')) ->configurePrimary(true), (new Reference('MY_AUTHOR', AuthorTable::class, Join::on('this.MY_AUTHOR_ID', 'ref.ID'))) ->configureJoinType('inner'), ]; } }
Как и в случае Reference с OneToMany, здесь тоже можно переопределить тип джойна методом configureJoinType (значение по умолчанию - "left"):
(new ManyToMany('AUTHORS', AuthorTable::class)) ->configureTableName('b_book_author') ->configureJoinType('inner')
Чтение данных работает аналогично отношениям 1:N:
// выборка со стороны авторов $author = \Bitrix\Main\Test\Typography\AuthorTable::getByPrimary(18, [ 'select' => ['*', 'BOOKS'] ])->fetchObject(); foreach ($author->getBooks() as $book) { echo $book->getTitle(); } // цикл выведет "Title 1" и "Title 2" // выборка со сороны книг $book = \Bitrix\Main\Test\Typography\BookTable::getByPrimary(2, [ 'select' => ['*', 'AUTHORS'] ])->fetchObject(); foreach ($book->getAuthors() as $author) { echo $author->getLastName(); } // цикл выведет "Last name 17" и "Last name 18"
Выборка объектов вместо массивов вновь выгодно отличается тем, что не происходит "двоения" данных, как это происходит с массивами:
$author = \Bitrix\Main\Test\Typography\AuthorTable::getByPrimary(18, [ 'select' => ['*', 'BOOK_' => 'BOOKS'] ])->fetchAll(); // вернет Array ( [0] => Array [ID] => 18 [NAME] => Name 18 [LAST_NAME] => Last name 18 [BOOK_ID] => 1 [BOOK_TITLE] => Title 1 [BOOK_PUBLISHER_ID] => 253 [BOOK_ISBN] => 978-3-16-148410-0 [BOOK_IS_ARCHIVED] => Y ) [1] => Array ( [ID] => 18 [NAME] => Name 18 [LAST_NAME] => Last name 18 [BOOK_ID] => 2 [BOOK_TITLE] => Title 2 [BOOK_PUBLISHER_ID] => 253 [BOOK_ISBN] => 456-1-05-586920-1 [BOOK_IS_ARCHIVED] => N ) )
Создание связи между объектами двух сущностей происходит так же, как в случае с отношениями 1:N:
// со стороны авторов $author = \Bitrix\Main\Test\Typography\AuthorTable::getByPrimary(17) ->fetchObject(); $book = \Bitrix\Main\Test\Typography\BookTable::getByPrimary(1) ->fetchObject(); $author->addToBooks($book); $author->save(); // со стороны книг $author = \Bitrix\Main\Test\Typography\AuthorTable::getByPrimary(17) ->fetchObject(); $book = \Bitrix\Main\Test\Typography\BookTable::getByPrimary(1) ->fetchObject(); $book->addToAuthors($author); $book->save();
Методы removeFrom и removeAll работают тоже аналогично.
Для массивов подобных конструкций не предусмотрено. Как связывать сущности, используя массивы - смотрите ниже в примере отношений Книг с Магазинами.
|
Когда есть дополнительные данные (количество книг в наличие), а не только первичные ключи исходных сущностей, такое отношение следует описать отдельной сущностью:
namespace Bitrix\Main\Test\Typography; use Bitrix\Main\ORM\Data\DataManager; use Bitrix\Main\ORM\Fields\IntegerField; use Bitrix\Main\ORM\Fields\Relations\Reference; use Bitrix\Main\ORM\Query\Join; class StoreBookTable extends DataManager { public static function getTableName() { return 'b_store_book'; } public static function getMap() { return [ (new IntegerField('STORE_ID')) ->configurePrimary(true), (new Reference('STORE', StoreTable::class, Join::on('this.STORE_ID', 'ref.ID'))) ->configureJoinType('inner'), (new IntegerField('BOOK_ID')) ->configurePrimary(true), (new Reference('BOOK', BookTable::class, Join::on('this.BOOK_ID', 'ref.ID'))) ->configureJoinType('inner'), (new IntegerField('QUANTITY')) ->configureDefaultValue(0) ]; } }
Если для простых отношений без вспомогательных данных использовались поля ManyToMany, здесь их использование будет сильно ограничено. Можно будет создавать и удалять связи, но не будет доступа к вспомогательному полю QUANTITY
. С помощью removeFrom*() можно будет удалить связь, с помощью addTo*() можно будет добавить связь со значением QUANTITY
только по умолчанию, и не будет возможности обновить значение QUANTITY
. Поэтому в таких случаях более гибким будет использование непосредственно сущности-посредника:
// объект книги $book = \Bitrix\Main\Test\Typography\BookTable::getByPrimary(1) ->fetchObject(); // объект магазина $store = \Bitrix\Main\Test\Typography\StoreTable::getByPrimary(34) ->fetchObject(); // новый объект связи книги с магазином $item = \Bitrix\Main\Test\Typography\StoreBookTable::createObject() ->setBook($book) ->setStore($store) ->setQuantity(5); // сохранение $item->save();
Обновление количества книг:
// объект существующей связи $item = \Bitrix\Main\Test\Typography\StoreBookTable::getByPrimary([ 'STORE_ID' => 33, 'BOOK_ID' => 2 ])->fetchObject(); // обновление количества $item->setQuantity(12); // сохранение $item->save();
Удаление связи:
// объект существующей связи $item = \Bitrix\Main\Test\Typography\StoreBookTable::getByPrimary([ 'STORE_ID' => 33, 'BOOK_ID' => 2 ])->fetchObject(); // удаление $item->delete();
То есть, работа с объектом связи ведется как с объектом любой другой сущности. Для массивов тоже следует использовать стандартные подходы по работе с данными:
// добавление \Bitrix\Main\Test\Typography\StoreBookTable::add([ 'STORE_ID' => 34, 'BOOK_ID' => 1, 'QUANTITY' => 5 ]); // обновление \Bitrix\Main\Test\Typography\StoreBookTable::update( ['STORE_ID' => 34, 'BOOK_ID' => 1], ['QUANTITY' => 12] ); // удаление \Bitrix\Main\Test\Typography\StoreBookTable::delete( ['STORE_ID' => 34, 'BOOK_ID' => 1] );
Выше было упомянуто, что использовать поле ManyToMany в случае со вспомогательными данными - непродуктивно. Правильнее будет использовать тип OneToMany:
//Файл bitrix/modules/main/lib/test/typography/booktable.php namespace Bitrix\Main\Test\Typography; use Bitrix\Main\ORM\Fields\Relations\OneToMany; class BookTable extends \Bitrix\Main\ORM\Data\DataManager { public static function getMap() { return [ // ... (new OneToMany('STORE_ITEMS', StoreBookTable::class, 'BOOK')) ]; } }
//Файл bitrix/modules/main/lib/test/typography/storetable.php namespace Bitrix\Main\Test\Typography; use Bitrix\Main\ORM\Fields\Relations\OneToMany; class StoreTable extends \Bitrix\Main\ORM\Data\DataManager { public static function getMap() { return [ // ... (new OneToMany('BOOK_ITEMS', StoreBookTable::class, 'STORE')) ]; } }
В таком случае выборка ничем не будет отличаться от отношений 1:N, только в этот раз будут возвращаться объекты отношения StoreBook, а не сущности-партнера:
$book = \Bitrix\Main\Test\Typography\BookTable::getByPrimary(1, [ 'select' => ['*', 'STORE_ITEMS'] ])->fetchObject(); foreach ($book->getStoreItems() as $storeItem) { printf( 'store "%s" has %s of book "%s"', $storeItem->getStoreId(), $storeItem->getQuantity(), $storeItem->getBookId() ); // выведет store "33" has 4 of book "1" }