diff --git a/src/db/Capacitor.php b/src/db/Capacitor.php index 7f321df..61ab4b4 100644 --- a/src/db/Capacitor.php +++ b/src/db/Capacitor.php @@ -3,6 +3,7 @@ namespace nur\sery\db; use nur\sery\php\func; use nur\sery\ValueException; +use Traversable; /** * Class Capacitor: un objet permettant d'attaquer un canal spécifique d'une @@ -140,7 +141,7 @@ class Capacitor implements ITransactor { return $this->storage->_charge($this->channel, $item, $func, $args, $values); } - function discharge(bool $reset=true): iterable { + function discharge(bool $reset=true): Traversable { return $this->storage->_discharge($this->channel, $reset); } @@ -152,7 +153,7 @@ class Capacitor implements ITransactor { return $this->storage->_one($this->channel, $filter, $mergeQuery); } - function all($filter, ?array $mergeQuery=null): iterable { + function all($filter, ?array $mergeQuery=null): Traversable { return $this->storage->_all($this->channel, $filter, $mergeQuery); } diff --git a/src/db/CapacitorChannel.php b/src/db/CapacitorChannel.php index 94b360e..ca060f0 100644 --- a/src/db/CapacitorChannel.php +++ b/src/db/CapacitorChannel.php @@ -3,6 +3,7 @@ namespace nur\sery\db; use nur\sery\cl; use nur\sery\str; +use Traversable; /** * Class CapacitorChannel: un canal d'une instance de {@link ICapacitor} @@ -20,10 +21,19 @@ class CapacitorChannel { const EACH_COMMIT_THRESHOLD = 100; + /** + * @var bool faut-il passer par le cache pour les requêtes de each() + * ça peut être nécessaire avec MySQL/MariaDB si on utilise les requêtes non + * bufférisées, et que la fonction + */ + const USE_CACHE = false; + static function verifix_name(?string &$name, ?string &$tableName=null): void { if ($name !== null) { $name = strtolower($name); - if ($tableName === null) $tableName = "${name}_channel"; + if ($tableName === null) { + $tableName = str_replace("-", "_", $tableName) . "_channel"; + } } else { $name = static::class; if ($name === self::class) { @@ -53,6 +63,7 @@ class CapacitorChannel { $this->tableName = $tableName; $this->manageTransactions = $manageTransactions ?? static::MANAGE_TRANSACTIONS; $this->eachCommitThreshold = self::verifix_eachCommitThreshold($eachCommitThreshold); + $this->useCache = static::USE_CACHE; $this->setup = false; $this->created = false; $columnDefinitions = cl::withn(static::COLUMN_DEFINITIONS); @@ -122,6 +133,17 @@ class CapacitorChannel { return $this; } + protected bool $useCache; + + function isUseCache(): bool { + return $this->useCache; + } + + function setUseCache(bool $useCache=true): self { + $this->useCache = $useCache; + return $this; + } + /** * initialiser ce channel avant sa première utilisation. */ @@ -358,7 +380,7 @@ class CapacitorChannel { return $this->capacitor->charge($item, $func, $args, $values); } - function discharge(bool $reset=true): iterable { + function discharge(bool $reset=true): Traversable { return $this->capacitor->discharge($reset); } @@ -370,7 +392,7 @@ class CapacitorChannel { return $this->capacitor->one($filter, $mergeQuery); } - function all($filter, ?array $mergeQuery=null): iterable { + function all($filter, ?array $mergeQuery=null): Traversable { return $this->capacitor->all($filter, $mergeQuery); } diff --git a/src/db/CapacitorStorage.php b/src/db/CapacitorStorage.php index e61a891..576765f 100644 --- a/src/db/CapacitorStorage.php +++ b/src/db/CapacitorStorage.php @@ -2,8 +2,10 @@ namespace nur\sery\db; use nur\sery\cl; +use nur\sery\db\cache\cache; use nur\sery\php\func; use nur\sery\ValueException; +use Traversable; /** * Class CapacitorStorage: objet permettant d'accumuler des données pour les @@ -364,7 +366,7 @@ EOT; } /** décharger les données du canal spécifié */ - function _discharge(CapacitorChannel $channel, bool $reset=true): iterable { + function _discharge(CapacitorChannel $channel, bool $reset=true): Traversable { $this->_create($channel); $rows = $this->db()->all([ "select item__", @@ -376,7 +378,7 @@ EOT; if ($reset) $this->_reset($channel); } - function discharge(?string $channel, bool $reset=true): iterable { + function discharge(?string $channel, bool $reset=true): Traversable { return $this->_discharge($this->getChannel($channel), $reset); } @@ -452,12 +454,7 @@ EOT; return $this->_one($this->getChannel($channel), $filter, $mergeQuery); } - /** - * obtenir les lignes correspondant au filtre sur le canal spécifié - * - * si $filter n'est pas un tableau, il est transformé en ["id_" => $filter] - */ - function _all(CapacitorChannel $channel, $filter, ?array $mergeQuery=null): iterable { + private function _allCached(string $id, CapacitorChannel $channel, $filter, ?array $mergeQuery=null): Traversable { $this->_create($channel); $this->verifixFilter($channel, $filter); $rows = $this->db()->all(cl::merge([ @@ -465,12 +462,28 @@ EOT; "from" => $channel->getTableName(), "where" => $filter, ], $mergeQuery), null, $this->getPrimaryKeys($channel)); + if ($channel->isUseCache()) { + $cacheIds = [$id, get_class($channel)]; + cache::get()->resetCached($cacheIds); + $rows = cache::new(null, $cacheIds, function() use ($rows) { + yield from $rows; + }); + } foreach ($rows as $key => $row) { yield $key => $this->unserialize($channel, $row); } } - function all(?string $channel, $filter, $mergeQuery=null): iterable { + /** + * obtenir les lignes correspondant au filtre sur le canal spécifié + * + * si $filter n'est pas un tableau, il est transformé en ["id_" => $filter] + */ + function _all(CapacitorChannel $channel, $filter, ?array $mergeQuery=null): Traversable { + return $this->_allCached("all", $channel, $filter, $mergeQuery); + } + + function all(?string $channel, $filter, $mergeQuery=null): Traversable { return $this->_all($this->getChannel($channel), $filter, $mergeQuery); } @@ -504,7 +517,8 @@ EOT; $tableName = $channel->getTableName(); try { $args ??= []; - foreach ($this->_all($channel, $filter, $mergeQuery) as $values) { + $all = $this->_allCached("each", $channel, $filter, $mergeQuery); + foreach ($all as $values) { $rowIds = $this->getRowIds($channel, $values); $updates = func::_call($onEach, [$values["item"], $values, ...$args]); if (is_array($updates) && $updates) { @@ -571,7 +585,8 @@ EOT; $tableName = $channel->getTableName(); try { $args ??= []; - foreach ($this->_all($channel, $filter) as $values) { + $all = $this->_allCached("delete", $channel, $filter); + foreach ($all as $values) { $rowIds = $this->getRowIds($channel, $values); $delete = boolval(func::_call($onEach, [$values["item"], $values, ...$args])); if ($delete) { diff --git a/src/db/cache/CacheChannel.php b/src/db/cache/CacheChannel.php new file mode 100644 index 0000000..e7ed3e7 --- /dev/null +++ b/src/db/cache/CacheChannel.php @@ -0,0 +1,115 @@ + "varchar(64) not null", + "id" => "varchar(64) not null", + "date_start" => "datetime", + "duration_" => "text", + "primary key (group_id, id)", + ]; + + static function get_cache_ids($id): array { + if (is_array($id)) { + $keys = array_keys($id); + if (array_key_exists("group_id", $id)) $groupIdKey = "group_id"; + else $groupIdKey = $keys[1] ?? null; + $groupId = $id[$groupIdKey] ?? ""; + if (array_key_exists("id", $id)) $idKey = "id"; + else $idKey = $keys[0] ?? null; + $id = $id[$idKey] ?? ""; + } else { + $groupId = ""; + } + if (preg_match('/^(.*\\\\)?([^\\\\]+)$/', $groupId, $ms)) { + # si le groupe est une classe, faire un hash du package pour limiter la + # longueur du groupe + [$package, $groupId] = [$ms[1], $ms[2]]; + $package = substr(md5($package), 0, 4); + $groupId = "${groupId}_$package"; + } + return ["group_id" => $groupId, "id" => $id]; + } + + function __construct(?string $duration=null, ?string $name=null) { + parent::__construct($name); + $this->duration = $duration ?? static::DURATION; + $this->includes = static::INCLUDES; + $this->excludes = static::EXCLUDES; + } + + protected string $duration; + + protected ?array $includes; + + protected ?array $excludes; + + function getItemValues($item): ?array { + return cl::merge(self::get_cache_ids($item), [ + "item" => null, + ]); + } + + function onCreate($item, array $values, ?array $alwaysNull, ?string $duration=null): ?array { + $now = new DateTime(); + $duration ??= $this->duration; + return [ + "date_start" => $now, + "duration" => new Delay($duration, $now), + ]; + } + + function onUpdate($item, array $values, array $pvalues, ?string $duration=null): ?array { + $now = new DateTime(); + $duration ??= $this->duration; + return [ + "date_start" => $now, + "duration" => new Delay($duration, $now), + ]; + } + + function shouldUpdate($cacheIds, bool $noCache=false): bool { + if ($noCache) return true; + + $cacheIds = self::get_cache_ids($cacheIds); + $groupId = $cacheIds["group_id"]; + if ($groupId) { + $includes = $this->includes; + $shouldInclude = $includes !== null && in_array($groupId, $includes); + $excludes = $this->excludes; + $shouldExclude = $excludes !== null && in_array($groupId, $excludes); + if (!$shouldInclude || $shouldExclude) return true; + } + + $found = false; + $expired = false; + $this->each($cacheIds, + function($item, $values) use (&$found, &$expired) { + $found = true; + $expired = $values["duration"]->isElapsed(); + }); + return !$found || $expired; + } + + function setCached($cacheIds, ?string $duration=null): void { + $this->charge($cacheIds, null, [$duration]); + } + + function resetCached($cacheIds) { + $cacheIds = self::get_cache_ids($cacheIds); + $this->delete($cacheIds); + } +} diff --git a/src/db/cache/RowsChannel.php b/src/db/cache/RowsChannel.php new file mode 100644 index 0000000..feaefff --- /dev/null +++ b/src/db/cache/RowsChannel.php @@ -0,0 +1,51 @@ + "varchar(128) primary key not null", + "all_values" => "mediumtext", + ]; + + function __construct($id, callable $builder, ?string $duration=null) { + $this->cacheIds = $cacheIds = CacheChannel::get_cache_ids($id); + $this->builder = Closure::fromCallable($builder); + $this->duration = $duration; + $name = "{$cacheIds["group_id"]}-{$cacheIds["id"]}"; + parent::__construct($name); + } + + protected array $cacheIds; + + protected Closure $builder; + + protected ?string $duration = null; + + function getItemValues($item): ?array { + $key = array_keys($item)[0]; + $row = $item[$key]; + return [ + "key" => $key, + "item" => $row, + "all_values" => implode(" ", cl::filter_n(cl::with($row))), + ]; + } + + function getIterator(): Traversable { + $cm = cache::get(); + if ($cm->shouldUpdate($this->cacheIds)) { + $this->capacitor->reset(); + foreach (($this->builder)() as $key => $row) { + $this->charge([$key => $row]); + } + $cm->setCached($this->cacheIds, $this->duration); + } + return $this->discharge(false); + } +} diff --git a/src/db/cache/cache.php b/src/db/cache/cache.php new file mode 100644 index 0000000..d20fba2 --- /dev/null +++ b/src/db/cache/cache.php @@ -0,0 +1,37 @@ +options = $params["options"] ?? static::OPTIONS; # configuration - $config = $params["replace_config"]; + $config = $params["replace_config"] ?? null; if ($config === null) { $config = $params["config"] ?? static::CONFIG; if (is_callable($config)) $config = [$config]; diff --git a/src/db/sqlite/Sqlite.php b/src/db/sqlite/Sqlite.php index 0a24859..ede6c95 100644 --- a/src/db/sqlite/Sqlite.php +++ b/src/db/sqlite/Sqlite.php @@ -97,7 +97,7 @@ class Sqlite implements IDatabase { $defaultAllowWal = static::ALLOW_WAL ?? !$inMemory; $this->allowWal = $params["allow_wal"] ?? $defaultAllowWal; # configuration - $config = $params["replace_config"]; + $config = $params["replace_config"] ?? null; if ($config === null) { $config = $params["config"] ?? static::CONFIG; if (is_callable($config)) $config = [$config];