事务工作单元

用事务工作单元更好地处理数据库相关工作。

引入相关类

  • use Leevel\Database\Ddd\Entity;
  • use Leevel\Database\Ddd\IUnitOfWork;
  • use Leevel\Database\Ddd\UnitOfWork;
  • use Tests\Database\DatabaseTestCase as TestCase;
  • use Tests\Database\Ddd\Entity\CompositeId;
  • use Tests\Database\Ddd\Entity\Guestbook;
  • use Tests\Database\Ddd\Entity\GuestbookRepository;
  • use Tests\Database\Ddd\Entity\Relation\Post;
  • use Throwable;

    保存一个实体

  1. public function testBaseUse()
  2. {
  3. $work = UnitOfWork::make();
  4. $this->assertInstanceof(UnitOfWork::class, $work);
  5. $this->assertInstanceof(IUnitOfWork::class, $work);
  6. $post = new Post([
  7. 'title' => 'hello world',
  8. 'user_id' => 1,
  9. 'summary' => 'post summary',
  10. ]);
  11. $this->assertNull($post->id);
  12. $work->persist($post);
  13. $work->flush();
  14. $this->assertSame('1', $post->id);
  15. $this->assertSame('1', $post['id']);
  16. $this->assertSame('1', $post->getId());
  17. $this->assertSame(1, $post->userId);
  18. $this->assertSame('post summary', $post->summary);
  19. }

::: tip
通过 persist 方法保存一个实体,并通过 flush 将实体持久化到数据库。
:::

保存多个实体

  1. public function testPersist()
  2. {
  3. $work = UnitOfWork::make();
  4. $this->assertInstanceof(UnitOfWork::class, $work);
  5. $this->assertInstanceof(IUnitOfWork::class, $work);
  6. $post = new Post([
  7. 'title' => 'hello world',
  8. 'user_id' => 1,
  9. 'summary' => 'post summary',
  10. ]);
  11. $this->assertNull($post->id);
  12. $post2 = new Post([
  13. 'title' => 'hello world',
  14. 'user_id' => 2,
  15. 'summary' => 'foo bar',
  16. ]);
  17. $this->assertNull($post2->id);
  18. $work->persist($post);
  19. $work->persist($post2);
  20. $work->flush();
  21. $this->assertSame('1', $post->id);
  22. $this->assertSame('1', $post['id']);
  23. $this->assertSame('1', $post->getId());
  24. $this->assertSame(1, $post->userId);
  25. $this->assertSame('post summary', $post->summary);
  26. $this->assertSame('2', $post2->id);
  27. $this->assertSame('2', $post2['id']);
  28. $this->assertSame('2', $post2->getId());
  29. $this->assertSame(2, $post2->userId);
  30. $this->assertSame('foo bar', $post2->summary);
  31. }

::: tip
底层会开启一个事务,只有全部保存成功才会真正持久化到数据库。
:::

新增实体

  1. public function testCreate()
  2. {
  3. $work = UnitOfWork::make();
  4. $this->assertInstanceof(UnitOfWork::class, $work);
  5. $this->assertInstanceof(IUnitOfWork::class, $work);
  6. $post = new Post([
  7. 'title' => 'hello world',
  8. 'user_id' => 1,
  9. 'summary' => 'post summary',
  10. ]);
  11. $post2 = new Post([
  12. 'title' => 'hello world',
  13. 'user_id' => 2,
  14. 'summary' => 'foo bar',
  15. ]);
  16. $this->assertNull($post->id);
  17. $this->assertNull($post2->id);
  18. $this->assertFalse($work->created($post));
  19. $this->assertFalse($work->created($post2));
  20. $this->assertFalse($work->registered($post));
  21. $this->assertFalse($work->registered($post2));
  22. $work->create($post);
  23. $work->create($post2);
  24. $this->assertTrue($work->created($post));
  25. $this->assertTrue($work->created($post2));
  26. $this->assertTrue($work->registered($post));
  27. $this->assertTrue($work->registered($post2));
  28. $work->flush();
  29. $this->assertFalse($work->created($post));
  30. $this->assertFalse($work->created($post2));
  31. $this->assertFalse($work->registered($post));
  32. $this->assertFalse($work->registered($post2));
  33. $this->assertSame('1', $post->id);
  34. $this->assertSame('1', $post['id']);
  35. $this->assertSame('1', $post->getId());
  36. $this->assertSame(1, $post->userId);
  37. $this->assertSame('post summary', $post->summary);
  38. $this->assertSame('2', $post2->id);
  39. $this->assertSame('2', $post2['id']);
  40. $this->assertSame('2', $post2->getId());
  41. $this->assertSame(2, $post2->userId);
  42. $this->assertSame('foo bar', $post2->summary);
  43. }

