Eloquentのリレーション活用方法【紐付けの設定と解除】

Eloquentのリレーションメソッドを活用して、紐付けの設定や解除を行う方法について解説します。
複数の手続きをまとめて行うメソッドが用意されているので、コードの可読性保守性を高めることができます。

実際に実行されるSQLを載せています。どういった動作が行われるのか理解するのに役立つと思います。

なお、Eloquentのリレーションについて以下3回に渡って解説しています。他の記事も参考にしてください。

動作確認に利用したリポジトリです。
https://github.com/raku-raku/laravel_eloquent_practice

紐づく子モデルを設定

save, saveMany
( Eloquentモデルで設定 )

save

$comment = new Comment(['body' => 'A new comment.']);
$post = Post::find(1);
$post->comments()->save($comment);
insert into `comments`
(`body`, `commentable_id`, `commentable_type`, `updated_at`, `created_at`)
values
('A new comment.', '1', 'App\Models\Post', '2018-11-17 13:05:41', '2018-11-17 13:05:41')

saveMany

$post = Post::find(1);
$post->comments()->saveMany([
    new Comment(['body' => 'A new comment.']),
    new Comment(['body' => 'Another comment.']),
]);
insert into `comments`
(`body`, `commentable_id`, `commentable_type`, `updated_at`, `created_at`)
values
('A new comment.', '1', 'App\Models\Post', '2018-11-17 13:07:28', '2018-11-17 13:07:28')

insert into `comments`
(`body`, `commentable_id`, `commentable_type`, `updated_at`, `created_at`)
values
('Another comment.', '1', 'App\Models\Post', '2018-11-17 13:07:28', '2018-11-17 13:07:28')

create, createMany
( 配列で設定 )

create

$comment = ['body' => 'A new comment.'];
$post = Post::find(1);
$post->comments()->create($comment);
insert into `comments` 
(`body`, `commentable_id`, `commentable_type`, `updated_at`, `created_at`) 
values 
('A new comment.', '1', 'App\Models\Post', '2018-11-17 13:12:32', '2018-11-17 13:12:32')

createMany

$post = Post::find(1);
$post->comments()->createMany([
    ['body' => 'A new comment.'],
    ['body' => 'Another comment.'],
]);
insert into `comments`
(`body`, `commentable_id`, `commentable_type`, `updated_at`, `created_at`)
values
('A new comment.', '1', 'App\Models\Post', '2018-11-17 13:10:52', '2018-11-17 13:10:52')

insert into `comments`
(`body`, `commentable_id`, `commentable_type`, `updated_at`, `created_at`)
values
('Another comment.', '1', 'App\Models\Post', '2018-11-17 13:10:52', '2018-11-17 13:10:52')

push
( 再帰的に設定 )

$country = Country::find(2);
$country->users[0]->name = 'user name';
$country->users[0]->posts[0]->title = 'post title';
$country->push();
select * from `countries` where `countries`.`id` = '2' limit 1

select * from `users` where `users`.`country_id` = '2' and `users`.`country_id` is not null

select * from `posts` where `posts`.`user_id` = '1' and `posts`.`user_id` is not null

update `users` set `name` = 'user name', `updated_at` = '2018-11-17 13:18:17' where `id` = '1'

update `posts` set `title` = 'post title', `updated_at` = '2018-11-17 13:18:17' where `id` = '5'

紐づく親モデルを設定

associate
( 紐付け設定 )

$user = User::find(1);
$post = Post::find(1);
$post->user()->associate($user);
$post->save();
update `posts` 
set `user_id` = '1', `updated_at` = '2018-11-17 13:26:16' 
where `id` = '1'

dissociate
( 紐付け解除 )

外部キーが nullable の場合、以下のように紐付けを解除することができます。

$phone = Phone::find(1);
$phone->user()->dissociate();
$phone->save();
update `phones`
set `user_id` = '', `updated_at` = '2018-11-17 13:35:09'
where `id` = '1'

多対多関係の紐付け

目的に応じた利用可能メソッド

多対多関係では、目的に応じて以下メソッドが用意されています。

メソッド insert update delete
attach × ×
detach × ×
updateExistingPivot × ×
sync
syncWithoutDetaching ×
toggle ×

insert, update, deleteは中間テーブルへの操作になります。

attach
( 紐付け追加 )

