orm: related partially loaded

This commit is contained in:
Yuri Kuznetsov
2024-11-14 16:39:51 +02:00
parent 79ef327953
commit 28e33813a2
11 changed files with 328 additions and 19 deletions
+14 -2
View File
@@ -34,6 +34,8 @@ use Espo\Core\Binding\BindingContainer;
use Espo\Core\Binding\BindingContainerBuilder;
use Espo\Core\Binding\BindingData;
use Espo\Core\InjectableFactory;
use Espo\ORM\DataLoader\Loader;
use Espo\ORM\DataLoader\RDBLoader;
use Espo\ORM\Entity;
use Espo\ORM\EntityFactory as EntityFactoryInterface;
use Espo\ORM\EntityManager;
@@ -95,11 +97,19 @@ class EntityFactory implements EntityFactoryInterface
->build()
);
$loader = $this->injectableFactory->createWithBinding(
RDBLoader::class,
BindingContainerBuilder::create()
->bindInstance(EntityManager::class, $this->entityManager)
->build()
);
$bindingContainer = $this->getBindingContainer(
$className,
$entityType,
$defs,
$relations
$relations,
$loader,
);
$entity = $this->injectableFactory->createWithBinding($className, $bindingContainer);
@@ -131,6 +141,7 @@ class EntityFactory implements EntityFactoryInterface
string $entityType,
array $defs,
Relations $relations,
Loader $loader,
): BindingContainer {
if (!$this->entityManager || !$this->valueAccessorFactory) {
@@ -147,7 +158,8 @@ class EntityFactory implements EntityFactoryInterface
->bindInstance(EntityManager::class, $this->entityManager)
->bindInstance(ValueAccessorFactory::class, $this->valueAccessorFactory)
->bindInstance(Helper::class, $this->helper)
->bindInstance(Relations::class, $relations);
->bindInstance(Relations::class, $relations)
->bindInstance(Loader::class, $loader);
return new BindingContainer($data);
}
@@ -36,6 +36,7 @@ use Espo\Core\Field\LinkParent;
use Espo\Core\Name\Field;
use Espo\Core\ORM\Entity;
use Espo\Entities\User;
use Espo\ORM\Entity as OrmEntity;
class Call extends Entity
{
@@ -154,4 +155,20 @@ class Call extends Entity
return $this;
}
public function setAccount(Link|Account|null $account): self
{
return $this->setRelatedLinkOrEntity('account', $account);
}
public function getAccount(): ?Account
{
/** @var ?Account */
return $this->relations->getOne('account');
}
public function getParent(): ?OrmEntity
{
return $this->relations->getOne(Field::PARENT);
}
}
@@ -36,6 +36,7 @@ use Espo\Core\Field\LinkParent;
use Espo\Core\Name\Field;
use Espo\Core\ORM\Entity;
use Espo\Entities\User;
use Espo\ORM\Entity as OrmEntity;
class Meeting extends Entity
{
@@ -159,4 +160,20 @@ class Meeting extends Entity
return $this;
}
public function setAccount(Link|Account|null $account): self
{
return $this->setRelatedLinkOrEntity('account', $account);
}
public function getAccount(): ?Account
{
/** @var ?Account */
return $this->relations->getOne('account');
}
public function getParent(): ?OrmEntity
{
return $this->relations->getOne(Field::PARENT);
}
}
@@ -107,10 +107,10 @@ class Opportunity extends Entity
$this->set('probability', $probability);
}
public function getAccount(): ?Link
public function getAccount(): ?Account
{
/** @var ?Link */
return $this->getValueObject('account');
/** @var ?Account */
return $this->relations->getOne('account');
}
/**
+18 -9
View File
@@ -38,6 +38,7 @@ use Espo\Core\ORM\Entity;
use Espo\Entities\Attachment;
use Espo\Entities\User;
use Espo\ORM\Collection;
use Espo\ORM\Entity as OrmEntity;
class Task extends Entity
{
@@ -99,15 +100,7 @@ class Task extends Entity
public function setParent(Entity|LinkParent|null $parent): self
{
if ($parent instanceof LinkParent) {
$this->setValueObject(Field::PARENT, $parent);
return $this;
}
$this->relations->set(Field::PARENT, $parent);
return $this;
return $this->setRelatedLinkOrEntity(Field::PARENT, $parent);
}
/**
@@ -130,4 +123,20 @@ class Task extends Entity
return $this;
}
public function setAccount(Link|Account|null $account): self
{
return $this->setRelatedLinkOrEntity('account', $account);
}
public function getAccount(): ?Account
{
/** @var ?Account */
return $this->relations->getOne('account');
}
public function getParent(): ?OrmEntity
{
return $this->relations->getOne(Field::PARENT);
}
}
+62 -3
View File
@@ -29,6 +29,8 @@
namespace Espo\ORM;
use Espo\ORM\DataLoader\EmptyLoader;
use Espo\ORM\DataLoader\Loader;
use Espo\ORM\Name\Attribute;
use Espo\ORM\Relation\EmptyRelations;
use Espo\ORM\Relation\Relations;
@@ -57,6 +59,7 @@ class BaseEntity implements Entity
protected ?EntityManager $entityManager;
private ?ValueAccessor $valueAccessor = null;
readonly protected Relations $relations;
readonly private Loader $loader;
/** @var array<string, bool> */
private array $writtenMap = [];
@@ -69,6 +72,8 @@ class BaseEntity implements Entity
/** @var array<string, mixed> */
private array $valuesContainer = [];
private bool $isPartiallyLoaded = false;
/**
* An ID.
*/
@@ -87,6 +92,7 @@ class BaseEntity implements Entity
?EntityManager $entityManager = null,
?ValueAccessorFactory $valueAccessorFactory = null,
?Relations $relations = null,
?Loader $loader = null,
) {
$this->entityType = $entityType;
$this->entityManager = $entityManager;
@@ -99,6 +105,7 @@ class BaseEntity implements Entity
}
$this->relations = $relations ?? new EmptyRelations();
$this->loader = $loader ?? new EmptyLoader();
}
/**
@@ -226,9 +233,17 @@ class BaseEntity implements Entity
$method = '_get' . ucfirst($attribute);
if (method_exists($this, $method)) {
if ($this->isPartiallyLoaded) {
$this->loadPartiallyLoaded();
}
return $this->$method();
}
if ($this->checkAttributeToFullyLoad($attribute)) {
$this->loadPartiallyLoaded();
}
if ($this->hasAttribute($attribute) && $this->hasInContainer($attribute)) {
return $this->getFromContainer($attribute);
}
@@ -351,14 +366,18 @@ class BaseEntity implements Entity
$method = '_has' . ucfirst($attribute);
if (method_exists($this, $method)) {
if ($this->isPartiallyLoaded) {
$this->loadPartiallyLoaded();
}
return (bool) $this->$method();
}
if (array_key_exists($attribute, $this->valuesContainer)) {
return true;
if ($this->checkAttributeToFullyLoad($attribute)) {
$this->loadPartiallyLoaded();
}
return false;
return $this->hasInContainer($attribute);
}
/**
@@ -893,6 +912,10 @@ class BaseEntity implements Entity
return !is_null($this->id);
}
if ($this->checkAttributeFetchedToFullyLoad($attribute)) {
$this->loadPartiallyLoaded();
}
return $this->hasInFetchedContainer($attribute);
}
@@ -919,6 +942,17 @@ class BaseEntity implements Entity
$this->writtenMap = [];
}
/**
* Set as partially loaded. For internal use.
*
* @internal
* @since 9.0.0
*/
public function setAsPartiallyLoaded(): void
{
$this->isPartiallyLoaded = true;
}
/**
* Set an entity as fetched. All current attribute values will be set as those that are fetched
* from DB.
@@ -1091,4 +1125,29 @@ class BaseEntity implements Entity
$this->populateFromArrayItem($attribute, $value);
}
}
private function checkAttributeToFullyLoad(string $attribute): bool
{
return
!$this->isNew() &&
$this->isPartiallyLoaded &&
$this->hasAttribute($attribute) &&
!$this->hasInContainer($attribute);
}
private function checkAttributeFetchedToFullyLoad(string $attribute): bool
{
return
!$this->isNew() &&
$this->isPartiallyLoaded &&
$this->hasAttribute($attribute) &&
!$this->hasInFetchedContainer($attribute);
}
private function loadPartiallyLoaded(): void
{
$this->isPartiallyLoaded = false;
$this->loader->load($this);
}
}
@@ -0,0 +1,38 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM Open Source CRM application.
* Copyright (C) 2014-2024 Yurii Kuznietsov, Taras Machyshyn, Oleksii Avramenko
* Website: https://www.espocrm.com
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* The interactive user interfaces in modified source and object code versions
* of this program must display Appropriate Legal Notices, as required under
* Section 5 of the GNU Affero General Public License version 3.
*
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
************************************************************************/
namespace Espo\ORM\DataLoader;
use Espo\ORM\Entity;
class EmptyLoader implements Loader
{
public function load(Entity $entity): void
{}
}
@@ -0,0 +1,37 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM Open Source CRM application.
* Copyright (C) 2014-2024 Yurii Kuznietsov, Taras Machyshyn, Oleksii Avramenko
* Website: https://www.espocrm.com
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* The interactive user interfaces in modified source and object code versions
* of this program must display Appropriate Legal Notices, as required under
* Section 5 of the GNU Affero General Public License version 3.
*
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
************************************************************************/
namespace Espo\ORM\DataLoader;
use Espo\ORM\Entity;
interface Loader
{
public function load(Entity $entity): void;
}
@@ -0,0 +1,65 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM Open Source CRM application.
* Copyright (C) 2014-2024 Yurii Kuznietsov, Taras Machyshyn, Oleksii Avramenko
* Website: https://www.espocrm.com
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* The interactive user interfaces in modified source and object code versions
* of this program must display Appropriate Legal Notices, as required under
* Section 5 of the GNU Affero General Public License version 3.
*
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
************************************************************************/
namespace Espo\ORM\DataLoader;
use Espo\ORM\Entity;
use Espo\ORM\EntityManager;
class RDBLoader implements Loader
{
public function __construct(
private EntityManager $entityManager,
) {}
public function load(Entity $entity): void
{
$loaded = $this->entityManager->getEntityById($entity->getEntityType(), $entity->getId());
if (!$loaded) {
return;
}
foreach ($loaded->getAttributeList() as $attribute) {
if (!$loaded->has($attribute)) {
continue;
}
$value = $loaded->get($attribute);
if (!$entity->hasFetched($attribute)) {
$entity->setFetched($attribute, $value);
}
if (!$entity->has($attribute)) {
$entity->set($attribute, $value);
}
}
}
}
@@ -29,9 +29,11 @@
namespace Espo\ORM\Relation;
use Espo\ORM\BaseEntity;
use Espo\ORM\Entity;
use Espo\ORM\EntityCollection;
use Espo\ORM\EntityManager;
use Espo\ORM\Name\Attribute;
use Espo\ORM\Query\Part\Order;
use Espo\ORM\Type\RelationType;
use LogicException;
@@ -221,6 +223,12 @@ class RDBRelations implements Relations
return null;
}
$foreignEntity = $this->getPartiallyLoadedForeignEntity($relation);
if ($foreignEntity) {
return $foreignEntity;
}
return $this->entityManager
->getRelation($this->entity, $relation)
->findOne();
@@ -284,4 +292,45 @@ class RDBRelations implements Relations
->tryGetRelation($relation)
?->getType();
}
private function getPartiallyLoadedForeignEntity(string $relation): ?BaseEntity
{
if (!$this->entity) {
throw new LogicException();
}
$relationDefs = $this->entityManager
->getDefs()
->getEntity($this->entity->getEntityType())
->getRelation($relation);
$relationType = $relationDefs->getType();
$id = null;
$foreignEntityType = null;
if ($relationType === RelationType::BELONGS_TO) {
$foreignEntityType = $relationDefs->getForeignEntityType();
$id = $this->entity->get($relation . 'Id');
} else if ($relationType === RelationType::BELONGS_TO_PARENT) {
$foreignEntityType = $this->entity->get($relation . 'Type');
$id = $this->entity->get($relation . 'Id');
}
if (!$foreignEntityType || !$id) {
return null;
}
$foreignEntity = $this->entityManager->getNewEntity($foreignEntityType);
if (!$foreignEntity instanceof BaseEntity) {
return null;
}
$foreignEntity->set(Attribute::ID, $id);
$foreignEntity->setAsFetched();
$foreignEntity->setAsPartiallyLoaded();
return $foreignEntity;
}
}
+8 -2
View File
@@ -57,7 +57,9 @@ class RelationsTest extends BaseTestCase
$this->assertInstanceOf(OpportunityExtended::class, $opp);
$this->assertNull($opp->getRelatedAccount());
$account = $em->createEntity(Account::ENTITY_TYPE);
/** @var Account $account */
$account = $em->createEntity(Account::ENTITY_TYPE, ['name' => 'Account 1']);
$opp = $em->createEntity(Opportunity::ENTITY_TYPE, [
'accountId' => $account->getId(),
]);
@@ -76,6 +78,7 @@ class RelationsTest extends BaseTestCase
$this->assertInstanceOf(Account::class, $account2);
$this->assertNotSame($account1, $account2);
$this->assertEquals($account->getId(), $account2->getId());
$this->assertEquals($account->getName(), $account2->getName());
$em->saveEntity($opp);
@@ -130,7 +133,7 @@ class RelationsTest extends BaseTestCase
$em = $this->getEntityManager();
/** @var Account $account */
$account = $em->createEntity(Account::ENTITY_TYPE);
$account = $em->createEntity(Account::ENTITY_TYPE, ['name' => 'Account 1']);
// belongsTo
@@ -140,6 +143,7 @@ class RelationsTest extends BaseTestCase
$em->refreshEntity($opp);
$this->assertEquals($account->getId(), $opp->getAccount()->getId());
$this->assertEquals($account->getName(), $opp->getAccount()->getName());
$opp->setAccount(null);
$em->saveEntity($opp, [SaveOption::SKIP_ALL => true]);
@@ -156,6 +160,8 @@ class RelationsTest extends BaseTestCase
$this->assertEquals($account->getId(), $task->get('parentId'));
$this->assertEquals($account->getEntityType(), $task->get('parentType'));
$this->assertEquals($account->getId(), $task->getParent()->getId());
$this->assertEquals($account->getName(), $task->getParent()->get('name'));
$task = $em->getRDBRepositoryByClass(Task::class)->getNew();
$task->setParent(null);