::: tip
底层执行的是 insert 语句,只有全部保存成功才会真正持久化到数据库。
:::

更新实体

  1. public function testUpdate()
  2. {
  3. $work = UnitOfWork::make();
  4. $this->assertInstanceof(UnitOfWork::class, $work);
  5. $this->assertInstanceof(IUnitOfWork::class, $work);
  6. $connect = $this->createDatabaseConnect();
  7. $this->assertSame('1', $connect->
  8. table('post')->
  9. insert([
  10. 'title' => 'hello world',
  11. 'user_id' => 1,
  12. 'summary' => 'post summary',
  13. ]));
  14. $this->assertSame('2', $connect->
  15. table('post')->
  16. insert([
  17. 'title' => 'hello world',
  18. 'user_id' => 2,
  19. 'summary' => 'foo bar',
  20. ]));
  21. $post = Post::find(1);
  22. $post2 = Post::find(2);
  23. $this->assertInstanceof(Entity::class, $post);
  24. $this->assertInstanceof(Entity::class, $post2);
  25. $this->assertInstanceof(Post::class, $post);
  26. $this->assertInstanceof(Post::class, $post2);
  27. $this->assertSame('1', $post->id);
  28. $this->assertSame('1', $post['id']);
  29. $this->assertSame('1', $post->getId());
  30. $this->assertSame('1', $post->userId);
  31. $this->assertSame('post summary', $post->summary);
  32. $this->assertSame('hello world', $post->title);
  33. $this->assertSame('2', $post2->id);
  34. $this->assertSame('2', $post2['id']);
  35. $this->assertSame('2', $post2->getId());
  36. $this->assertSame('2', $post2->userId);
  37. $this->assertSame('foo bar', $post2->summary);
  38. $this->assertSame('hello world', $post2->title);
  39. $this->assertFalse($work->updated($post));
  40. $this->assertFalse($work->updated($post2));
  41. $this->assertFalse($work->registered($post));
  42. $this->assertFalse($work->registered($post2));
  43. $post->title = 'new post title';
  44. $post->summary = 'new post summary';
  45. $post2->title = 'new post2 title';
  46. $post2->summary = 'new post2 summary';
  47. $work->update($post);
  48. $work->update($post2);
  49. $this->assertTrue($work->updated($post));
  50. $this->assertTrue($work->updated($post2));
  51. $this->assertTrue($work->registered($post));
  52. $this->assertTrue($work->registered($post2));
  53. $work->flush();
  54. $this->assertFalse($work->updated($post));
  55. $this->assertFalse($work->updated($post2));
  56. $this->assertFalse($work->registered($post));
  57. $this->assertFalse($work->registered($post2));
  58. $this->assertSame('1', $post->id);
  59. $this->assertSame('1', $post['id']);
  60. $this->assertSame('1', $post->getId());
  61. $this->assertSame('1', $post->userId);
  62. $this->assertSame('new post title', $post->title);
  63. $this->assertSame('new post summary', $post->summary);
  64. $this->assertSame('2', $post2->id);
  65. $this->assertSame('2', $post2['id']);
  66. $this->assertSame('2', $post2->getId());
  67. $this->assertSame('2', $post2->userId);
  68. $this->assertSame('new post2 title', $post2->title);
  69. $this->assertSame('new post2 summary', $post2->summary);
  70. }

