justraviga / laravel-dynamodb-extreme
JustRaviga DynamoDb access package
Requires
- php: ^8.2
- aws/aws-sdk-php: ^3.269
- laravel/framework: ^10.0
Requires (Dev)
- orchestra/testbench: ^8.5
- pestphp/pest: ^2.0
- pestphp/pest-plugin-laravel: ^2.0
- phpunit/phpunit: ^10.0
This package is auto-updated.
Last update: 2024-12-09 19:03:11 UTC
README
This is a Query Builder package for DynamoDb access. Inspired by other versions that didn't quite feel eloquent enough (see what I did there?), this one has the following features, just like Laravel:
Get a single Model instance from DynamoDb
$model = Model::find($partitionKey, $sortKey);
Or, to throw Illuminate\Database\Eloquent\ModelNotFoundException
if no results are returned:
$model = Model::findOrFail($partitionKey, $sortKey);
Get a Collection of Model instances from DynamoDb
Notes:
- Only a single Model type is supported here. You must make sure your query will only return models of the same type.
- An Exception will be thrown if a property found in the database is not in the $fillable array on the model.
- DynamoDb only supports exact matches on Partition keys, and
<
,<=
,=
,>=
,>
,begins_with
, andbetween
matches on Sort Keys.
/** @var DynamoDbResult $result */ $result = Model::where('partitionKey', $partitionKey) ->where('sortKey', 'begins_with', 'MODEL#') ->get(); /** @var \Illuminate\Support\Collection<DynamoDbModel> $models */ $models = $result->results;
You can optionally sort on the sortKey and limit the number of results:
/** @var DynamoDbResult $models */ $models = Model::where('partitionKey', $partitionKey) ->sortDescending() ->limit(10) ->get();
If you know you're going to exceed the 1mb query size limit of DynamoDb, you can use the getAll()
method to make
many queries until the entire result set is returned:
/** @var DynamoDbResult $models */ $models = Model::where('partitionKey', $partitionKey) ->getAll();
You can also get a specific set of results combining limit()
and after()
:
$twoResults = Model::where('partitionKey', $partitionKey) ->limit(2) ->get(); $twoMoreResults = Model::where('partitionKey', $partitionKey) ->after($twoResults->lastEvaluatedKey) ->limit(2) ->get();
A shortcut for this is built into the paginate
method:
$twoResults = Model::where('partitionKey', $partitionKey) ->limit(2) ->paginate(); $twoMoreResults = Model::where('partitionKey', $partitionKey) ->limit(2) ->paginate($twoResults->lastEvaluatedKey());
Get a collection of models using a Secondary Index
Provided the secondary index is configured (see below), it will be detected from the fields you are querying.
/** @var \Illuminate\Support\Collection<Model> $models */ $models = Model::where('index_partition_key', $partitionKey) ->where('index_sort_key', $sortKey) ->get();
Create a Model instance in DynamoDb
Immediately persist the model with create
:
$model = Model::create([ 'partitionKey' => 'value', 'sortKey' => 'value', // other attributes... ]);
Build the model in memory without making a database query with make
:
$model = Model::make([ 'partitionKey' => 'value', 'sortKey' => 'sortKey', // other attributes... ]); // persist the model $model->save();
Alternatively use the new Model()
syntax:
$model = new Model([ 'partitionKey' => 'value', 'sortKey' => 'value', // other attributes ]); // persist the model $model->save();
Accessing Model attributes
Once you've created your model, attributes are accessed the same as you would with Laravel, using object property syntax:
$model = Model::find($partitionKey, $sortKey); $model->someAttribute = 'value';
You can update many properties at the same time using the fill
method:
$model = Model::find($partitionKey, $sortKey); $model->fill([ 'someAttribute' => 'value', // other attributes ]);
These changes are not automatically persisted back to Dynamo, you need to save them manually:
$model = Model::find($partitionKey, $sortKey); $model->someAttribute = 'value'; $model->save();
or
$model = Model::find($partitionKey, $sortKey); $model->fill([ 'someAttribute' => 'value', ])->save();
or even, to combine fill
and save
:
$model = Model::find($partitionKey, $sortKey); $model->update([ 'someAttribute' => 'value', ]);
Setup and global config
Environment variables
DYNAMODB_REGION
defaults tolocalhost
, should be set to your main DynamoDb instance region (eu-west-2, for example)DYNAMODB_VERSION
defaults tolatest
, should be set to the version of your DynamoDb instance if you need itDYNAMODB_KEY
your DynamoDb access key (username)DYNAMODB_SECRET
your DynamoDb secret (password)DYNAMODB_ENDPOINT
defaults tohttp://localhost:8000
, the address of your DynamoDb installationDYNAMODB_TABLE
to define a default table for all models (useful when working with a single-table design in a specific application)DYNAMODB_CONSISTENT_READ
defaults totrue
, use to set a default consistent read value (can still be overwritten by specific models)DYNAMODB_LOG_QUERIES
defaults tofalse
, use to add logging for all DynamoDb queries made (the json object being sent to Dynamo will be logged)
Configuration options
The config file can be exported with Laravel's publish command: php artisan vendor:publish
, it looks like this:
[ 'region' => env('DYNAMODB_REGION', 'localhost'), 'version' => env('DYNAMODB_VERSION', 'latest'), 'credentials' => [ 'key' => env('DYNAMODB_KEY', ''), 'secret' => env('DYNAMODB_SECRET', ''), ], 'endpoint' => env('DYNAMODB_ENDPOINT', 'http://localhost:8000'), 'defaults' => [ 'consistent_read' => env('DYNAMODB_CONSISTENT_READ', true), 'table' => env('DYNAMODB_TABLE', 'default'), 'partition_key' => 'pk', 'sort_key' => 'sk', 'global_secondary_indexes' => [ 'gsi1' => [ 'pk' => 'gsi1_pk', 'sk' => 'gsi1_sk', ] ], 'log_queries' => env('DYNAMODB_LOG_QUERIES', false), ], ]
Apart from the environment-based config, here you can specify defaults for partition key, sort key, and global secondary indexes. These are intended to be sensible defaults based on general usage patterns of DynamoDb.
Model Configuration
Creating a model is easy. At a bare minimum, you just need to define the table name, the partitionKey and sortKey, and an array of "fillable" attributes (note the partition and sort keys need to be fillable!):
class Model extends \DynamoDb\Models\DynamoDbModel { // optional, see environment variable for table name protected string $table = 'models'; // optional, see environment variable for partition/sort keys protected string $partitionKey = 'pk'; protected string $sortKey = 'sk'; // required! must include at least partition/sort keys public array $fillable = [ 'pk', 'sk', // other attributes... ]; }
To write more verbose code, you can create a mapping from internal/database fields to friendly attributes by overriding
the fieldMappings
protected property. The mappings are applied when fetching from the database and when saving to the
database. The rest of the time, you always use the mapped property name.
class Model extends \DynamoDb\Models\DynamoDbModel { // optional protected array $fieldMappings = [ 'pk' => 'my_uuid', 'sk' => 'created_date', ]; }
By default, all models' Partition Keys are built with the model class name followed by a #
then a UUID-7 (time-ordered UUID).
Sort Keys default to the model name.
For example:
class Model extends \DynamoDb\Models\DynamoDbModel { } $model = new Model(); // partition key = 'MODEL#{uuid-7} // sort key = 'MODEL'
You can also change these default values when creating new models by overriding these methods:
class Model extends \DynamoDb\Models\DynamoDbModel { public function defaultPartitionKey(): string { return 'MODEL#' . random_int(0,1000); } public function defaultSortKey(): string { return 'CREATED_AT#' . now()->toISOString(); } public function defaultValues(): array { return [ 'attribute' => 'default value', ]; } }
Related models
You can configure related models that share the same Partition Key with some minor extra setup:
class ParentModel extends \DynamoDb\Models\DynamoDbModel { public function childModels(): \DynamoDb\DynamoDb\Relation { return $this->addRelation(ChildModel::class); } }
By default, this uses the Partition key of the parent model, and matches on the Sort key with a "begins_with" query against the child model's class name followed by a hash (#), something like begins_with(sk, 'CHILDMODEL#')
.
The matching itself can be configured on the child models by overriding the relationSearchParams
method:
class ChildModel extends \DynamoDb\Models\DynamoDbModel { public static function relationSearchParams(): array { return [ 'sk', 'begins_with', 'CLASSNAME#' ]; } }
The related models can be accessed as a Collection using a property of the same name as the relationship method, e.g:
class Model extends \DynamoDb\Models\DynamoDbModel { public function children(): \DynamoDb\DynamoDb\Relation { return $this->addRelation(Child::class); } } $model = Model::find(...); $model->children->map(fn ($child) => $child->doSomething());
"inline" relations
In addition to using different rows in a DynamoDb table to hold related models, you can use a JSON attribute on a record as a relation. We call this an "inline relation" and they can be accessed the same way as other relations:
class Model extends \DynamoDb\Models\DynamoDbModel { public function inline(): InlineRelation { return $this->addInlineRelation(Inline::class, 'inline'); } } $model = Model::find(...); $model->inline->map(fn ($child) => $child->doSomething());
If you want to use a secondary index on your table, just define the Partition and Sort Key names in the $indexes
array.
Be sure to add them to the $fillable
array as well.
class Model extends \DynamoDb\Models\DynamoDbModel { // optional, see config values protected array $indexes = [ 'gsi1' => [ 'pk' => 'gsi1_pk', 'sk' => 'gsi1_sk', ], ]; // required if you want to use a secondary index public array $fillable = [ 'gsi1_pk', 'gsi1_sk', // other attributes... ]; }
You can define mappings from secondary indexes to friendly model properties in the $fieldMappings
array too.
class Model extends \JustRaviga\LaravelDynamodbExtreme\Models\DynamoDbModel { // optional protected array $fieldMappings = [ 'gsi1_pk' => 'user_uuid', 'gsi1_sk' => 'created_date', ]; }
Minimum Requirements
- PHP 8.2
- Laravel 10.0
Wishlist
The things we might like to include (eventually) but didn't have time to properly consider:
Wrapping DynamoDb responses in a cache layer
Optionally have models fetched from DynamoDb stored in memory for the duration of the request so multiple calls to the same partition/sort key return the same data without querying Dynamo.
Attribute casts
Similar to the Laravel model $casts array, attributes should be coerced to/from these data types when fetching and saving with DynamoDb.
Extend the Collection class to include filter methods based on DynamoDb models
Potential for new methods on the Collection class for pagination based on the Last Evaluated Key from DynamoDb.
Better documentation 🙈
Docs can always be improved.
Concurrent requests
Figure out how to make many concurrent requests that can be awaited and only continue execution once all of the requests have returned their data.
Table generation in code
Essentially Laravel's migration system for DynamoDb tables
Cascade deletions
Delete related models after deleting a parent model