Skip to content

Commit ba50060

Browse files
author
Composite PHP
committed
Performance optimizations
1 parent 1ae4801 commit ba50060

14 files changed

+204
-63
lines changed

src/AbstractCachedTable.php

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88

99
abstract class AbstractCachedTable extends AbstractTable
1010
{
11-
use SelectRawTrait;
11+
use Helpers\SelectRawTrait;
1212

1313
protected const CACHE_VERSION = 1;
1414

@@ -26,7 +26,7 @@ abstract protected function getFlushCacheKeys(AbstractEntity $entity): array;
2626
/**
2727
* @throws \Throwable
2828
*/
29-
public function save(AbstractEntity &$entity): void
29+
public function save(AbstractEntity $entity): void
3030
{
3131
$cacheKeys = $this->collectCacheKeysByEntity($entity);
3232
parent::save($entity);
@@ -54,7 +54,7 @@ public function saveMany(array $entities): void
5454
/**
5555
* @throws \Throwable
5656
*/
57-
public function delete(AbstractEntity &$entity): void
57+
public function delete(AbstractEntity $entity): void
5858
{
5959
$cacheKeys = $this->collectCacheKeysByEntity($entity);
6060
parent::delete($entity);

src/AbstractTable.php

Lines changed: 51 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -2,21 +2,22 @@
22

33
namespace Composite\DB;
44

5+
use Composite\DB\Exceptions\DbException;
56
use Composite\DB\MultiQuery\MultiInsert;
67
use Composite\DB\MultiQuery\MultiSelect;
7-
use Composite\Entity\Helpers\DateTimeHelper;
88
use Composite\Entity\AbstractEntity;
9-
use Composite\DB\Exceptions\DbException;
9+
use Composite\Entity\Helpers\DateTimeHelper;
1010
use Doctrine\DBAL\Connection;
11-
use Doctrine\DBAL\Platforms\PostgreSQLPlatform;
1211
use Ramsey\Uuid\UuidInterface;
1312

1413
abstract class AbstractTable
1514
{
16-
use SelectRawTrait;
15+
use Helpers\SelectRawTrait;
16+
use Helpers\DatabaseSpecificTrait;
1717

1818
protected readonly TableConfig $config;
1919

20+
2021
abstract protected function getConfig(): TableConfig;
2122

2223
public function __construct()
@@ -44,49 +45,51 @@ public function getConnectionName(): string
4445
* @return void
4546
* @throws \Throwable
4647
*/
47-
public function save(AbstractEntity &$entity): void
48+
public function save(AbstractEntity $entity): void
4849
{
4950
$this->config->checkEntity($entity);
5051
if ($entity->isNew()) {
5152
$connection = $this->getConnection();
5253
$this->checkUpdatedAt($entity);
5354

54-
$insertData = $this->formatData($entity->toArray());
55+
$insertData = $this->prepareDataForSql($entity->toArray());
5556
$this->getConnection()->insert($this->getTableName(), $insertData);
5657

57-
if ($this->config->autoIncrementKey) {
58-
$insertData[$this->config->autoIncrementKey] = intval($connection->lastInsertId());
59-
$entity = $entity::fromArray($insertData);
60-
} else {
61-
$entity->resetChangedColumns();
58+
if ($this->config->autoIncrementKey && ($lastInsertedId = $connection->lastInsertId())) {
59+
$insertData[$this->config->autoIncrementKey] = intval($lastInsertedId);
60+
$entity::schema()
61+
->getColumn($this->config->autoIncrementKey)
62+
->setValue($entity, $insertData[$this->config->autoIncrementKey]);
6263
}
64+
$entity->resetChangedColumns($insertData);
6365
} else {
6466
if (!$changedColumns = $entity->getChangedColumns()) {
6567
return;
6668
}
67-
$connection = $this->getConnection();
68-
$where = $this->getPkCondition($entity);
69-
69+
$changedColumns = $this->prepareDataForSql($changedColumns);
7070
if ($this->config->hasUpdatedAt() && property_exists($entity, 'updated_at')) {
7171
$entity->updated_at = new \DateTimeImmutable();
7272
$changedColumns['updated_at'] = DateTimeHelper::dateTimeToString($entity->updated_at);
7373
}
74+
$whereParams = $this->getPkCondition($entity);
7475
if ($this->config->hasOptimisticLock()
7576
&& method_exists($entity, 'getVersion')
7677
&& method_exists($entity, 'incrementVersion')) {
77-
$where['lock_version'] = $entity->getVersion();
78+
$whereParams['lock_version'] = $entity->getVersion();
7879
$entity->incrementVersion();
7980
$changedColumns['lock_version'] = $entity->getVersion();
8081
}
81-
$entityUpdated = $connection->update(
82-
table: $this->getTableName(),
83-
data: $changedColumns,
84-
criteria: $where,
82+
$updateString = implode(', ', array_map(fn ($key) => $this->escapeIdentifier($key) . "=?", array_keys($changedColumns)));
83+
$whereString = implode(' AND ', array_map(fn ($key) => $this->escapeIdentifier($key) . "=?", array_keys($whereParams)));
84+
85+
$entityUpdated = (bool)$this->getConnection()->executeStatement(
86+
sql: "UPDATE " . $this->escapeIdentifier($this->getTableName()) . " SET $updateString WHERE $whereString;",
87+
params: array_merge(array_values($changedColumns), array_values($whereParams)),
8588
);
8689
if ($this->config->hasOptimisticLock() && !$entityUpdated) {
8790
throw new Exceptions\LockException('Failed to update entity version, concurrency modification, rolling back.');
8891
}
89-
$entity->resetChangedColumns();
92+
$entity->resetChangedColumns($changedColumns);
9093
}
9194
}
9295

@@ -101,7 +104,7 @@ public function saveMany(array $entities): void
101104
if ($entity->isNew()) {
102105
$this->config->checkEntity($entity);
103106
$this->checkUpdatedAt($entity);
104-
$rowsToInsert[] = $this->formatData($entity->toArray());
107+
$rowsToInsert[] = $this->prepareDataForSql($entity->toArray());
105108
unset($entities[$i]);
106109
}
107110
}
@@ -113,14 +116,15 @@ public function saveMany(array $entities): void
113116
}
114117
if ($rowsToInsert) {
115118
$chunks = array_chunk($rowsToInsert, 1000);
119+
$connection = $this->getConnection();
116120
foreach ($chunks as $chunk) {
117121
$multiInsert = new MultiInsert(
122+
connection: $connection,
118123
tableName: $this->getTableName(),
119124
rows: $chunk,
120125
);
121126
if ($multiInsert->getSql()) {
122-
$stmt = $this->getConnection()->prepare($multiInsert->getSql());
123-
$stmt->executeQuery($multiInsert->getParameters());
127+
$connection->executeStatement($multiInsert->getSql(), $multiInsert->getParameters());
124128
}
125129
}
126130
}
@@ -135,7 +139,7 @@ public function saveMany(array $entities): void
135139
* @param AbstractEntity $entity
136140
* @throws \Throwable
137141
*/
138-
public function delete(AbstractEntity &$entity): void
142+
public function delete(AbstractEntity $entity): void
139143
{
140144
$this->config->checkEntity($entity);
141145
if ($this->config->hasSoftDelete()) {
@@ -144,8 +148,12 @@ public function delete(AbstractEntity &$entity): void
144148
$this->save($entity);
145149
}
146150
} else {
147-
$where = $this->getPkCondition($entity);
148-
$this->getConnection()->delete($this->getTableName(), $where);
151+
$whereParams = $this->getPkCondition($entity);
152+
$whereString = implode(' AND ', array_map(fn ($key) => $this->escapeIdentifier($key) . "=?", array_keys($whereParams)));
153+
$this->getConnection()->executeQuery(
154+
sql: "DELETE FROM " . $this->escapeIdentifier($this->getTableName()) . " WHERE $whereString;",
155+
params: array_values($whereParams),
156+
);
149157
}
150158
}
151159

@@ -192,8 +200,15 @@ protected function _countAll(array|Where $where = []): int
192200
*/
193201
protected function _findByPk(mixed $pk): mixed
194202
{
195-
$where = $this->getPkCondition($pk);
196-
return $this->_findOne($where);
203+
$whereParams = $this->getPkCondition($pk);
204+
$whereString = implode(' AND ', array_map(fn ($key) => $this->escapeIdentifier($key) . "=?", array_keys($whereParams)));
205+
$row = $this->getConnection()
206+
->executeQuery(
207+
sql: "SELECT * FROM " . $this->escapeIdentifier($this->getTableName()) . " WHERE $whereString;",
208+
params: array_values($whereParams),
209+
)
210+
->fetchAssociative();
211+
return $this->createEntity($row);
197212
}
198213

199214
/**
@@ -304,7 +319,14 @@ protected function getPkCondition(int|string|array|AbstractEntity|UuidInterface
304319
{
305320
$condition = [];
306321
if ($data instanceof AbstractEntity) {
307-
$data = $data->toArray();
322+
if ($data->isNew()) {
323+
$data = $data->toArray();
324+
} else {
325+
foreach ($this->config->primaryKeys as $key) {
326+
$condition[$key] = $data->getOldValue($key);
327+
}
328+
return $condition;
329+
}
308330
}
309331
if (is_array($data)) {
310332
foreach ($this->config->primaryKeys as $key) {
@@ -324,20 +346,4 @@ private function checkUpdatedAt(AbstractEntity $entity): void
324346
$entity->updated_at = new \DateTimeImmutable();
325347
}
326348
}
327-
328-
/**
329-
* @param array<string, mixed> $data
330-
* @return array<string, mixed>
331-
* @throws \Doctrine\DBAL\Exception
332-
*/
333-
private function formatData(array $data): array
334-
{
335-
$supportsBoolean = $this->getConnection()->getDatabasePlatform() instanceof PostgreSQLPlatform;
336-
foreach ($data as $columnName => $value) {
337-
if (is_bool($value) && !$supportsBoolean) {
338-
$data[$columnName] = $value ? 1 : 0;
339-
}
340-
}
341-
return $data;
342-
}
343349
}

src/Helpers/DatabaseSpecificTrait.php

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
<?php declare(strict_types=1);
2+
3+
namespace Composite\DB\Helpers;
4+
5+
use Composite\DB\Exceptions\DbException;
6+
use Doctrine\DBAL\Driver;
7+
8+
trait DatabaseSpecificTrait
9+
{
10+
private ?bool $isPostgreSQL = null;
11+
private ?bool $isMySQL = null;
12+
private ?bool $isSQLite = null;
13+
14+
private function identifyPlatform(): void
15+
{
16+
if ($this->isPostgreSQL !== null) {
17+
return;
18+
}
19+
$driver = $this->getConnection()->getDriver();
20+
if ($driver instanceof Driver\AbstractPostgreSQLDriver) {
21+
$this->isPostgreSQL = true;
22+
$this->isMySQL = $this->isSQLite = false;
23+
} elseif ($driver instanceof Driver\AbstractSQLiteDriver) {
24+
$this->isSQLite = true;
25+
$this->isPostgreSQL = $this->isMySQL = false;
26+
} elseif ($driver instanceof Driver\AbstractMySQLDriver) {
27+
$this->isMySQL = true;
28+
$this->isPostgreSQL = $this->isSQLite = false;
29+
} else {
30+
// @codeCoverageIgnoreStart
31+
throw new DbException('Unsupported driver ' . $driver::class);
32+
// @codeCoverageIgnoreEnd
33+
}
34+
}
35+
36+
/**
37+
* @param array<string, mixed> $data
38+
* @return array<string, mixed>
39+
*/
40+
private function prepareDataForSql(array $data): array
41+
{
42+
$this->identifyPlatform();
43+
foreach ($data as $columnName => $value) {
44+
if (is_bool($value) && !$this->isPostgreSQL) {
45+
$data[$columnName] = $value ? 1 : 0;
46+
}
47+
}
48+
return $data;
49+
}
50+
51+
protected function escapeIdentifier(string $key): string
52+
{
53+
$this->identifyPlatform();
54+
if ($this->isMySQL) {
55+
return implode('.', array_map(fn ($part) => "`$part`", explode('.', $key)));
56+
} else {
57+
return '"' . $key . '"';
58+
}
59+
}
60+
}

src/SelectRawTrait.php renamed to src/Helpers/SelectRawTrait.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
<?php declare(strict_types=1);
22

3-
namespace Composite\DB;
3+
namespace Composite\DB\Helpers;
44

5+
use Composite\DB\Where;
56
use Doctrine\DBAL\Query\QueryBuilder;
67

78
trait SelectRawTrait

src/MultiQuery/MultiInsert.php

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,14 @@
22

33
namespace Composite\DB\MultiQuery;
44

5+
use Composite\DB\Helpers\DatabaseSpecificTrait;
6+
use Doctrine\DBAL\Connection;
7+
58
class MultiInsert
69
{
10+
use DatabaseSpecificTrait;
11+
12+
private Connection $connection;
713
private string $sql = '';
814
/** @var array<string, mixed> */
915
private array $parameters = [];
@@ -12,13 +18,14 @@ class MultiInsert
1218
* @param string $tableName
1319
* @param list<array<string, mixed>> $rows
1420
*/
15-
public function __construct(string $tableName, array $rows) {
21+
public function __construct(Connection $connection, string $tableName, array $rows) {
1622
if (!$rows) {
1723
return;
1824
}
25+
$this->connection = $connection;
1926
$firstRow = reset($rows);
20-
$columnNames = array_map(fn($columnName) => "`$columnName`", array_keys($firstRow));
21-
$this->sql = "INSERT INTO `$tableName` (" . implode(', ', $columnNames) . ") VALUES ";
27+
$columnNames = array_map(fn ($columnName) => $this->escapeIdentifier($columnName), array_keys($firstRow));
28+
$this->sql = "INSERT INTO " . $this->escapeIdentifier($tableName) . " (" . implode(', ', $columnNames) . ") VALUES ";
2229
$valuesSql = [];
2330

2431
$index = 0;
@@ -47,4 +54,9 @@ public function getParameters(): array
4754
{
4855
return $this->parameters;
4956
}
57+
58+
private function getConnection(): Connection
59+
{
60+
return $this->connection;
61+
}
5062
}

tests/MultiQuery/MultiInsertTest.php

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
namespace Composite\DB\Tests\MultiQuery;
44

5+
use Composite\DB\ConnectionManager;
56
use Composite\DB\MultiQuery\MultiInsert;
67

78
class MultiInsertTest extends \PHPUnit\Framework\TestCase
@@ -11,7 +12,8 @@ class MultiInsertTest extends \PHPUnit\Framework\TestCase
1112
*/
1213
public function test_multiInsertQuery($tableName, $rows, $expectedSql, $expectedParameters)
1314
{
14-
$multiInserter = new MultiInsert($tableName, $rows);
15+
$connection = ConnectionManager::getConnection('sqlite');
16+
$multiInserter = new MultiInsert($connection, $tableName, $rows);
1517

1618
$this->assertEquals($expectedSql, $multiInserter->getSql());
1719
$this->assertEquals($expectedParameters, $multiInserter->getParameters());
@@ -31,7 +33,7 @@ public static function multiInsertQuery_dataProvider()
3133
[
3234
['a' => 'value1_1', 'b' => 'value2_1'],
3335
],
34-
"INSERT INTO `testTable` (`a`, `b`) VALUES (:a0, :b0);",
36+
'INSERT INTO "testTable" ("a", "b") VALUES (:a0, :b0);',
3537
['a0' => 'value1_1', 'b0' => 'value2_1']
3638
],
3739
[
@@ -40,7 +42,7 @@ public static function multiInsertQuery_dataProvider()
4042
['a' => 'value1_1', 'b' => 'value2_1'],
4143
['a' => 'value1_2', 'b' => 'value2_2']
4244
],
43-
"INSERT INTO `testTable` (`a`, `b`) VALUES (:a0, :b0), (:a1, :b1);",
45+
'INSERT INTO "testTable" ("a", "b") VALUES (:a0, :b0), (:a1, :b1);',
4446
['a0' => 'value1_1', 'b0' => 'value2_1', 'a1' => 'value1_2', 'b1' => 'value2_2']
4547
],
4648
[
@@ -49,7 +51,7 @@ public static function multiInsertQuery_dataProvider()
4951
['column1' => 'value1_1'],
5052
['column1' => 123]
5153
],
52-
"INSERT INTO `testTable` (`column1`) VALUES (:column10), (:column11);",
54+
'INSERT INTO "testTable" ("column1") VALUES (:column10), (:column11);',
5355
['column10' => 'value1_1', 'column11' => 123]
5456
]
5557
];

0 commit comments

Comments
 (0)