staudenmeir/eloquent-json-relations

Laravel Eloquent JSON键关系的扩展

v1.13 2024-07-21 12:43 UTC

README

CI Code Coverage Latest Stable Version Total Downloads License

这个Laravel Eloquent扩展增加了对BelongsToHasOneHasManyHasOneThroughHasManyThroughMorphToMorphOneMorphMany关系的JSON外键支持。

它还提供了使用JSON数组实现的多对多多对多通过关系。

兼容性

  • MySQL 5.7+
  • MariaDB 10.2+
  • PostgreSQL 9.3+
  • SQLite 3.38+
  • SQL Server 2016+

安装

composer require "staudenmeir/eloquent-json-relations:^1.1"

如果你在Windows的PowerShell中(例如在VS Code中),请使用此命令

composer require "staudenmeir/eloquent-json-relations:^^^^1.1"

版本

用法

一对一关系

在这个例子中,UserLocaleBelongsTo关系。没有专门的列,但外键(locale_id)作为属性存储在JSON字段(users.options)中。

class User extends Model
{
    use \Staudenmeir\EloquentJsonRelations\HasJsonRelationships;

    protected $casts = [
        'options' => 'json',
    ];

    public function locale()
    {
        return $this->belongsTo(Locale::class, 'options->locale_id');
    }
}

class Locale extends Model
{
    use \Staudenmeir\EloquentJsonRelations\HasJsonRelationships;

    public function users()
    {
        return $this->hasMany(User::class, 'options->locale_id');
    }
}

请记住在父模型和相关模型中都使用HasJsonRelationships特性。

引用完整性

MySQLMariaDBSQL Server上,您仍然可以通过生成/计算列上的外键来确保引用完整性。

Laravel迁移在MySQL/MariaDB上支持此功能

Schema::create('users', function (Blueprint $table) {
    $table->bigIncrements('id');
    $table->json('options');
    $locale_id = DB::connection()->getQueryGrammar()->wrap('options->locale_id');
    $table->unsignedBigInteger('locale_id')->storedAs($locale_id);
    $table->foreign('locale_id')->references('id')->on('locales');
});

Laravel迁移也支持SQL Server上的此功能

Schema::create('users', function (Blueprint $table) {
    $table->bigIncrements('id');
    $table->json('options');
    $locale_id = DB::connection()->getQueryGrammar()->wrap('options->locale_id');
    $locale_id = 'CAST('.$locale_id.' AS INT)';
    $table->computed('locale_id', $locale_id)->persisted();
    $table->foreign('locale_id')->references('id')->on('locales');
});

多对多关系

该包还引入了两种新的关系类型:BelongsToJsonHasManyJson

使用它们实现与JSON数组的多对多关系。

在这个例子中,UserRoleBelongsToMany关系。没有中间表,但外键作为数组存储在JSON字段(users.options)中。

ID数组

默认情况下,关系将中继记录存储为ID数组

class User extends Model
{
    use \Staudenmeir\EloquentJsonRelations\HasJsonRelationships;

    protected $casts = [
       'options' => 'json',
    ];
    
    public function roles(): \Staudenmeir\EloquentJsonRelations\Relations\BelongsToJson
    {
        return $this->belongsToJson(Role::class, 'options->role_ids');
    }
}

class Role extends Model
{
    use \Staudenmeir\EloquentJsonRelations\HasJsonRelationships;

    public function users(): \Staudenmeir\EloquentJsonRelations\Relations\HasManyJson
    {
        return $this->hasManyJson(User::class, 'options->role_ids');
    }
}

BelongsToJson关系的一侧,您可以使用attach()detach()sync()toggle()

$user = new User;
$user->roles()->attach([1, 2])->save(); // Now: [1, 2]

$user->roles()->detach([2])->save();    // Now: [1]

$user->roles()->sync([1, 3])->save();   // Now: [1, 3]

$user->roles()->toggle([2, 3])->save(); // Now: [1, 2]

对象数组

您也可以将中继记录作为具有附加属性的对象存储

class User extends Model
{
    use \Staudenmeir\EloquentJsonRelations\HasJsonRelationships;

    protected $casts = [
       'options' => 'json',
    ];
    
    public function roles(): \Staudenmeir\EloquentJsonRelations\Relations\BelongsToJson
    {
        return $this->belongsToJson(Role::class, 'options->roles[]->role_id');
    }
}

class Role extends Model
{
    use \Staudenmeir\EloquentJsonRelations\HasJsonRelationships;

    public function users(): \Staudenmeir\EloquentJsonRelations\Relations\HasManyJson
    {
        return $this->hasManyJson(User::class, 'options->roles[]->role_id');
    }
}

在这里,options->roles是JSON数组的路径。role_id是记录对象中外键属性的名称

$user = new User;
$user->roles()->attach([1 => ['active' => true], 2 => ['active' => false]])->save();
// Now: [{"role_id":1,"active":true},{"role_id":2,"active":false}]

$user->roles()->detach([2])->save();
// Now: [{"role_id":1,"active":true}]

$user->roles()->sync([1 => ['active' => false], 3 => ['active' => true]])->save();
// Now: [{"role_id":1,"active":false},{"role_id":3,"active":true}]

$user->roles()->toggle([2 => ['active' => true], 3])->save();
// Now: [{"role_id":1,"active":false},{"role_id":2,"active":true}]

