nham24/laravel-dynamodb

基于 DynamoDB 的 Eloquent 模型和查询构建器,适用于 Laravel。

1.0.1 2023-01-11 07:09 UTC

This package is not auto-updated.

Last update: 2024-09-18 17:08:12 UTC


README

本包仅是 kitar/laravel-dynamodb 的分支,以支持 Laravel 5.8。如果你的项目正在使用 Laravel 6+,你应该使用 kitar/laravel-dynamodb

test codecov

基于 DynamoDB 的 Eloquent 模型和查询构建器,适用于 Laravel。

动机

  • 我想使用 DynamoDB 与 Laravel。(例如,使用自定义用户提供者进行身份验证)
  • 我想使用一个简单的 API,无需担心手动处理表达式属性等繁琐事情。
  • 我想尽可能扩展 Laravel 的代码来
    • 依赖于 Laravel 严谨的代码。
    • 保持额外的实现简单且易于维护。
  • 我不想让它完全兼容 Eloquent,因为 DynamoDB 与关系型数据库不同。
  • 我渴望 jessengers/laravel-mongodb。如果我们有 DynamoDB 的那个会怎么样?

安装

通过 Composer 安装包

$ composer require nham24/laravel-dynamodb

Laravel

我们只支持 Laravel >= 5.8。

将 dynamodb 配置添加到 config/database.php

'connections' => [

    'dynamodb' => [
        'driver' => 'dynamodb',
        'region' => env('AWS_DEFAULT_REGION'),
        'access_key' => env('AWS_ACCESS_KEY_ID'),
        'secret_key' => env('AWS_SECRET_ACCESS_KEY')
    ],

    ...

],

非 Laravel 项目

对于 Laravel 以外的使用,您可以手动创建连接并开始使用 查询构建器 进行查询。

$connection = new Kitar\Dynamodb\Connection([
    'region' => env('AWS_DEFAULT_REGION'),
    'access_key' => env('AWS_ACCESS_KEY_ID'),
    'secret_key' => env('AWS_SECRET_ACCESS_KEY')
]);

$connection->table('your-table')->...

示例数据

本文档中的许多示例代码都在查询 DynamoDB 的官方示例数据。如果您想尝试这些代码,请先将其加载到您的表中。

模型

DynamoDB 模型扩展了 Eloquent 模型,因此我们可以使用熟悉的功能,例如转换器、序列化等。

