orm: related partially loaded
This commit is contained in:
@@ -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');
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user