kitar / laravel-dynamodb
基于DynamoDB的Eloquent模型和查询构建器,用于Laravel。
Requires
- php: ^7.3|^7.4|^8.0|^8.1|^8.2|^8.3
- aws/aws-sdk-php: ^3.0
- illuminate/container: ^6.0|^7.0|^8.0|^9.0|^10.0|^11.0
- illuminate/database: ^6.0|^7.0|^8.0|^9.0|^10.0|^11.0
- illuminate/hashing: ^6.0|^7.0|^8.0|^9.0|^10.0|^11.0
- illuminate/support: ^6.0|^7.0|^8.0|^9.0|^10.0|^11.0
Requires (Dev)
- illuminate/auth: ^6.0|^7.0|^8.0|^9.0|^10.0|^11.0
- mockery/mockery: ^1.3
- phpunit/phpunit: ^8.0|^9.0|^10.0
- symfony/var-dumper: ^5.0|^6.0|^7.0
- vlucas/phpdotenv: ^4.1|^5.0
README
基于DynamoDB的Eloquent模型和查询构建器,用于Laravel。
您可以在kitar/simplechat中找到示例实现。
- 动机
- 安装
- 示例数据
- 模型
- 使用模型进行身份验证
- 查询构建器
- 调试
- 测试
动机
- 我想使用DynamoDB与Laravel(例如,使用自定义用户提供者进行身份验证)。
- 我想使用一个简单的API,无需担心像手动处理表达式属性等繁琐的事情。
- 我想尽可能多地扩展Laravel的代码,
- 依赖于Laravel的强大代码。
- 保持额外的实现简单且易于维护。
- 我不想使其与Eloquent完全兼容,因为DynamoDB与关系数据库不同。
- 我渴望jessengers/laravel-mongodb。如果我们也为DynamoDB有那个会怎样?
安装
通过Composer安装包
$ composer require kitar/laravel-dynamodb
Laravel (6.x, 7.x, 8.x, 9.x, 10.x, 11.x)
将dynamodb配置添加到 config/database.php
'connections' => [ 'dynamodb' => [ 'driver' => 'dynamodb', 'key' => env('AWS_ACCESS_KEY_ID'), 'secret' => env('AWS_SECRET_ACCESS_KEY'), 'region' => env('AWS_DEFAULT_REGION', 'us-east-1'), 'token' => env('AWS_SESSION_TOKEN', null), 'endpoint' => env('DYNAMODB_ENDPOINT', null), 'prefix' => '', // table prefix ], ... ],
在您的 .env
文件中更新 DB_CONNECTION
变量
DB_CONNECTION=dynamodb
非Laravel项目
对于Laravel之外的使用,您可以手动创建连接并开始使用 查询构建器 进行查询。
$connection = new Kitar\Dynamodb\Connection([ 'key' => env('AWS_ACCESS_KEY_ID'), 'secret' => env('AWS_SECRET_ACCESS_KEY'), 'region' => env('AWS_DEFAULT_REGION', 'us-east-1'), 'token' => env('AWS_SESSION_TOKEN', null), 'endpoint' => env('DYNAMODB_ENDPOINT', null), 'prefix' => '', // table prefix ]); $connection->table('your-table')->...
示例数据
本文档中的许多示例代码都是查询DynamoDB的官方示例数据。如果您想使用实际DynamoDB表尝试这些代码,在尝试之前将其加载到您的表中会更方便。
模型
DynamoDB模型扩展Eloquent模型,这样我们就可以使用熟悉的特性,如设置器、序列化等。
Eloquent模型与DynamoDB模型的主要区别是
- Eloquent模型
- 可以处理关系。
- 将调用转发到模型(Eloquent)查询构建器。(例如,
create
、createOrFirst
、where
、with
)
- DynamoDB模型
- 不能处理关系。
- 将调用转发到数据库(DynamoDB)查询构建器。(例如,
getItem
、putItem
、scan
、filter
)
扩展基本模型
大多数属性与原始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();
您还可以重写scan()
方法以满足您的需求,例如为单表设计过滤模型。例如
public static function scan($exclusiveStartKey = null, $sort = 'asc', $limit = 50) { return static::index('GSI1') ->keyCondition('GSI1PK', '=', 'PRODUCT#') ->keyCondition('GSI1SK', 'begins_with', 'PRODUCT#') ->exclusiveStartKey($exclusiveStartKey) ->scanIndexForward($sort == 'desc' ? false : true) ->limit($limit) ->query(); }
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.
您还可以修改find()
方法的行为以满足您的需求。例如
public static function find($userId) { return parent::find([ 'PK' => str_starts_with($userId, 'USER#') ? $userId : 'USER#'.$userId, 'SK' => 'USER#', ]); }
create()
$user = User::create([ 'email' => 'foo@bar.com', 'type' => 'profile' // Sort key. If we don't specify this, sortKeyDefault will be used. ]);
save()
$user = new User([ 'email' => 'foo@bar.com', 'type' => 'profile' ]); $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' => '...', ]);
高级查询
我们可以通过模型使用查询构建器函数,例如query
scan
filter
condition
keyCondition
等。
例如
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 ); }); }
更改身份验证配置
然后在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_name
和api_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()
方法来创建用户。
查询构建器
我们可以不使用模型使用查询构建器。
$result = DB::table('Thread')->scan();
甚至可以完全在Laravel之外。
$connection = new Kitar\Dynamodb\Connection([ 'key' => env('AWS_ACCESS_KEY_ID'), 'secret' => env('AWS_SECRET_ACCESS_KEY'), 'region' => env('AWS_DEFAULT_REGION', 'us-east-1'), 'token' => env('AWS_SESSION_TOKEN', null), 'endpoint' => env('DYNAMODB_ENDPOINT', null), 'prefix' => '', // table prefix ]); $result = $connection->table('Thread')->scan();
如果我们通过模型查询,我们不需要指定表名,响应将是模型实例。
$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 ]);
目前,我们只支持简单的SET
和REMOVE
操作。如果属性有值,它将被传递给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表中的数据时,我们使用putItem
、updateItem
和DeleteItem
。我们可以使用条件表达式来决定哪些项目应该被修改。
condition()
要指定条件表达式,我们使用condition
子句。这与原始的where
子句基本相同,但它用于条件表达式。
DB::table('ProductCatalog') ->condition('Id', 'attribute_not_exists') ->putItem([ 'Id' => 101, 'ProductCategory' => 'Can I overwrite?' ]);
请注意,我们指定
attribute_not_exists
作为条件运算符。这是一个DynamoDB特定的运算符,称为函数
。有关详细信息,请参阅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()
当我们进行query
时,我们必须指定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();
排序顺序
query
的结果总是按排序键值排序。要反转顺序,将ScanIndexForward
参数设置为false
。
$response = DB::table('Thread') ->keyCondition('ForumName', '=', 'Amazon DynamoDB') ->scanIndexForward(false) ->query();
请注意,DynamoDB的
ScanIndexForward
是针对query
的功能。它不会与scan
一起工作。
处理扫描
scan()
$response = DB::table('Thread')->scan();
过滤结果
当我们进行query
或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();
分页结果
单个query
或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->getLastEvaluatedKey(); // array
或者,您可以使用单个模型达到相同的结果;但是,请注意,此方法计划在v2.x版本之后废弃。
$products->first()->meta()['LastEvaluatedKey']; // array
使用全局二级索引
某些应用程序可能需要执行多种类型的查询,使用不同的属性作为查询条件。为了满足这些需求,您可以在Amazon DynamoDB中创建一个或多个全局二级索引,并对这些索引发出query
请求。
index()
使用index
子句来指定全局二级索引的名称。
$response = DB::table('Reply') ->index('PostedBy-Message-index') ->keyCondition('PostedBy', '=', 'User A') ->keyCondition('Message', '=', 'DynamoDB Thread 2 Reply 1 text') ->query();
原子计数器
DynamoDB支持原子计数器。当我们通过模型或查询构建器调用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', ]);
批处理操作
批量操作可以在单个调用中获取、放入或删除多个项目。存在一些DynamoDB限制(如项目数量、有效负载大小等),请在提前查看文档。(BatchGetItem,BatchWriteItem)
batchGetItem()
DB::table('Thread') ->batchGetItem([ [ 'ForumName' => 'Amazon DynamoDB', 'Subject' => 'DynamoDB Thread 1' ], [ 'ForumName' => 'Amazon DynamoDB', 'Subject' => 'DynamoDB Thread 2' ] ]);
batchPutItem()
DB::table('Thread') ->batchPutItem([ [ 'ForumName' => 'Amazon DynamoDB', 'Subject' => 'DynamoDB Thread 3' ], [ 'ForumName' => 'Amazon DynamoDB', 'Subject' => 'DynamoDB Thread 4' ] ]);
这是一个使用
batchWriteItem
批量放入项目的便捷方法。
batchDeleteItem()
DB::table('Thread') ->batchDeleteItem([ [ 'ForumName' => 'Amazon DynamoDB', 'Subject' => 'DynamoDB Thread 1' ], [ 'ForumName' => 'Amazon DynamoDB', 'Subject' => 'DynamoDB Thread 2' ] ]);
这是一个使用
batchWriteItem
批量删除项目的便捷方法。
batchWriteItem()
DB::table('Thread') ->batchWriteItem([ [ 'PutRequest' => [ 'Item' => [ 'ForumName' => 'Amazon DynamoDB', 'Subject' => 'DynamoDB Thread 3' ] ] ], [ 'DeleteRequest' => [ 'Key' => [ 'ForumName' => 'Amazon DynamoDB', 'Subject' => 'DynamoDB Thread 1' ] ] ] ]);
condition() 和 filter() 的DynamoDB特定运算符
对于condition
和filter
子句,我们可以使用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
函数目前不支持。
调试
dryRun()
我们可以通过向查询添加dryRun()
来检查实际上将发送给DynamoDB的参数(以及哪个方法)。例如:
// via Model $request = ProductCatalog::dryRun()->getItem(['Id' => 101]); // via Query Builder $request = DB::table('ProductCatalog')->dryRun()->getItem(['Id' => 101]); dump($request);
我们的PHPUnit测试也使用这个特性,实际上不调用DynamoDB。
测试
$ ./vendor/bin/phpunit