Eloquent 模型与 DynamoDB 模型的主要区别是

  • Eloquent 模型
    • 可以处理关系。
    • 将调用转发到模型(Eloquent)查询构建器。(例如,createcreateOrFirstwherewith
  • DynamoDB 模型
    • 不能处理关系。
    • 将调用转发到数据库(DynamoDB)查询构建器。(例如,getItemputItemscanfilter

扩展基本模型

大多数属性与原始 Eloquent 模型相同,但有一些 DynamoDB 特定属性。

例如,如果我们的表只有一个分区键,则模型将如下所示

use Kitar\Dynamodb\Model\Model;

class ProductCatalog extends Model
{
    protected $table = 'ProductCatalog';
    protected $primaryKey = 'Id';
    protected $fillable = ['Id', 'Price', 'Title'];
}

如果我们的表还有一个排序键

use Kitar\Dynamodb\Model\Model;

class Thread extends Model
{
    protected $table = 'Thread';
    protected $primaryKey = 'ForumName';
    protected $sortKey = 'Subject';
    protected $fillable = ['ForumName', 'Subject'];
}

如果我们设置了 sortKeyDefault,那么在没有指定排序键的情况下实例化或调用 find 时将使用它。

use Kitar\Dynamodb\Model\Model;
use Illuminate\Auth\Authenticatable;
use Illuminate\Contracts\Auth\Authenticatable as AuthenticatableContract;

class User extends Model implements AuthenticatableContract
{
    use Authenticatable;

    protected $table = 'User';
    protected $primaryKey = 'email';
    protected $sortKey = 'type';
    protected $sortKeyDefault = 'profile';
    protected $fillable = [
        'name', 'email', 'password', 'type',
    ];
}

请注意,此模型实现了 Illuminate\Contracts\Auth\Authenticatable 并使用 Illuminate\Auth\Authenticatable。这是可选的,但如果我们使用它们,我们也可以使用此模型进行身份验证。有关身份验证的详细信息,请参阅身份验证部分

基本用法

检索所有模型

$products = ProductCatalog::scan();

或者,

$products = ProductCatalog::all();

DynamoDB 每次调用只能处理最多 1MB 的结果集,因此如果有更多结果,我们必须进行分页。有关详细信息,请参阅分页结果

检索一个模型

如果模型只有分区键

ProductCatalog::find(101);

如果模型也有排序键

Thread::find([
    'ForumName' => 'Amazon DynamoDB', // Partition key
    'Subject' => 'DynamoDB Thread 1' // Sort key
]);

如果模型有排序键且定义了 sortKeyDefault

User::find('foo@bar.com'); // Partition key. sortKeyDefault will be used for Sort key.

save()

$user = new User([
    'email' => 'foo@bar.com',
    'type' => 'profile' // Sort key. If we don't specify this, sortKeyDefault will be used.
]);

$user->save();
$user->name = 'foo';
$user->save();

update()

$user->update([
    'name' => 'foobar'
]);

delete()

$user->delete();

increment() / decrement()

当我们调用 increment()decrement() 时,底层将使用 原子计数器

$user->increment('views', 1);
$user->decrement('views', 1);

我们还可以传递其他要更新的属性。

$user->increment('views', 1, [
    'last_viewed_at' => '...',
]);

高级查询

我们可以在模型中通过例如 queryscanfilterconditionkeyCondition 等函数使用 Query Builder。

例如

Thread::keyCondition('ForumName', '=', 'Amazon DynamoDB')
        ->keyCondition('Subject', 'begins_with', 'DynamoDB')
        ->filter('Views', '=', 0)
        ->query();

请参阅查询构建器以获取详细信息。

通过模型进行身份验证

我们可以创建一个自定义用户提供程序以使用 DynamoDB 进行身份验证。有关详细信息,请参阅Laravel 的官方文档

要使用模型进行身份验证,模型应实现 Illuminate\Contracts\Auth\Authenticatable 协议。在本节中,我们将使用上面提到的 User 模型。

注册自定义用户提供者

在准备可验证模型后,我们需要创建自定义用户提供程序。我们可以自己创建它(很简单),但本节我们将使用 Kitar\Dynamodb\Model\AuthUserProvider

要注册自定义用户提供程序,请在 App/Providers/AuthServiceProvider.php 中添加以下代码。

use Kitar\Dynamodb\Model\AuthUserProvider;
...
public function boot()
{
    $this->registerPolicies();

    Auth::provider('dynamodb', function ($app, array $config) {
        return new AuthUserProvider(
            $app['hash'],
            $config['model'],
            $config['api_token_name'] ?? null,
            $config['api_token_index'] ?? null
        );
    });
}

更改 auth 配置

然后在 config/auth.php 中指定身份验证的驱动程序和模型名称。

'providers' => [
    // Eloquent
    // 'users' => [
    //     'driver' => 'eloquent',
    //     'model' => App\User::class,
    // ],

    // DynamoDB
    'users' => [
        'driver' => 'dynamodb',
        'model' => App\User::class,
        'api_token_name' => 'api_token',
        'api_token_index' => 'api_token-index'
    ],
],

api_token_nameapi_token_index 是可选的,但如果我们使用 API 令牌身份验证,则需要它们。

注册控制器

可能需要修改注册控制器。例如,如果我们使用 Laravel Breeze,修改如下。

class RegisteredUserController extends Controller
{
    ...

    public function store(Request $request)
    {
        $request->validate([
            'name' => 'required|string|max:255',
            'email' => ['required', 'string', 'email', 'max:255', function ($attribute, $value, $fail) {
                if (User::find($value)) {
                    $fail('The '.$attribute.' has already been taken.');
                }
            }],
            'password' => 'required|string|confirmed|min:8',
        ]);

        $user = new User([
            'name' => $request->name,
            'email' => $request->email,
            'password' => Hash::make($request->password),
        ]);
        $user->save();

        Auth::login($user);

        event(new Registered($user));

        return redirect(RouteServiceProvider::HOME);
    }
}

有两个修改。第一个是添加了对 email 的闭包验证器,而不是 unique 验证器。第二个是使用 save() 方法创建用户,而不是使用 create() 方法。

查询构建器

我们可以不通过模型使用 Query Builder。

$result = DB::table('Thread')->scan();

甚至可以完全在 Laravel 之外使用。

$connection = new Kitar\Dynamodb\Connection([
    'region' => env('AWS_DEFAULT_REGION'),
    'access_key' => env('AWS_ACCESS_KEY_ID'),
    'secret_key' => env('AWS_SECRET_ACCESS_KEY')
]);

$result = $connection->table('Thread')->scan();

如果我们通过模型进行查询,我们不需要指定表名,并且响应将是模型实例(s)。

$threads = Thread::scan();

基本用法

getItem()

$response = DB::table('ProductCatalog')
                ->getItem(['Id' => 101]);

而不是手动打包,传递一个纯数组。Kitar\Dynamodb\Query\Grammar 将在查询之前自动打包它们。

putItem()

DB::table('Thread')
    ->putItem([
        'ForumName' => 'Amazon DynamoDB',
        'Subject' => 'New discussion thread',
        'Message' => 'First post in this thread',
        'LastPostedBy' => 'fred@example.com',
        'LastPostedDateTime' => '201603190422'
    ]);

updateItem()

DB::table('Thread')
    ->key([
        'ForumName' => 'Laravel',
        'Subject' => 'Laravel Thread 1'
    ])->updateItem([
        'LastPostedBy' => null, // REMOVE
        'Replies' => null, // REMOVE
        'Message' => 'Updated' // SET
    ]);

目前,我们仅支持简单的 SETREMOVE 操作。如果属性有值,它将传递到 SET 操作。如果值为 null,它将传递到 REMOVE 操作。

deleteItem()

DB::table('Thread')
    ->deleteItem([
        'ForumName' => 'Amazon DynamoDB',
        'Subject' => 'New discussion thread'
    ]);

投影表达式

投影表达式是一个标识所需属性的字符串。(它类似于 SQL 的 select 语句)

select()

我们可以以与原始 select 子句相同的方式指定投影表达式。

$response = DB::table('ProductCatalog')
                ->select('Price', 'Title')
                ->getItem(['Id' => 101]);

条件表达式

当我们操作 Amazon DynamoDB 表中的数据时,我们使用 putItemupdateItemDeleteItem。我们可以使用条件表达式来确定应修改哪些项目。

condition()

要指定条件表达式,我们使用 condition 子句。这与原始的 where 子句基本相同,但它是为条件表达式而设计的。

DB::table('ProductCatalog')
    ->condition('Id', 'attribute_not_exists')
    ->putItem([
        'Id' => 101,
        'ProductCategory' => 'Can I overwrite?'
    ]);

请注意,我们指定 attribute_not_exists 作为条件的操作符。这是 DynamoDB 特有的操作符,称为 function。有关详细信息,请参阅DynamoDB 的条件() 和 filter() 特定运算符

OR 语句

DB::table('ProductCatalog')
    ->condition('Id', 'attribute_not_exists')
    ->orCondition('Price', 'attribute_not_exists)
    ->putItem([...]);

AND 语句

DB::table('ProductCatalog')
    ->condition('Id', 'attribute_not_exists')
    ->condition('Price', 'attribute_not_exists)
    ->putItem([...]);

conditionIn()

ProductCatalog::key(['Id' => 101])
                ->conditionIn('ProductCategory', ['Book', 'Bicycle'])
                ->updateItem([
                    'Description' => 'updated!'
                ]);

conditionBetween()

ProductCatalog::key(['Id' => 101])
                ->conditionBetween('Price', [0, 10])
                ->updateItem([
                    'Description' => 'updated!'
                ]);

处理查询

Amazon DynamoDB 中的查询操作是根据主键值查找项。

query() 和 keyCondition()

当我们进行 查询 时,必须指定 keyCondition

我们可以使用一些比较运算符来排序键,但我们必须使用等式条件来指定分区键。

$response = DB::table('Thread')
                ->keyCondition('ForumName', '=', 'Amazon DynamoDB')
                ->keyCondition('Subject', 'begins_with', 'DynamoDB')
                ->query();

keyConditionBetween()

$response = DB::table('Thread')
                ->keyCondition('ForumName', '=', 'Amazon DynamoDB')
                ->keyConditionBetween('Subject', ['DynamoDB Thread 1', 'DynamoDB Thread 2'])
                ->query();

处理扫描

scan()

$response = DB::table('Thread')->scan();

过滤结果

当我们进行 查询扫描 时,我们可以在返回之前使用过滤表达式来过滤结果。

这不能减少读取容量,但可以减少流量数据的大小。

filter()

$response = DB::table('Thread')
                ->filter('LastPostedBy', '=', 'User A')
                ->scan();

OR 语句

$response = DB::table('Thread')
                ->filter('LastPostedBy', '=', 'User A')
                ->orFilter('LastPostedBy', '=', 'User B')
                ->scan();

AND 语句

$response = DB::table('Thread')
                ->filter('LastPostedBy', '=', 'User A')
                ->filter('Subject', 'begins_with', 'DynamoDB')
                ->scan();

filterIn()

$response = DB::table('Thread')
                ->filterIn('LastPostedBy', ['User A', 'User B'])
                ->scan();

filterBetween()

$response = DB::table('ProductCatalog')
                ->filterBetween('Price', [0, 100])
                ->scan();

分页结果

单个 查询扫描 只能返回一个大小在 1 MB 限制以内的结果集。如果有更多结果,我们需要分页。

exclusiveStartKey()

如果有更多结果,响应将包含 LastEvaluatedKey

$response = DB::table('ProductCatalog')
                ->limit(5)
                ->scan();

$response['LastEvaluatedKey']; // array

我们可以将此键传递给 exclusiveStartKey 来获取下一页结果。

$response = DB::table('ProductCatalog')
                ->exclusiveStartKey($response['LastEvaluatedKey'])
                ->limit(5)
                ->scan();

如果您通过模型使用 Query Builder,您可以通过以下方式访问到 exclusiveStartKey

$products = ProductCatalog::limit(5)->scan();

$products->first()->meta()['LastEvaluatedKey']; // array

使用全局二级索引

某些应用程序可能需要执行许多不同类型的查询,使用不同的属性作为查询条件。为了支持这些需求,您可以在 Amazon DynamoDB 中创建一个或多个全局二级索引,并对这些索引发出 查询 请求。

index()

使用 index 子句来指定全局二级索引的名称。

$response = DB::table('Reply')
                ->index('PostedBy-Message-index')
                ->keyCondition('PostedBy', '=', 'User A')
                ->keyCondition('Message', '=', 'DynamoDB Thread 2 Reply 1 text')
                ->query();

原子计数器

DynamoDB 支持原子计数器。[链接](https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/WorkingWithItems.html#WorkingWithItems.AtomicCounters)。当我们通过模型或查询构建器调用 increment()decrement() 时,底层将使用原子计数器。

DB::('Thread')->key([
    'ForumName' => 'Laravel',
    'Subject' => 'Laravel Thread 1'
])->increment('Replies', 2);

我们还可以传递其他要更新的属性。

DB::('Thread')->key([
    'ForumName' => 'Laravel',
    'Subject' => 'Laravel Thread 1'
])->increment('Replies', 2, [
    'LastPostedBy' => 'User A',
]);

condition() 和 filter() 的 DynamoDB 特定操作符

对于 conditionfilter 子句,我们可以使用 DynamoDB 的比较运算符和函数。

比较器

可以使用以下形式的 =<><<=>>=

filter($key, $comparator, $value);

函数

可用的函数有

filter($key, 'attribute_exists');
filter($key, 'attribute_not_exists');
filter($key, 'attribute_type', $type);
filter($key, 'begins_with', $value);
filter($key, 'contains', $value);

当前不支持 size 函数。

测试

$ ./vendor/bin/phpunit