::: tip
底层执行的是 update 语句,只有全部保存成功才会真正持久化到数据库。
:::

删除实体

  1. public function testDelete()
  2. {
  3. $work = UnitOfWork::make();
  4. $this->assertInstanceof(UnitOfWork::class, $work);
  5. $this->assertInstanceof(IUnitOfWork::class, $work);
  6. $connect = $this->createDatabaseConnect();
  7. $this->assertSame('1', $connect->
  8. table('post')->
  9. insert([
  10. 'title' => 'hello world',
  11. 'user_id' => 1,
  12. 'summary' => 'post summary',
  13. ]));
  14. $this->assertSame('2', $connect->
  15. table('post')->
  16. insert([
  17. 'title' => 'hello world',
  18. 'user_id' => 2,
  19. 'summary' => 'foo bar',
  20. ]));
  21. $post = Post::find(1);
  22. $post2 = Post::find(2);
  23. $this->assertInstanceof(Entity::class, $post);
  24. $this->assertInstanceof(Entity::class, $post2);
  25. $this->assertInstanceof(Post::class, $post);
  26. $this->assertInstanceof(Post::class, $post2);
  27. $this->assertSame('1', $post->id);
  28. $this->assertSame('1', $post['id']);
  29. $this->assertSame('1', $post->getId());
  30. $this->assertSame('1', $post->userId);
  31. $this->assertSame('post summary', $post->summary);
  32. $this->assertSame('hello world', $post->title);
  33. $this->assertSame('2', $post2->id);
  34. $this->assertSame('2', $post2['id']);
  35. $this->assertSame('2', $post2->getId());
  36. $this->assertSame('2', $post2->userId);
  37. $this->assertSame('foo bar', $post2->summary);
  38. $this->assertSame('hello world', $post2->title);
  39. $this->assertFalse($work->deleted($post));
  40. $this->assertFalse($work->deleted($post2));
  41. $this->assertFalse($work->registered($post));
  42. $this->assertFalse($work->registered($post2));
  43. $work->delete($post);
  44. $work->delete($post2);
  45. $this->assertTrue($work->deleted($post));
  46. $this->assertTrue($work->deleted($post2));
  47. $this->assertTrue($work->registered($post));
  48. $this->assertTrue($work->registered($post2));
  49. $work->flush();
  50. $this->assertFalse($work->deleted($post));
  51. $this->assertFalse($work->deleted($post2));
  52. $this->assertFalse($work->registered($post));
  53. $this->assertFalse($work->registered($post2));
  54. $postAfter = Post::find(1);
  55. $post2After = Post::find(2);
  56. $this->assertNull($postAfter->id);
  57. $this->assertNull($postAfter['id']);
  58. $this->assertNull($postAfter->getId());
  59. $this->assertNull($postAfter->userId);
  60. $this->assertNull($postAfter->title);
  61. $this->assertNull($postAfter->summary);
  62. $this->assertNull($post2After->id);
  63. $this->assertNull($post2After['id']);
  64. $this->assertNull($post2After->getId());
  65. $this->assertNull($post2After->userId);
  66. $this->assertNull($post2After->title);
  67. $this->assertNull($post2After->summary);
  68. }

::: tip
底层执行的是 delete 语句,只有全部保存成功才会真正持久化到数据库。
:::