$roleId1 = 1;
$roleId2 = 2;
$user1 = User::find(1);
$user1->roles()->attach($roleId1);
$user1->roles()->attach($roleId2, ['column1' => 'xxxxx']);
insert into `role_user`
(`created_at`, `role_id`, `updated_at`, `user_id`)
values
('2018-11-17 13:42:41', '1', '2018-11-17 13:42:41', '1')

insert into `role_user`
(`column1`, `created_at`, `role_id`, `updated_at`, `user_id`)
values
('xxxxx', '2018-11-17 13:42:41', '2', '2018-11-17 13:42:41', '1')

以下のように、まとめて追加することもできます。

$roleId1 = 1;
$roleId2 = 2;
$user2 = User::find(2);
$user2->roles()->attach([
    $roleId1 => ['column1' => 'xxxxx'],
    $roleId2 => ['column1' => 'yyyyy'],
]);
insert into `role_user` 
(`column1`, `created_at`, `role_id`, `updated_at`, `user_id`) 
values 
('xxxxx', '2018-11-17 13:50:30', '1', '2018-11-17 13:50:30', '2'), 
('yyyyy', '2018-11-17 13:50:30', '2', '2018-11-17 13:50:30', '2')

detach
( 紐付け解除 )

引数を指定しない場合、全ての紐付けが解除されます。

$user1 = User::find(1);
$user1->roles()->detach(1);

$user2 = User::find(2);
$user2->roles()->detach();
delete from `role_user` where `user_id` = '1' and `role_id` in ('1')

delete from `role_user` where `user_id` = '2'

updateExistingPivot
( 中間テーブルの値更新 )

updateExistingPivotメソッド を利用すると中間テーブルに存在するカラムの値を更新できます。

$roleId = 1;
$user = User::find(1);
$user->roles()->updateExistingPivot($roleId, ['column1' => 'xxxxx']);
update `role_user` 
set `column1` = 'xxxxx', 
    `updated_at` = '2018-11-17 16:14:11' 
where `user_id` = '1' 
and `role_id` in ('1')

sync
( 同期 )

syncメソッド を利用すると紐付け状態を 同期 できます。

つまり、現在の紐付け状態に応じて、

  • 紐付けの追加
  • 中間テーブルの値更新
  • 紐付けの解除

をまとめて行います。

動作確認していきます。

現在の紐付け状態

mysql> SELECT * FROM `role_user` WHERE `user_id` = 1;
+---------+---------+------------------------------------------------+---------------------------------------------------+---------------------+---------------------+
| user_id | role_id | column1                                        | column2                                           | created_at          | updated_at          |
+---------+---------+------------------------------------------------+---------------------------------------------------+---------------------+---------------------+
|       1 |       1 | Queen will hear you! You see, she came upon a. | Cat said, waving its right ear and left off when. | 2018-11-17 14:22:12 | 2018-11-17 14:22:12 |
|       1 |       2 | Queen will hear you! You see, she came upon a. | Cat said, waving its right ear and left off when. | 2018-11-17 14:22:12 | 2018-11-17 14:22:12 |
+---------+---------+------------------------------------------------+---------------------------------------------------+---------------------+---------------------+
2 rows in set (0.00 sec)

Userが 2つのRole と紐づいている状態です。

syncメソッド実行

$user = User::find(1);
$user->roles()->sync([2 => ['column1' => 'xxxxx'], 3]);
delete from `role_user`
where `user_id` = '1'
and `role_id` in ('1')


update `role_user`
set `column1` = 'xxxxx',
    `updated_at` = '2018-11-17 14:29:29'
where `user_id` = '1'
and `role_id` in ('2')


insert into `role_user`
(`created_at`, `role_id`, `updated_at`, `user_id`)
values
('2018-11-17 14:29:29', '3', '2018-11-17 14:29:29', '1')
role_id 処理
1 紐付け解除されました
2 中間テーブルの値が更新されました
3 紐付け追加されました

syncメソッド実行後の紐付け状態

mysql> SELECT * FROM `role_user` WHERE `user_id` = 1;
+---------+---------+---------+---------------------------------------------------+---------------------+---------------------+
| user_id | role_id | column1 | column2                                           | created_at          | updated_at          |
+---------+---------+---------+---------------------------------------------------+---------------------+---------------------+
|       1 |       2 | xxxxx   | Cat said, waving its right ear and left off when. | 2018-11-17 14:22:12 | 2018-11-17 14:29:29 |
|       1 |       3 | NULL    | NULL                                              | 2018-11-17 14:29:29 | 2018-11-17 14:29:29 |
+---------+---------+---------+---------------------------------------------------+---------------------+---------------------+
2 rows in set (0.00 sec)

