Skip to content

Commit

Permalink
Option "Saving via Eloquent Model" (#21)
Browse files Browse the repository at this point in the history
* fix phpunit config deprectaion

* added model param to config and MessageData

* Separate savers

* Move upsert/delete logic from Upserter; Implements Eloquent Saver

* Added tests to MessageDataRetrieverTest

* Delete Destroyer and Upserter; Reorganize structure

* Tests for upsert&destroy via non-unique target keys

* Tests for upsert&destroy with small chunk

* optimize

---------

Co-authored-by: evgeny <evgeny.iva@cadolabs.io>
  • Loading branch information
evgeek and evgeny committed May 26, 2023
1 parent 869b0f7 commit 4770e96
Show file tree
Hide file tree
Showing 20 changed files with 602 additions and 303 deletions.
30 changes: 16 additions & 14 deletions phpunit.xml.dist
Original file line number Diff line number Diff line change
@@ -1,19 +1,13 @@
<?xml version="1.0" encoding="UTF-8"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
bootstrap="vendor/autoload.php"
colors="true" processIsolation="false"
stopOnFailure="false"
xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/10.0/phpunit.xsd"
cacheDirectory=".phpunit.cache"
<phpunit
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
bootstrap="vendor/autoload.php"
colors="true"
processIsolation="false"
stopOnFailure="false"
xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/10.1/phpunit.xsd"
cacheDirectory=".phpunit.cache"
>
<coverage>
<include>
<directory suffix=".php">./src</directory>
</include>
<exclude>
<directory suffix=".php">./database</directory>
</exclude>
</coverage>
<php>
<env name="APP_ENV" value="testing"/>
<ini name="error_reporting" value="-1"/>
Expand All @@ -29,4 +23,12 @@
<directory suffix="Test.php">./tests</directory>
</testsuite>
</testsuites>
<source>
<include>
<directory suffix=".php">./src</directory>
</include>
<exclude>
<directory suffix=".php">./database</directory>
</exclude>
</source>
</phpunit>
28 changes: 0 additions & 28 deletions src/Integration/Laravel/Receive/Destroyer.php

This file was deleted.

34 changes: 30 additions & 4 deletions src/Integration/Laravel/Receive/MessageData/MessageData.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,23 @@

namespace Umbrellio\TableSync\Integration\Laravel\Receive\MessageData;

use Illuminate\Database\Eloquent\Model;
use Umbrellio\TableSync\Integration\Laravel\Receive\Savers\Saver;

class MessageData
{
public function __construct(
private readonly string $table,
private readonly string $target,
private readonly Saver $saver,
private readonly array $targetKeys,
private readonly array $data
private readonly array $data,
) {
}

public function getTable(): string
/** @return non-empty-string|class-string<Model> */
public function getTarget(): string
{
return $this->table;
return $this->target;
}

public function getTargetKeys(): array
Expand All @@ -27,4 +32,25 @@ public function getData(): array
{
return $this->data;
}

public function upsert(float $version): void
{
if ($this->isEmpty()) {
return;
}
$this->saver->upsert($this, $version);
}

public function destroy(): void
{
if ($this->isEmpty()) {
return;
}
$this->saver->destroy($this);
}

private function isEmpty(): bool
{
return empty(array_filter($this->data));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,13 @@

namespace Umbrellio\TableSync\Integration\Laravel\Receive\MessageData;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\App;
use Umbrellio\TableSync\Integration\Laravel\Exceptions\Receive\IncorrectAdditionalDataHandler;
use Umbrellio\TableSync\Integration\Laravel\Exceptions\Receive\IncorrectConfiguration;
use Umbrellio\TableSync\Integration\Laravel\Receive\Savers\EloquentSaver;
use Umbrellio\TableSync\Integration\Laravel\Receive\Savers\QuerySaver;
use Umbrellio\TableSync\Integration\Laravel\Receive\Savers\Saver;
use Umbrellio\TableSync\Messages\ReceivedMessage;

class MessageDataRetriever
Expand All @@ -19,11 +24,11 @@ public function __construct(
public function retrieve(ReceivedMessage $message): MessageData
{
$messageConfig = $this->configForMessage($message);
$table = $this->retrieveTable($messageConfig);
[$target, $saver] = $this->retrieveTargetAndSaver($messageConfig);
$targetKeys = $this->retrieveTargetKeys($messageConfig);
$data = $this->retrieveData($message, $messageConfig);

return new MessageData($table, $targetKeys, $data);
return new MessageData($target, $saver, $targetKeys, $data);
}

private function configForMessage(ReceivedMessage $message): array
Expand All @@ -35,13 +40,24 @@ private function configForMessage(ReceivedMessage $message): array
return $this->config[$message->getModel()];
}

private function retrieveTable(array $messageConfig): string
/** @return array{0: string, 1: Saver} */
private function retrieveTargetAndSaver(array $messageConfig): array
{
if (!isset($messageConfig['table'])) {
throw new IncorrectConfiguration('Table configuration required');
$table = $messageConfig['table'] ?? null;
$model = $messageConfig['model'] ?? null;
if (!$table && !$model) {
throw new IncorrectConfiguration('Table or Model configuration required');
}
if ($table && $model) {
throw new IncorrectConfiguration('Table and Model configuration cannot be set simultaneously');
}
if ($model && !is_subclass_of($model, Model::class)) {
throw new IncorrectConfiguration('Model must be subclass of ' . Model::class);
}

return $messageConfig['table'];
return $table ?
[$table, App::make(QuerySaver::class)] :
[$model, App::make(EloquentSaver::class)];
}

private function retrieveTargetKeys(array $messageConfig): array
Expand Down
8 changes: 2 additions & 6 deletions src/Integration/Laravel/Receive/Receiver.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,8 @@

namespace Umbrellio\TableSync\Integration\Laravel\Receive;

use Illuminate\Support\Facades\App;
use Umbrellio\TableSync\Integration\Laravel\Exceptions\UnknownMessageEvent;
use Umbrellio\TableSync\Integration\Laravel\Receive\MessageData\MessageDataRetriever;
use Umbrellio\TableSync\Integration\Laravel\Receive\Upserter\Upserter;
use Umbrellio\TableSync\Messages\ReceivedMessage;

class Receiver
Expand All @@ -24,12 +22,10 @@ public function receive(ReceivedMessage $message): void

switch ($event) {
case 'update':
$upserter = App::make(Upserter::class);
$upserter->upsert($data, $message->getVersion());
$data->upsert($message->getVersion());
break;
case 'destroy':
$destroyer = new Destroyer();
$destroyer->destroy($data);
$data->destroy();
break;
default:
throw new UnknownMessageEvent("Unknown event: {$event}");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

declare(strict_types=1);

namespace Umbrellio\TableSync\Integration\Laravel\Receive\Upserter;
namespace Umbrellio\TableSync\Integration\Laravel\Receive\Savers\ConflictResolvers;

use Umbrellio\TableSync\Integration\Laravel\Receive\MessageData\MessageData;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

declare(strict_types=1);

namespace Umbrellio\TableSync\Integration\Laravel\Receive\Upserter;
namespace Umbrellio\TableSync\Integration\Laravel\Receive\Savers\ConflictResolvers;

use Umbrellio\TableSync\Integration\Laravel\Receive\MessageData\MessageData;

Expand Down
88 changes: 88 additions & 0 deletions src/Integration/Laravel/Receive/Savers/EloquentSaver.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
<?php

declare(strict_types=1);

namespace Umbrellio\TableSync\Integration\Laravel\Receive\Savers;

use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\Config;
use Umbrellio\TableSync\Integration\Laravel\Receive\MessageData\MessageData;

class EloquentSaver implements Saver
{
private const DEFAULT_LIMIT = 500;

public function upsert(MessageData $messageData, float $version): void
{
foreach ($messageData->getData() as $item) {
$query = $this->getQueryByTargetKeys($messageData, $item);

if ($query->count() === 0) {
$model = new ($messageData->getTarget())();
$this->fillAndSaveModel($model, $version, array_keys($item), $item);
continue;
}

$this->updateChanged($query, $version, $messageData, $item);
}
}

public function destroy(MessageData $messageData): void
{
foreach ($messageData->getData() as $item) {
$query = $this
->getQueryByTargetKeys($messageData, $item)
->limit($this->getLimit());

while ($query->count() !== 0) {
$query
->get()
->each(fn (Model $model) => $model->forceDelete());
}
}
}

protected function getQueryByTargetKeys(MessageData $messageData, array $item): Builder
{
/** @var class-string<Model> $modelClass */
$modelClass = $messageData->getTarget();

return $modelClass::query()->where(Arr::only($item, $messageData->getTargetKeys()));
}

protected function fillAndSaveModel(Model $model, float $version, array $columns, array $values): void
{
foreach ($columns as $key) {
$model->{$key} = $values[$key];
}
$model->setAttribute('version', $version);
$model->save();
}

protected function updateChanged(Builder $query, float $version, MessageData $messageData, array $item): void
{
$columns = array_keys($messageData->getData()[0]);
$updateColumns = array_diff($columns, $messageData->getTargetKeys());

$query->where('version', '<', $version)
->where(function (Builder $builder) use ($updateColumns, $item) {
foreach ($updateColumns as $column) {
$builder->orWhere($column, '!=', $item[$column]);
}
})
->limit($this->getLimit());

while ($query->count() !== 0) {
$query
->get()
->each(fn (Model $model) => $this->fillAndSaveModel($model, $version, $updateColumns, $item));
}
}

protected function getLimit(): int
{
return Config::get('table_sync.receive.eloquent_chunk_size', self::DEFAULT_LIMIT);
}
}
Loading

0 comments on commit 4770e96

Please sign in to comment.