刷新实体

  1. public function testRefresh()
  2. {
  3. $work = UnitOfWork::make();
  4. $this->assertInstanceof(UnitOfWork::class, $work);
  5. $this->assertInstanceof(IUnitOfWork::class, $work);
  6. $connect = $this->createDatabaseConnect();
  7. $this->assertSame('1', $connect->
  8. table('post')->
  9. insert([
  10. 'title' => 'hello world',
  11. 'user_id' => 1,
  12. 'summary' => 'post summary',
  13. ]));
  14. $post = new Post([
  15. 'id' => 1,
  16. 'title' => 'old',
  17. 'summary' => 'old',
  18. ], true);
  19. $this->assertSame(1, $post->getId());
  20. $this->assertSame('old', $post->getSummary());
  21. $this->assertSame('old', $post->getTitle());
  22. $work->persist($post);
  23. $work->refresh($post);
  24. $this->assertSame('1', $post->getId());
  25. $this->assertSame('post summary', $post->getSummary());
  26. $this->assertSame('hello world', $post->getTitle());
  27. $work->flush();
  28. $post = Post::find(1);
  29. $this->assertInstanceof(Entity::class, $post);
  30. $this->assertInstanceof(Post::class, $post);
  31. $this->assertSame('1', $post->id);
  32. $this->assertSame('1', $post['id']);
  33. $this->assertSame('1', $post->getId());
  34. $this->assertSame('1', $post->userId);
  35. $this->assertSame('post summary', $post->summary);
  36. $this->assertSame('hello world', $post->title);
  37. }

::: tip
底层执行的是 select 语句,这个操作会读取数据库最新信息并刷新实体的属性。
:::

手工启动事务 beginTransaction

  1. public function testBeginTransaction()
  2. {
  3. $work = UnitOfWork::make();
  4. $this->assertInstanceof(UnitOfWork::class, $work);
  5. $this->assertInstanceof(IUnitOfWork::class, $work);
  6. $connect = $this->createDatabaseConnect();
  7. $this->assertSame('1', $connect->
  8. table('post')->
  9. insert([
  10. 'title' => 'hello world',
  11. 'user_id' => 1,
  12. 'summary' => 'post summary',
  13. ]));
  14. $work->beginTransaction();
  15. $post = Post::find(1);
  16. $work->update($post);
  17. try {
  18. $post->title = 'new title';
  19. $work->flush();
  20. $work->commit();
  21. } catch (Throwable $e) {
  22. $work->close();
  23. $work->rollBack();
  24. }
  25. $this->assertSame('1', $post->getId());
  26. $this->assertSame('new title', $post->getTitle());
  27. }

::: tip
通常来说事务工作单元会自动帮你处理事务,可以通过手工 beginTransaction,成功 commit 或者失败 rollBack,系统提供了 API 让你也手工开启事务处理。
:::

执行失败事务回滚 rollBack

  1. public function testFlushButRollBack()
  2. {
  3. $this->expectException(\Leevel\Database\DuplicateKeyException::class);
  4. $this->expectExceptionMessage(
  5. 'SQLSTATE[23000]: Integrity constraint violation: 1062 Duplicate entry \'1\' for key \'PRIMARY\''
  6. );
  7. $work = UnitOfWork::make();
  8. $post = new Post([
  9. 'id' => 1,
  10. 'title' => 'old',
  11. 'summary' => 'old',
  12. ]);
  13. $post2 = new Post([
  14. 'id' => 1,
  15. 'title' => 'old',
  16. 'summary' => 'old',
  17. ]);
  18. $work->create($post);
  19. $work->create($post2);
  20. $work->flush();
  21. }

::: tip
底层会自动运行一个事务,如果执行失败自动回滚,不会更新数据库。
:::

事务包裹在闭包中 transaction

  1. public function testTransaction()
  2. {
  3. $work = UnitOfWork::make();
  4. $this->assertInstanceof(UnitOfWork::class, $work);
  5. $this->assertInstanceof(IUnitOfWork::class, $work);
  6. $connect = $this->createDatabaseConnect();
  7. $this->assertSame('1', $connect->
  8. table('post')->
  9. insert([
  10. 'title' => 'hello world',
  11. 'user_id' => 1,
  12. 'summary' => 'post summary',
  13. ]));
  14. $work->transaction(function ($w) {
  15. $post = Post::find(1);
  16. $w->update($post);
  17. $post->title = 'new title';
  18. });
  19. $newPost = Post::find(1);
  20. $this->assertSame('1', $newPost->getId());
  21. $this->assertSame('new title', $newPost->getTitle());
  22. }

::: tip
可以将事务包裹在一个闭包中,如果执行失败自动回滚,不会更新数据库。
:::

事务包裹在闭包中失败回滚 transaction

  1. public function testTransactionAndRollBack()
  2. {
  3. $this->expectException(\Leevel\Database\DuplicateKeyException::class);
  4. $this->expectExceptionMessage(
  5. 'SQLSTATE[23000]: Integrity constraint violation: 1062 Duplicate entry \'1\' for key \'PRIMARY\''
  6. );
  7. $work = UnitOfWork::make();
  8. $this->assertInstanceof(UnitOfWork::class, $work);
  9. $this->assertInstanceof(IUnitOfWork::class, $work);
  10. $connect = $this->createDatabaseConnect();
  11. $work->transaction(function ($w) {
  12. $post = new Post([
  13. 'id' => 1,
  14. 'title' => 'old',
  15. 'summary' => 'old',
  16. ]);
  17. $post2 = new Post([
  18. 'id' => 1,
  19. 'title' => 'old',
  20. 'summary' => 'old',
  21. ]);
  22. $w->create($post);
  23. $w->create($post2);
  24. });
  25. $this->assertSame(0, $connect->table('post')->findCount());
  26. }

::: tip
可以将事务包裹在一个闭包中,执行失败自动回滚测试,不会更新数据库。
:::

设置根实体 setRootEntity

  1. public function testSetRootEntity()
  2. {
  3. $work = UnitOfWork::make();
  4. $this->assertInstanceof(UnitOfWork::class, $work);
  5. $this->assertInstanceof(IUnitOfWork::class, $work);
  6. $connect = $this->createDatabaseConnect();
  7. $this->assertSame('1', $connect->
  8. table('post')->
  9. insert([
  10. 'title' => 'hello world',
  11. 'user_id' => 1,
  12. 'summary' => 'post summary',
  13. ]));
  14. $post = Post::find(1);
  15. $work->setRootEntity($post);
  16. $work->update($post);
  17. $post->title = 'new title';
  18. $work->flush();
  19. $this->assertSame('1', $post->getId());
  20. $this->assertSame('new title', $post->getTitle());
  21. $newPost = Post::find(1);
  22. $this->assertSame('1', $newPost->getId());
  23. $this->assertSame('new title', $newPost->getTitle());
  24. }

::: tip
系统默认读取基础的数据库配置来处理数据相关信息,设置跟实体可以更改事务处理的数据库连接。
:::

更改数据库连接 setConnect

  1. public function testSetConnectNotFoundWillUseDefault()
  2. {
  3. $work = UnitOfWork::make();
  4. $this->assertInstanceof(UnitOfWork::class, $work);
  5. $this->assertInstanceof(IUnitOfWork::class, $work);
  6. $connect = $this->createDatabaseConnect();
  7. $this->assertSame('1', $connect->
  8. table('post')->
  9. insert([
  10. 'title' => 'hello world',
  11. 'user_id' => 1,
  12. 'summary' => 'post summary',
  13. ]));
  14. $post = Post::find(1);
  15. $work->setConnect('hello');
  16. $work->update($post);
  17. $post->title = 'new title';
  18. $work->flush();
  19. $this->assertSame('1', $post->getId());
  20. $this->assertSame('new title', $post->getTitle());
  21. $newPost = Post::find(1);
  22. $this->assertSame('1', $newPost->getId());
  23. $this->assertSame('new title', $newPost->getTitle());
  24. }

