UoW changes updates to README. UoW tests added
This commit is contained in:
parent
a5182ae282
commit
d8cae37a4d
28
README.md
28
README.md
|
|
@ -535,6 +535,34 @@ Caveats and notes:
|
||||||
- If you need to force an immediate operation within a UoW (for advanced cases), DAOs use an internal `UnitOfWork::suspendDuring()` helper to avoid re-enqueueing nested calls.
|
- If you need to force an immediate operation within a UoW (for advanced cases), DAOs use an internal `UnitOfWork::suspendDuring()` helper to avoid re-enqueueing nested calls.
|
||||||
- The UoW MVP does not yet apply cascade rules; ordering is per-connection in enqueue order.
|
- The UoW MVP does not yet apply cascade rules; ordering is per-connection in enqueue order.
|
||||||
|
|
||||||
|
### Relation-aware delete ordering and cascades (MVP)
|
||||||
|
|
||||||
|
- When you enable a UoW and enqueue a parent delete via `deleteById()`, Pairity will automatically delete child rows/documents first for relations marked with a cascade flag, then execute the parent delete. This ensures referential integrity without manual orchestration.
|
||||||
|
|
||||||
|
- Supported relation types for cascades: `hasMany`, `hasOne`.
|
||||||
|
|
||||||
|
- Enable cascades by adding a flag to the relation metadata in your DAO:
|
||||||
|
|
||||||
|
```php
|
||||||
|
protected function relations(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'posts' => [
|
||||||
|
'type' => 'hasMany',
|
||||||
|
'dao' => PostDao::class,
|
||||||
|
'foreignKey' => 'user_id',
|
||||||
|
'localKey' => 'id',
|
||||||
|
'cascadeDelete' => true, // or: 'cascade' => ['delete' => true]
|
||||||
|
],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Behavior details:
|
||||||
|
- Inside `UnitOfWork::run(...)`, enqueuing `UserDao->deleteById($id)` will synthesize and run `PostDao->deleteBy(['user_id' => $id])` before deleting the user.
|
||||||
|
- Works for both SQL DAOs and Mongo DAOs.
|
||||||
|
- Current MVP focuses on delete cascades; cascades for updates and more advanced ordering rules can be added later.
|
||||||
|
|
||||||
## Roadmap
|
## Roadmap
|
||||||
|
|
||||||
- Relations enhancements:
|
- Relations enhancements:
|
||||||
|
|
|
||||||
93
tests/UnitOfWorkCascadeMongoTest.php
Normal file
93
tests/UnitOfWorkCascadeMongoTest.php
Normal file
|
|
@ -0,0 +1,93 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Pairity\Tests;
|
||||||
|
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
use Pairity\NoSql\Mongo\MongoConnectionManager;
|
||||||
|
use Pairity\NoSql\Mongo\AbstractMongoDao;
|
||||||
|
use Pairity\Orm\UnitOfWork;
|
||||||
|
use Pairity\Model\AbstractDto;
|
||||||
|
|
||||||
|
final class UnitOfWorkCascadeMongoTest extends TestCase
|
||||||
|
{
|
||||||
|
private function hasMongoExt(): bool
|
||||||
|
{
|
||||||
|
return \extension_loaded('mongodb');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testDeleteByIdCascadesToChildren(): void
|
||||||
|
{
|
||||||
|
if (!$this->hasMongoExt()) {
|
||||||
|
$this->markTestSkipped('ext-mongodb not loaded');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Connect (skip if server unavailable)
|
||||||
|
try {
|
||||||
|
$conn = MongoConnectionManager::make([
|
||||||
|
'host' => \getenv('MONGO_HOST') ?: '127.0.0.1',
|
||||||
|
'port' => (int)(\getenv('MONGO_PORT') ?: 27017),
|
||||||
|
]);
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
$this->markTestSkipped('Mongo not available: ' . $e->getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Inline DTOs
|
||||||
|
$userDto = new class([]) extends AbstractDto {};
|
||||||
|
$userDtoClass = \get_class($userDto);
|
||||||
|
$postDto = new class([]) extends AbstractDto {};
|
||||||
|
$postDtoClass = \get_class($postDto);
|
||||||
|
|
||||||
|
// Inline DAOs with relation and cascadeDelete=true
|
||||||
|
$UserDao = new class($conn, $userDtoClass, $postDtoClass) extends AbstractMongoDao {
|
||||||
|
private string $userDto; private string $postDto; public function __construct($c, string $u, string $p) { parent::__construct($c); $this->userDto = $u; $this->postDto = $p; }
|
||||||
|
protected function collection(): string { return 'pairity_test.uow_users_cascade'; }
|
||||||
|
protected function dtoClass(): string { return $this->userDto; }
|
||||||
|
protected function relations(): array {
|
||||||
|
return [
|
||||||
|
'posts' => [
|
||||||
|
'type' => 'hasMany',
|
||||||
|
'dao' => get_class(new class($this->getConnection(), $this->postDto) extends AbstractMongoDao {
|
||||||
|
private string $dto; public function __construct($c, string $d) { parent::__construct($c); $this->dto = $d; }
|
||||||
|
protected function collection(): string { return 'pairity_test.uow_posts_cascade'; }
|
||||||
|
protected function dtoClass(): string { return $this->dto; }
|
||||||
|
}),
|
||||||
|
'foreignKey' => 'user_id',
|
||||||
|
'localKey' => '_id',
|
||||||
|
'cascadeDelete' => true,
|
||||||
|
],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
$PostDao = new class($conn, $postDtoClass) extends AbstractMongoDao {
|
||||||
|
private string $dto; public function __construct($c, string $d) { parent::__construct($c); $this->dto = $d; }
|
||||||
|
protected function collection(): string { return 'pairity_test.uow_posts_cascade'; }
|
||||||
|
protected function dtoClass(): string { return $this->dto; }
|
||||||
|
};
|
||||||
|
|
||||||
|
$userDao = new $UserDao($conn, $userDtoClass, $postDtoClass);
|
||||||
|
$postDao = new $PostDao($conn, $postDtoClass);
|
||||||
|
|
||||||
|
// Clean
|
||||||
|
foreach ($postDao->findAllBy([]) as $p) { $id = (string)($p->toArray(false)['_id'] ?? ''); if ($id) { $postDao->deleteById($id); } }
|
||||||
|
foreach ($userDao->findAllBy([]) as $u) { $id = (string)($u->toArray(false)['_id'] ?? ''); if ($id) { $userDao->deleteById($id); } }
|
||||||
|
|
||||||
|
// Seed
|
||||||
|
$u = $userDao->insert(['email' => 'c@example.com']);
|
||||||
|
$uid = (string)($u->toArray(false)['_id'] ?? '');
|
||||||
|
$postDao->insert(['user_id' => $uid, 'title' => 'A']);
|
||||||
|
$postDao->insert(['user_id' => $uid, 'title' => 'B']);
|
||||||
|
|
||||||
|
// UoW: delete parent -> children should be deleted first
|
||||||
|
UnitOfWork::run(function() use ($userDao, $uid) {
|
||||||
|
$userDao->deleteById($uid);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Verify
|
||||||
|
$children = $postDao->findAllBy(['user_id' => $uid]);
|
||||||
|
$this->assertCount(0, $children, 'Child posts should be deleted via cascade');
|
||||||
|
$this->assertNull($userDao->findById($uid));
|
||||||
|
}
|
||||||
|
}
|
||||||
83
tests/UnitOfWorkCascadeSqliteTest.php
Normal file
83
tests/UnitOfWorkCascadeSqliteTest.php
Normal file
|
|
@ -0,0 +1,83 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Pairity\Tests;
|
||||||
|
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
use Pairity\Database\ConnectionManager;
|
||||||
|
use Pairity\Model\AbstractDto;
|
||||||
|
use Pairity\Model\AbstractDao;
|
||||||
|
use Pairity\Orm\UnitOfWork;
|
||||||
|
|
||||||
|
final class UnitOfWorkCascadeSqliteTest extends TestCase
|
||||||
|
{
|
||||||
|
private function conn()
|
||||||
|
{
|
||||||
|
return ConnectionManager::make(['driver' => 'sqlite', 'path' => ':memory:']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testDeleteByIdCascadesToChildren(): void
|
||||||
|
{
|
||||||
|
$conn = $this->conn();
|
||||||
|
// schema
|
||||||
|
$conn->execute('CREATE TABLE users (id INTEGER PRIMARY KEY AUTOINCREMENT, email TEXT)');
|
||||||
|
$conn->execute('CREATE TABLE posts (id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER, title TEXT)');
|
||||||
|
|
||||||
|
// DTOs
|
||||||
|
$userDto = new class([]) extends AbstractDto {};
|
||||||
|
$postDto = new class([]) extends AbstractDto {};
|
||||||
|
$userDtoClass = get_class($userDto);
|
||||||
|
$postDtoClass = get_class($postDto);
|
||||||
|
|
||||||
|
// DAOs with hasMany relation and cascadeDelete=true
|
||||||
|
$UserDao = new class($conn, $userDtoClass, $postDtoClass) extends AbstractDao {
|
||||||
|
private string $userDto; private string $postDto;
|
||||||
|
public function __construct($c, string $u, string $p) { parent::__construct($c); $this->userDto = $u; $this->postDto = $p; }
|
||||||
|
public function getTable(): string { return 'users'; }
|
||||||
|
protected function dtoClass(): string { return $this->userDto; }
|
||||||
|
protected function relations(): array {
|
||||||
|
return [
|
||||||
|
'posts' => [
|
||||||
|
'type' => 'hasMany',
|
||||||
|
'dao' => get_class(new class($this->getConnection(), $this->postDto) extends AbstractDao {
|
||||||
|
private string $dto; public function __construct($c, string $d) { parent::__construct($c); $this->dto = $d; }
|
||||||
|
public function getTable(): string { return 'posts'; }
|
||||||
|
protected function dtoClass(): string { return $this->dto; }
|
||||||
|
}),
|
||||||
|
'foreignKey' => 'user_id',
|
||||||
|
'localKey' => 'id',
|
||||||
|
'cascadeDelete' => true,
|
||||||
|
],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
protected function schema(): array { return ['primaryKey' => 'id', 'columns' => ['id'=>['cast'=>'int'],'email'=>['cast'=>'string']]]; }
|
||||||
|
};
|
||||||
|
|
||||||
|
$PostDao = new class($conn, $postDtoClass) extends AbstractDao {
|
||||||
|
private string $dto; public function __construct($c, string $d) { parent::__construct($c); $this->dto = $d; }
|
||||||
|
public function getTable(): string { return 'posts'; }
|
||||||
|
protected function dtoClass(): string { return $this->dto; }
|
||||||
|
protected function schema(): array { return ['primaryKey'=>'id','columns'=>['id'=>['cast'=>'int'],'user_id'=>['cast'=>'int'],'title'=>['cast'=>'string']]]; }
|
||||||
|
};
|
||||||
|
|
||||||
|
$userDao = new $UserDao($conn, $userDtoClass, $postDtoClass);
|
||||||
|
$postDao = new $PostDao($conn, $postDtoClass);
|
||||||
|
|
||||||
|
// seed
|
||||||
|
$u = $userDao->insert(['email' => 'c@example.com']);
|
||||||
|
$uid = (int)($u->toArray(false)['id'] ?? 0);
|
||||||
|
$postDao->insert(['user_id' => $uid, 'title' => 'A']);
|
||||||
|
$postDao->insert(['user_id' => $uid, 'title' => 'B']);
|
||||||
|
|
||||||
|
// UoW: delete user; expect posts to be deleted first via cascade
|
||||||
|
UnitOfWork::run(function() use ($userDao, $uid) {
|
||||||
|
$userDao->deleteById($uid);
|
||||||
|
});
|
||||||
|
|
||||||
|
// verify posts gone and user gone
|
||||||
|
$remainingPosts = $postDao->findAllBy(['user_id' => $uid]);
|
||||||
|
$this->assertCount(0, $remainingPosts, 'Child posts should be deleted via cascade');
|
||||||
|
$this->assertNull($userDao->findById($uid));
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue