transactional save

This commit is contained in:
Yurii
2026-03-03 16:01:15 +02:00
parent f298d6ff5a
commit 19d3789c24
5 changed files with 170 additions and 1 deletions

View File

@@ -69,6 +69,13 @@ class Database extends RDBRepository
*/
protected $hooksDisabled = false;
/**
* To save and remove in a DB transaction.
*
* @since 9.4.0
*/
protected bool $transactionalSave = false;
/** @var ?array<string, mixed> */
private $restoreData = null;
/** @var Metadata */
@@ -97,6 +104,8 @@ class Database extends RDBRepository
$this->recordIdGenerator = $recordIdGenerator;
$this->hooksDisabled = $this->hooksDisabled || $metadata->get("entityDefs.$entityType.hooksDisabled");
$this->transactionalSave = $this->transactionalSave ||
$metadata->get("entityDefs.$entityType.transactionalSave");
$hookMediator = null;
@@ -126,6 +135,23 @@ class Database extends RDBRepository
* @param array<string, mixed> $options
*/
public function save(Entity $entity, array $options = []): void
{
if ($this->transactionalSave) {
$this->entityManager->getTransactionManager()->run(function () use ($entity, $options) {
$this->saveInternal($entity, $options);
});
return;
}
$this->saveInternal($entity, $options);
}
/**
* @param TEntity $entity
* @param array<string, mixed> $options
*/
private function saveInternal(Entity $entity, array $options = []): void
{
if (
$entity->isNew() &&
@@ -144,6 +170,31 @@ class Database extends RDBRepository
parent::save($entity, $options);
}
/**
* Remove a record (mark as deleted).
*/
public function remove(Entity $entity, array $options = []): void
{
if ($this->transactionalSave) {
$this->entityManager->getTransactionManager()->run(function () use ($entity, $options) {
$this->removeInternal($entity, $options);
});
return;
}
$this->removeInternal($entity, $options);
}
/**
* @param TEntity $entity
* @param array<string, mixed> $options
*/
private function removeInternal(Entity $entity, array $options = []): void
{
parent::remove($entity, $options);
}
/**
* @deprecated Do not extend. Use hooks.
*

View File

@@ -35,6 +35,13 @@
"type": "bool",
"tooltip": true
}
},
"transactionalSave": {
"location": "entityDefs",
"fieldDefs": {
"type": "bool",
"tooltip": true
}
}
},
"Contact": {
@@ -73,6 +80,13 @@
"type": "bool",
"tooltip": true
}
},
"transactionalSave": {
"location": "entityDefs",
"fieldDefs": {
"type": "bool",
"tooltip": true
}
}
},
"Lead": {
@@ -111,6 +125,13 @@
"type": "bool",
"tooltip": true
}
},
"transactionalSave": {
"location": "entityDefs",
"fieldDefs": {
"type": "bool",
"tooltip": true
}
}
},
"Opportunity": {
@@ -134,6 +155,13 @@
"type": "bool",
"tooltip": true
}
},
"transactionalSave": {
"location": "entityDefs",
"fieldDefs": {
"type": "bool",
"tooltip": true
}
}
},
"Document": {
@@ -157,6 +185,13 @@
"type": "bool",
"tooltip": true
}
},
"transactionalSave": {
"location": "entityDefs",
"fieldDefs": {
"type": "bool",
"tooltip": true
}
}
},
"Case": {
@@ -180,6 +215,13 @@
"type": "bool",
"tooltip": true
}
},
"transactionalSave": {
"location": "entityDefs",
"fieldDefs": {
"type": "bool",
"tooltip": true
}
}
},
"KnowledgeBaseArticle": {
@@ -203,6 +245,13 @@
"type": "bool",
"tooltip": true
}
},
"transactionalSave": {
"location": "entityDefs",
"fieldDefs": {
"type": "bool",
"tooltip": true
}
}
},
"Meeting": {
@@ -248,6 +297,13 @@
"type": "bool",
"tooltip": true
}
},
"transactionalSave": {
"location": "entityDefs",
"fieldDefs": {
"type": "bool",
"tooltip": true
}
}
},
"Call": {
@@ -293,6 +349,13 @@
"type": "bool",
"tooltip": true
}
},
"transactionalSave": {
"location": "entityDefs",
"fieldDefs": {
"type": "bool",
"tooltip": true
}
}
},
"Task": {
@@ -334,6 +397,13 @@
"type": "bool",
"tooltip": true
}
},
"transactionalSave": {
"location": "entityDefs",
"fieldDefs": {
"type": "bool",
"tooltip": true
}
}
},
"TargetList": {
@@ -350,6 +420,13 @@
"type": "bool",
"tooltip": true
}
},
"transactionalSave": {
"location": "entityDefs",
"fieldDefs": {
"type": "bool",
"tooltip": true
}
}
},
"@Event": {
@@ -427,6 +504,13 @@
"type": "bool",
"tooltip": true
}
},
"transactionalSave": {
"location": "entityDefs",
"fieldDefs": {
"type": "bool",
"tooltip": true
}
}
}
}

View File

@@ -57,7 +57,8 @@
"aclContactLink": "ACL Contact Link",
"aclAccountLink": "ACL Account Link",
"categories": "Categories",
"lockable": "Lockable"
"lockable": "Lockable",
"transactionalSave": "Transactional Save"
},
"options": {
"type": {
@@ -100,6 +101,7 @@
"beforeSaveApiScript": "A script called on create and update API requests before an entity is saved. Use for custom validation and duplicate checking."
},
"tooltips": {
"transactionalSave": "Record saves and removals will be wrapped into DB transactions.",
"lockable": "Enables the ability to lock records.",
"categories": "Enable the category tree feature. Records can be placed into categories.",
"aclContactLink": "The link with Contact to use when applying access control for portal users.",

View File

@@ -76,6 +76,13 @@
"type": "bool",
"tooltip": true
}
},
"transactionalSave": {
"location": "entityDefs",
"fieldDefs": {
"type": "bool",
"tooltip": true
}
}
},
"@Person": {
@@ -132,6 +139,13 @@
"type": "bool",
"tooltip": true
}
},
"transactionalSave": {
"location": "entityDefs",
"fieldDefs": {
"type": "bool",
"tooltip": true
}
}
},
"@Base": {
@@ -195,6 +209,13 @@
"type": "bool",
"tooltip": true
}
},
"transactionalSave": {
"location": "entityDefs",
"fieldDefs": {
"type": "bool",
"tooltip": true
}
}
},
"@BasePlus": {
@@ -258,6 +279,13 @@
"type": "bool",
"tooltip": true
}
},
"transactionalSave": {
"location": "entityDefs",
"fieldDefs": {
"type": "bool",
"tooltip": true
}
}
}
}

View File

@@ -92,6 +92,10 @@
"type": "boolean",
"description": "Disable hooks. As of v8.2."
},
"transactionalSave": {
"type": "boolean",
"description": "Enables record saving and removing in a DB transaction. As of v9.4."
},
"entityClassName": {
"type": "string",
"description": "An entity. Should implement Espo\\ORM\\Entity. Usually should extend Espo\\Core\\ORM\\Entity. As of v8.2."