::: tip
如果没有存在的连接,则会使用默认的连接。
:::

无实体执行 flush 什么都不做

  1. public function testFlushButNotFoundAny()
  2. {
  3. $work = UnitOfWork::make(new Post());
  4. $this->assertNull($work->flush());
  5. }

::: tip
实际上什么也不会发生。
:::

实体实体支持缓存

  1. public function testPersistStageManagedEntityDoNothing()
  2. {
  3. $work = UnitOfWork::make();
  4. $connect = $this->createDatabaseConnect();
  5. $post = new Post([
  6. 'id' => 1,
  7. 'title' => 'old',
  8. 'summary' => 'old',
  9. ]);
  10. $work->persist($post, 'create');
  11. $work->persist($post, 'create');
  12. $work->flush();
  13. $this->assertSame(1, $connect->table('post')->findCount());
  14. }

::: tip
保存两个一样的实体,第二个实体并不会被添加。
:::

重新保存已删除的实体实体

  1. public function testPersistStageRemovedEntity()
  2. {
  3. $work = UnitOfWork::make();
  4. $connect = $this->createDatabaseConnect();
  5. $this->assertSame('1', $connect->
  6. table('post')->
  7. insert([
  8. 'title' => 'hello world',
  9. 'user_id' => 1,
  10. 'summary' => 'post summary',
  11. ]));
  12. $post = Post::find(1);
  13. $this->assertSame('1', $post->getId());
  14. $this->assertSame('hello world', $post->getTitle());
  15. $this->assertSame('post summary', $post->getSummary());
  16. $work->delete($post);
  17. $work->persist($post);
  18. $work->flush();
  19. $this->assertSame(1, $connect->table('post')->findCount());
  20. }

::: tip
这样被删除的实体并不会被删除。
:::

注册更新的实体不能重新被创建

  1. public function testCreateButAlreadyInUpdates()
  2. {
  3. $this->expectException(\InvalidArgumentException::class);
  4. $this->expectExceptionMessage(
  5. 'Updated entity `Tests\\Database\\Ddd\\Entity\\Relation\\Post` cannot be added for create.'
  6. );
  7. $work = UnitOfWork::make();
  8. $post = new Post(['id' => 5, 'title' => 'foo']);
  9. $work->update($post);
  10. $work->create($post);
  11. }

注册删除的实体不能重新被创建

  1. public function testCreateButAlreadyInDeletes()
  2. {
  3. $this->expectException(\InvalidArgumentException::class);
  4. $this->expectExceptionMessage(
  5. 'Deleted entity `Tests\\Database\\Ddd\\Entity\\Relation\\Post` cannot be added for create.'
  6. );
  7. $work = UnitOfWork::make();
  8. $post = new Post(['id' => 5]);
  9. $work->delete($post);
  10. $work->create($post);
  11. }

注册替换的实体不能重新被创建

  1. public function testCreateButAlreadyInReplaces()
  2. {
  3. $this->expectException(\InvalidArgumentException::class);
  4. $this->expectExceptionMessage(
  5. 'Replaced entity `Tests\\Database\\Ddd\\Entity\\Relation\\Post` cannot be added for create.'
  6. );
  7. $work = UnitOfWork::make();
  8. $post = new Post(['id' => 5]);
  9. $work->replace($post);
  10. $work->create($post);
  11. }

不能多次创建同一个实体

  1. public function testCreateManyTimes()
  2. {
  3. $this->expectException(\InvalidArgumentException::class);
  4. $this->expectExceptionMessage(
  5. 'Entity `Tests\\Database\\Ddd\\Entity\\Relation\\Post` cannot be added for twice.'
  6. );
  7. $work = UnitOfWork::make();
  8. $connect = $this->createDatabaseConnect();
  9. $post = new Post(['title' => 'foo']);
  10. $work->create($post);
  11. $work->create($post);
  12. }