限制:在SQLite和SQL Server上,这些关系只能部分工作。

HasOneJson

如果您只想检索单个相关实例,请定义HasOneJson关系

class Role extends Model
{
use \Staudenmeir\EloquentJsonRelations\HasJsonRelationships;

    public function latestUser(): \Staudenmeir\EloquentJsonRelations\Relations\HasOneJson
    {
        return $this->hasOneJson(User::class, 'options->roles[]->role_id')
            ->latest();
    }
}

复合键

如果需要多个列匹配,您可以定义复合键

传递以JSON键开始的键的数组

class Employee extends Model
{
    public function tasks(): \Staudenmeir\EloquentJsonRelations\Relations\BelongsToJson
    {
        return $this->belongsToJson(
            Task::class,
            ['options->work_stream_ids', 'team_id'],
            ['work_stream_id', 'team_id']
        );
    }
}

class Task extends Model
{
    public function employees(): \Staudenmeir\EloquentJsonRelations\Relations\HasManyJson
    {
        return $this->hasManyJson(
            Employee::class,
            ['options->work_stream_ids', 'team_id'],
            ['work_stream_id', 'team_id']
        );
    }
}

查询性能

MySQL

在MySQL 8.0.17+上,您可以通过多值索引来提高查询性能。

在数组是列本身时(例如,users.role_ids)使用此迁移

Schema::create('users', function (Blueprint $table) {
    // ...
    
    // Array of IDs
    $table->rawIndex('(cast(`role_ids` as unsigned array))', 'users_role_ids_index');
    
    // Array of objects
    $table->rawIndex('(cast(`roles`->\'$[*]."role_id"\' as unsigned array))', 'users_roles_index');
});

在数组嵌套在对象内部时(例如,users.options->role_ids)使用此迁移

Schema::create('users', function (Blueprint $table) {
    // ...
    
    // Array of IDs
    $table->rawIndex('(cast(`options`->\'$."role_ids"\' as unsigned array))', 'users_role_ids_index');
    
    // Array of objects
    $table->rawIndex('(cast(`options`->\'$."roles"[*]."role_id"\' as unsigned array))', 'users_roles_index');
});

MySQL 对语法非常挑剔,因此我建议您使用 EXPLAIN 检查一下执行的关系查询实际上是否使用了索引。

PostgreSQL

在 PostgreSQL 中,您可以使用 jsonb 列和 GIN 索引来提高查询性能。

当 ID/对象的数组是列本身时(例如 users.role_ids),使用此迁移。

Schema::create('users', function (Blueprint $table) {
    $table->id();
    $table->jsonb('role_ids');
    $table->index('role_ids')->algorithm('gin');
});

在数组嵌套在对象内部时(例如,users.options->role_ids)使用此迁移

Schema::create('users', function (Blueprint $table) {
    $table->id();
    $table->jsonb('options');
    $table->rawIndex('("options"->\'role_ids\')', 'users_options_index')->algorithm('gin');
});

Has-Many-Through 关系

类似于 Laravel 的 HasManyThrough,当 JSON 列在中间表(Laravel 9+)中时,您可以定义 HasManyThroughJson 关系。这需要 staudenmeir/eloquent-has-many-deep

考虑 RoleProject 通过 User 之间的关系

Role → 有多个 JSON → User → 有多个 Project

安装附加包,将 HasRelationships 特性添加到父(第一个)模型,并将 JSON 列作为 JsonKey 对象传递

class Role extends Model
{
    use \Staudenmeir\EloquentHasManyDeep\HasRelationships;

    public function projects()
    {
        return $this->hasManyThroughJson(
            Project::class,
            User::class,
            new \Staudenmeir\EloquentJsonRelations\JsonKey('options->role_ids')
        );
    }
}

反向关系看起来像这样

class Project extends Model
{
    use \Staudenmeir\EloquentHasManyDeep\HasRelationships;

    public function roles()
    {
        return $this->hasManyThroughJson(
            Role::class, User::class, 'id', 'id', 'user_id', new JsonKey('options->role_ids')
        );
    }
}

深度关系连接

您可以使用 staudenmeir/eloquent-has-many-deep(Laravel 9+)将 JSON 关系包括到深层关系中,通过将它们与其他关系连接起来。

考虑 UserPermission 通过 Role 之间的关系

User → 属于 JSON → Role → 有多个 → Permission

安装附加包,将 HasRelationships 特性添加到父(第一个)模型,并 定义一个深层关系

class User extends Model
{
    use \Staudenmeir\EloquentHasManyDeep\HasRelationships;
    use \Staudenmeir\EloquentJsonRelations\HasJsonRelationships;

    public function permissions(): \Staudenmeir\EloquentHasManyDeep\HasManyDeep
    {
        return $this->hasManyDeepFromRelations(
            $this->roles(),
            (new Role)->permissions()
        );
    }
    
    public function roles(): \Staudenmeir\EloquentJsonRelations\Relations\BelongsToJson
    {
        return $this->belongsToJson(Role::class, 'options->role_ids');
    }
}

class Role extends Model
{
    public function permissions()
    {
        return $this->hasMany(Permission::class);
    }
}

$permissions = User::find($id)->permissions;

贡献

请参阅 CONTRIBUTINGCODE OF CONDUCT 以获取详细信息。