Laravel Key Value Datenbank
Um nicht für jede Kleinigkeit gleich das Datenbanklayout erweitern oder anpassen zu müssen, wollte ich einen Key-Value-Ansatz versuchen.
Im Unterschied zu einer klassischen Tabelle können mit einer Key-Value-Tabelle beliebige Felder gespeichert werden, ohne zusätzliche Spalten in der Datenbank erstellen zu müssen. Eine mögliche Alternative zu Key-Value könnte das Speichern als JSON-Datentyp darstellen, siehe: Laravel Store JSON .
Eine Key-Value-Datenbank ist zwar extrem flexibel, verliert dafür aber einige Möglichkeiten die eine Datenbank von Haus aus bieten würde. Nicht nur, dass den Feldern keine Datentypen mehr zugewiesen werden können, mit einer Key-Value-DB werden einfache Queries oder das Sortieren und Filtern nach bestimmten Kriterien zur Herausforderung. Zudem muss auch die Performance berücksichtigt werden: denn ein Index auf bestimmte Felder ist dann auf Spalten-Ebene nicht mehr möglich ...
Die Verwendung dieses Ansatzes sollte gut überlegt werden: In den meisten Fällen ist Key-Value nicht die richtige Lösung ...
Hier ein kurzes Beispiel von den Daten meiner Heizung:
eine klassiche Tabelle für meine Heizungsdaten würde ca. so aussehen:
Day | count_R | accuracy_perc | sum_H_min | sum_W_min | sum_U_min | sum_S_min | sum_power_wh | avg_c |
20200201 | 0 | 99 | 200 | 100 | 10 | 10 | 100 | 3 |
20200202 | 1 | 88 | 202 | 101 | 11 | 11 | 101 | 2 |
Die gleichen Daten sind in meiner Key-Value Datenbank wie folgt abgelegt:
type | scope | key | value |
heating_day | 20200201 | count_r | 0 |
heating_day | 20200201 | accuracy_perc | 99 |
heating_day | 20200201 | sum_H_min | 200 |
heating_day | 20200201 | sum_W_min | 100 |
heating_day | 20200201 | sum_U_min | 10 |
heating_day | 20200201 | sum_S_min | 10 |
heating_day | 20200201 | sum_power_wh | 100 |
heating_day | 20200201 | avg_c | 3 |
heating_day | 20200202 | count_r | 1 |
heating_day | 20200202 | accuracy_perc | 88 |
heating_day | 20200202 | sum_H_min | 202 |
heating_day | 20200202 | sum_W_min | 101 |
heating_day | 20200202 | sum_U_min | 11 |
heating_day | 20200202 | sum_S_min | 11 |
heating_day | 20200202 | sum_power_wh | 101 |
heating_day | 20200202 | avg_c | 2 |
Mittels "type" wird definiert um was es sich handelt, "scope" ist dann z.B. ein Zeitpunkt, ein Datum oder andere Kriterien die ich später für das Filtern der Daten verwenden kann.
Hier auch zum Vergleich wie eine Tabelle mit einet JSON-Spalte aussehen könnte:
type | scope | data |
heating_day | 20200201 | {"count_r":0,"accuracy_perc":99,"sum_H_min":200,"sum_W_min":100,"sum_U_min":100,"sum_S_min":10,"sum_power_wh":100,"avg_C":3} |
heating_day | 20200201 | {"count_r":1,"accuracy_perc":88,"sum_H_min":202,"sum_W_min":101,"sum_U_min":11,"sum_S_min":11,"sum_power_wh":101,"avg_C":2} |
siehe: Laravel Store JSON
Der Aufbau
Migration
Schema::create('key_values', function (Blueprint $table) {
$table->bigIncrements('id');
$table->string('type')->nullable();
$table->string('scope')->nullable();
$table->string('key');
$table->string('value');
$table->timestamps();
});​
Model
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class key_value extends Model
{
protected $fillable = [
'type','key', 'value', 'scope'
];
}
schreiben:
use App\key_value;
..
key_value::updateOrCreate(
['type' => 'heating_day', 'scope' => $day, 'key' => 'sum_0_min'],['value' => $val]
);
Der Speichervorgang ist noch relativ einfach, kniffeliger wird es, wenn wir Daten aus der Datenbank lesen wollen:
lesen:
hier habe ich eine Weile herumexperimentiert, am schönsten: bitte korrigiert mich wenn ihr da eine besser Idee habt, habe ich es mit der Funkton mapToGroups hinbekommen:
use App\key_value;
..
$weather_days= (key_value::where('type','heating_day')->orderBy('scope', 'DESC')->get())
->mapToGroups(function ($item, $key) {
return [$item['scope'] => [$item['key'] => $item['value']]];
});
foreach ($weather_days as $day => $weather_day) {
$count_h=head($weather_day->where("sum_H_min")->first()?? array())?: 0;
mapToGroups liefert mir pro Datengruppe eine Collection mit den entsprechenden Feldern als Array, schaut ca. so aus:
Illuminate\Support\Collection {#858 â–¼
#items: array:32 [â–¼
"2020-01-25" => Illuminate\Database\Eloquent\Collection {#384 â–¼
#items: array:8 [â–¼
0 => array:1 [â–¼
"count_R" => "1"
]
1 => array:1 [â–¼
"accuracy_perc" => "93.68"
]
2 => array:1 [â–¼
"sum_H_min" => "243"
]
3 => array:1 [â–¼
"sum_W_min" => "116"
]
4 => array:1 [â–¼
"sum_U_min" => "398"
]
5 => array:1 [â–¼
"sum_S_min" => "4"
]
6 => array:1 [â–¼
"sum_0_min" => "588"
]
7 => array:1 [â–¼
"sum_power_wh" => "1476"
]
]
}
"2020-01-24" => Illuminate\Database\Eloquent\Collection {#826 â–¶}
...
Dadurch dass ich die Typen bereits vorfiltere, habe ich aktuell mit diesem Ansatz kein Performance-Thema, zusätzlich könnte der Filter auch noch mit "WhereIn" auf bestimmte Keys eingeschränkt werden.
{{percentage}} % positiv