Views: 625
Last Modified: 30.03.2022

These are primitive relations without auxiliary data

A book can have several authors and an author can have several books. In such cases, a separate table is created with two fields AUTHOR_ID and BOOK_ID. The ORM won't have to issue it as a separate entity, its sufficient to describe the relation by the special field ManyToMany:

//File 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')
		];
	}
}
//File 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')
		];
	}
}

Field description for both entities is optional - a single field can have a description; however, the access to data is granted only to this one field.

The constructor passes the field name and partner entity class. In case of primitive relations, it's sufficient to call the method configureTableName with indicated table name, storing related data. The more complex case will be overviewed below, in the example for Books and Stores relations.

In this case, system memory automatically creates a temporary entity for handling a staging table. In actuality, you won't see its traces anywhere, but for purposes of understanding the process and possible additional settings, we overview such case. System entity for staging table has the following approximate contents:

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'),
		];
	}
}

This is no more than standard entity with references (directed relations 1:N) to the source partner entities. Field names are generated based on entity names and their primary keys:

new IntegerField('BOOK_ID') - snake_case from Book + primary field ID
new Reference('BOOK') - snake_case from Book
new IntegerField('AUTHOR_ID') - snake_case from Author + primary field ID
new Reference('AUTHOR') - snake_case from Author

To directly set field name, use the following configuration methods (this is especially pertinent in entities with composite primary keys to avoid confusion):

//File 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')
		];
	}
}
//File 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')
		];
	}
}

The method configureLocalPrimary indicates how the field relation from current entity's primary key will be named. In similar fashion configureRemotePrimary indicates the primary key fields for partner entity key. Methods configureLocalReference and configureRemoteReference set reference names to source entities. For the configuration described above, the relations system entity will be approximately as follows:

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'),
		];
	}
}

Just as in case with Reference and OneToMany, you can also redefine type of join by the method configureJoinType (default value - "left"):

(new ManyToMany('AUTHORS', AuthorTable::class))
	->configureTableName('b_book_author')
	->configureJoinType('inner')

Data reading operates similarly to the relations 1:N:

// fetched from author side
$author = \Bitrix\Main\Test\Typography\AuthorTable::getByPrimary(18, [
	'select' => ['*', 'BOOKS']
])->fetchObject();

foreach ($author->getBooks() as $book)
{
	echo $book->getTitle();
}
// prints "Title 1" and "Title 2"

// retrieved from side of books
$book = \Bitrix\Main\Test\Typography\BookTable::getByPrimary(2, [
	'select' => ['*', 'AUTHORS']
])->fetchObject();

foreach ($book->getAuthors() as $author)
{
	echo $author->getLastName();
}
// prints "Last name 17" and "Last name 18"

Once again, retrieving objects instead of arrays is more advantageous due to not having "duplicated" data, as it happens with arrays:

$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
	)
)

Creating relations between objects of two entities occurs in the same manner as in case with relations 1:N:

// from author side
$author = \Bitrix\Main\Test\Typography\AuthorTable::getByPrimary(17)
	->fetchObject();

$book = \Bitrix\Main\Test\Typography\BookTable::getByPrimary(1)
	->fetchObject();

$author->addToBooks($book);

$author->save();


// from books' side
$author = \Bitrix\Main\Test\Typography\AuthorTable::getByPrimary(17)
	->fetchObject();

$book = \Bitrix\Main\Test\Typography\BookTable::getByPrimary(1)
	->fetchObject();

$book->addToAuthors($author);

$book->save();

Methods removeFrom and removeAll operate in the same manner.

No constructions are designed for such constructor arrays. See the example below for Books with Stores to overview how to bind entities using the arrays.

Relations with auxiliary data


STORE_ID BOOK_ID QUANTITY
33 14
33 20
43 29

When there is additional data (number of books in stock) and not only primary keys for source entities, such relation must be described by a separate entity:

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'))
			A	->configurePrimary(true),

			(new Reference('BOOK', BookTable::class,
				Join::on('this.BOOK_ID', 'ref.ID')))
				->configureJoinType('inner'),

			(new IntegerField('QUANTITY'))
				->configureDefaultValue(0)
		];
	}
}

ManyToMany fields were used for simple relations, but here their use will be significantly limited. Relations can be created and deleted, but without access to auxiliary field QUANTITY. Use of removeFrom*() can delete the relation and addTo*() can add the relation with QUANTITY value only by default, without the option to update the QUANTITY value. That's why in such cases more flexible approach would be using proxy entity directly:

// book object
$book = \Bitrix\Main\Test\Typography\BookTable::getByPrimary(1)
	->fetchObject();

// store object
$store = \Bitrix\Main\Test\Typography\StoreTable::getByPrimary(34)
	->fetchObject();

// new book and store relations object
$item = \Bitrix\Main\Test\Typography\StoreBookTable::createObject()
	->setBook($book)
	->setStore($store)
	->setQuantity(5);

// saving
$item->save();

Number of books update:

// existing relation object
$item = \Bitrix\Main\Test\Typography\StoreBookTable::getByPrimary([
	'STORE_ID' => 33, 'BOOK_ID' => 2
])->fetchObject();

// quantity update
$item->setQuantity(12);

// saving
$item->save();

Deleting the relation:

// existing relation object
$item = \Bitrix\Main\Test\Typography\StoreBookTable::getByPrimary([
	'STORE_ID' => 33, 'BOOK_ID' => 2
])->fetchObject();

// deleting
$item->delete();

The relation object is handled in the same manner as objects of any other entities. Arrays must also use standard approaches for data handling:

// adding
\Bitrix\Main\Test\Typography\StoreBookTable::add([
	'STORE_ID' => 34, 'BOOK_ID' => 1, 'QUANTITY' => 5
]);

// updating
\Bitrix\Main\Test\Typography\StoreBookTable::update(
	['STORE_ID' => 34, 'BOOK_ID' => 1],
	['QUANTITY' => 12]
);

// deleting
\Bitrix\Main\Test\Typography\StoreBookTable::delete(
	['STORE_ID' => 34, 'BOOK_ID' => 1]
);

As mentioned above, using the field ManyToMany in case with auxiliary data is unproductive. More correct way is to use the type OneToMany:

//File 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'))
		];
	}
}
//File 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'))
		];
	}
}

In such case fetched data won't be different from the relations 1:N, only in this case, returns StoreBook relations objects and not partner-entities:

$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()
	);
	// prints store "33" has 4 of book "1"
}




Courses developed by Bitrix24