syncWithoutDetaching
( 同期|削除はしない )

syncメソッド では 紐付け解除 も行われてしまいます。
紐付け解除したくないのであれば、 syncWithoutDetachingメソッド を利用します。

現在の紐付け状態

mysql> SELECT * FROM `role_user` WHERE `user_id` = 1;
+---------+---------+---------------------------------------------------+------------------------------------------------+---------------------+---------------------+
| user_id | role_id | column1                                           | column2                                        | created_at          | updated_at          |
+---------+---------+---------------------------------------------------+------------------------------------------------+---------------------+---------------------+
|       1 |       1 | I believe.' 'Boots and shoes under the hedge. In. | Alice did not venture to ask his neighbour to. | 2018-11-17 14:43:26 | 2018-11-17 14:43:26 |
|       1 |       2 | I believe.' 'Boots and shoes under the hedge. In. | Alice did not venture to ask his neighbour to. | 2018-11-17 14:43:26 | 2018-11-17 14:43:26 |
+---------+---------+---------------------------------------------------+------------------------------------------------+---------------------+---------------------+
2 rows in set (0.07 sec)

syncWithoutDetachingメソッド実行

$user = User::find(1);
$user->roles()->syncWithoutDetaching([2 => ['column1' => 'xxxxx'], 3]);
update `role_user`
set `column1` = 'xxxxx',
    `updated_at` = '2018-11-17 15:29:45'
where `user_id` = '1' and `role_id` in ('2')


insert into `role_user`
(`created_at`, `role_id`, `updated_at`, `user_id`)
alues
('2018-11-17 15:29:45', '3', '2018-11-17 15:29:45', '1')

syncWithoutDetachingメソッド実行後の紐付け状態

mysql> SELECT * FROM `role_user` WHERE `user_id` = 1;
+---------+---------+--------------------------------------------+-----------------------------------------------+---------------------+---------------------+
| user_id | role_id | column1                                    | column2                                       | created_at          | updated_at          |
+---------+---------+--------------------------------------------+-----------------------------------------------+---------------------+---------------------+
|       1 |       3 | Alice looked round, eager to see the Mock. | I am very tired of sitting by her sister was. | 2018-11-17 16:00:27 | 2018-11-17 16:00:27 |
+---------+---------+--------------------------------------------+-----------------------------------------------+---------------------+---------------------+
1 row in set (0.01 sec)

toggle
( 紐付け状態切り替え )

現在の紐付け状態に応じて、動作が変わります。

指定された紐付け対象が、 紐づいている状態 であれば 紐付け解除 します。
指定された紐付け対象が、 紐づいていない状態 であれば 紐付け します。

現在の紐付け状態

mysql> SELECT * FROM `role_user` WHERE `user_id` = 1;
+---------+---------+----------------------------------------------+---------------------------------------------------+---------------------+---------------------+
| user_id | role_id | column1                                      | column2                                           | created_at          | updated_at          |
+---------+---------+----------------------------------------------+---------------------------------------------------+---------------------+---------------------+
|       1 |       3 | Dodo. Then they both cried. 'Wake up, Alice. | There was not going to begin lessons: you d only. | 2018-11-17 15:56:39 | 2018-11-17 15:56:39 |
+---------+---------+----------------------------------------------+---------------------------------------------------+---------------------+---------------------+
1 row in set (0.00 sec)

toggleメソッド実行

$user = User::find(1);
$user->roles()->toggle([1, 2, 3]);
delete from `role_user` 
where `user_id` = '1' 
and `role_id` in ('3')

insert into `role_user` 
(`created_at`, `role_id`, `updated_at`, `user_id`) 
values 
('2018-11-17 16:01:00', '1', '2018-11-17 16:01:00', '1'), 
('2018-11-17 16:01:00', '2', '2018-11-17 16:01:00', '1')

toggleメソッド実行後の紐付け状態

mysql> SELECT * FROM `role_user` WHERE `user_id` = 1;
+---------+---------+---------+---------+---------------------+---------------------+
| user_id | role_id | column1 | column2 | created_at          | updated_at          |
+---------+---------+---------+---------+---------------------+---------------------+
|       1 |       1 | NULL    | NULL    | 2018-11-17 16:01:00 | 2018-11-17 16:01:00 |
|       1 |       2 | NULL    | NULL    | 2018-11-17 16:01:00 | 2018-11-17 16:01:00 |
+---------+---------+---------+---------+---------------------+---------------------+
2 rows in set (0.01 sec)