Sejauh ini kita banyak membaca lewat relasi; sekarang saatnya menulis lewat relasi. Eloquent menyediakan method operasi relasi yang membebaskan kita dari pengisian foreign key manual: save() dan create() untuk menambah anak pada relasi hasMany, associate() untuk menautkan induk pada belongsTo, serta keluarga attach/detach/sync untuk pivot table. Setiap method menempatkan nilai relasi pada tempatnya secara otomatis — dan beberapa di antaranya mengembalikan laporan yang layak dibaca. Semua contoh berikut dijalankan berurutan pada data yang sama, mulai dari post 3 yang belum punya komentar.

save() dan create() #

Method save() pada relasi menerima model yang sudah dibuat, mengisi foreign key-nya, lalu menyimpannya:

php
use App\Models\Comment;
use App\Models\Post;

$post = Post::find(3); // post tanpa komentar di data awal

$comment = new Comment(['isi' => 'Baru belajar, izin bertanya.', 'user_id' => 2]);
$post->comments()->save($comment);

printf("komentar #%d tersimpan | post_id=%d | user_id=%d\n",
    $comment->id, $comment->post_id, $comment->user_id);

Output yang Dihasilkan:

text
komentar #10 tersimpan | post_id=3 | user_id=2

Setelah save(), kolom post_id pada model komentar terisi otomatis dari induknya — kita tidak pernah menulis post_id secara manual. Versi create() menggabungkan pembuatan model dan penyimpanan dalam satu panggilan:

php
$comment = $post->comments()->create(['isi' => 'Sudah coba, sukses!', 'user_id' => 3]);

printf("komentar #%d menempel di post %d\n", $comment->id, $comment->post_id);

Output yang Dihasilkan:

text
komentar #11 menempel di post 3

Perlu dicatat bahwa save() hanya mengurus foreign key milik relasi yang memanggilnya — kolom user_id pada contoh di atas tetap kita isi sendiri karena komentar dibuat lewat relasi comments, bukan lewat relasi user. Saudara lainnya, saveMany(), menerima array model sekaligus.

createMany() #

Untuk menyisipkan banyak baris berurutan, createMany() menerima array atribut:

php
$comments = $post->comments()->createMany([
    ['isi' => 'Komentar batch pertama.', 'user_id' => 1],
    ['isi' => 'Komentar batch kedua.', 'user_id' => 2],
]);

foreach ($comments as $comment) {
    printf("komentar #%d: %s\n", $comment->id, $comment->isi);
}
echo 'komentar post 3 sekarang: ', $post->comments()->count(), "\n";

Output yang Dihasilkan:

text
komentar #12: Komentar batch pertama.
komentar #13: Komentar batch kedua.
komentar post 3 sekarang: 4

Method ini mengembalikan koleksi model yang baru dibuat, masing-masing sudah membawa post_id yang benar. Di belakang layar Eloquent memanggil create() berulang kali, sehingga event model tetap menyala untuk setiap baris.

associate() dan dissociate() #

Untuk relasi belongsTo, penautan induk dilakukan dari sisi anak. Komentar baru dibuat, lalu post dan penulisnya ditautkan sebelum disimpan:

php
$budi = User::find(1);

$comment = new Comment(['isi' => 'Komentar dari relasi belongsTo.']);
$comment->post()->associate($post);
$comment->user()->associate($budi);
$comment->save();

printf("komentar #%d oleh user %d di post %d\n",
    $comment->id, $comment->user_id, $comment->post_id);

Output yang Dihasilkan:

text
komentar #14 oleh user 1 di post 3

Dua panggilan associate() mengisi post_id dan user_id tanpa menyentuh database — nilai ditulis ke atribut model, dan save() terjadi sekali di akhir. Kebalikannya, dissociate() mengosongkan foreign key di memori:

php
$comment->post()->dissociate();
echo 'post_id setelah dissociate: ', var_export($comment->post_id, true), "\n";

try {
    $comment->save();
} catch (\Illuminate\Database\QueryException $e) {
    echo 'save() gagal: ', substr($e->getMessage(), 0, 66), "\n";
}

Output yang Dihasilkan:

text
post_id setelah dissociate: NULL
save() gagal: SQLSTATE[23000]: Integrity constraint violation: 19 NOT NULL const

attach(), detach(), dan sync() pada belongsToMany #

Operasi pivot berjalan dari sisi relasi many-to-many. Kita pakai user Siti yang di data awal memegang role editor:

php
$siti = User::find(2);
$editor = Role::find(1);
$penulis = Role::find(2);

$siti->roles()->attach($penulis, ['added_by' => 3]);
echo 'setelah attach: ', $siti->fresh()->roles()->orderBy('roles.id')->pluck('name')->implode(', '), "\n";

$siti->roles()->detach($editor);
echo 'setelah detach: ', $siti->fresh()->roles()->pluck('name')->implode(', '), "\n";

$changes = $siti->roles()->sync([2 => ['added_by' => 3], 3 => ['added_by' => 1]]);
echo 'sync: ', json_encode($changes), "\n";
echo 'hasil akhir: ', $siti->fresh()->roles()->orderBy('roles.id')->pluck('name')->implode(', '), "\n";

Output yang Dihasilkan:

text
setelah attach: editor, penulis
setelah detach: penulis
sync: {"attached":[3],"detached":[],"updated":[]}
hasil akhir: penulis, admin

attach() menambah role penulis, detach() melepaskan editor, lalu sync() menyelaraskan sisa daftar: admin disisipkan dan added_by penulis diperbarui. Penjelasan lengkap kolom pivot beserta laporan attached/detached/updated sudah kita bedah di chapter Pivot Table .

push(): Menyimpan Model Beserta Relasinya #

Kadang satu operasi harus menyimpan banyak sekaligus: revisi isi post sekaligus beberapa komentar barunya. Method push() menyimpan model dan seluruh relasi yang termuat di property relations-nya:

php
$post = Post::find(5);
$post->isi = 'Isi post kelima direvisi hari ini.';

$post->setRelation('comments', $post->comments()->makeMany([
    ['isi' => 'Komentar hasil makeMany pertama.', 'user_id' => 2],
    ['isi' => 'Komentar hasil makeMany kedua.', 'user_id' => 3],
]));

echo 'push(): ', var_export($post->push(), true), "\n";
echo 'isi terbaru di DB: ', $post->fresh()->isi, "\n";

foreach (Comment::where('post_id', 5)->orderByDesc('id')->get() as $comment) {
    echo 'komentar #', $comment->id, ' (post_id=', $comment->post_id, '): ', $comment->isi, "\n";
}

Output yang Dihasilkan:

text
push(): true
isi terbaru di DB: Isi post kelima direvisi hari ini.
komentar #16 (post_id=5): Komentar hasil makeMany kedua.
komentar #15 (post_id=5): Komentar hasil makeMany pertama.
komentar #9 (post_id=5): Nanti saya praktikkan ulang.

Revisi isi post dan dua komentar baru tersimpan dari satu panggilan. Kunci pola ini ada di makeMany(): method tersebut membuat model komentar tanpa menyimpannya tetapi sudah mengisi post_id dari post yang sudah ada — dan push() menyimpan anak apa adanya tanpa mengisi foreign key. Anak yang foreign key-nya belum terisi akan gagal constraint NOT NULL saat di-push.

Sebagai rangkuman, tabel berikut memetakan operasi ke jenis relasi yang tepat:

OperasiJenis relasiYang terjadi
$post->comments()->save($c)hasMany / hasOneisi foreign key anak lalu simpan
$post->comments()->create([...])hasMany / hasOnebuat model baru + isi foreign key + simpan
$comment->post()->associate($p)belongsToisi foreign key di memori tanpa query
$user->roles()->attach($r)belongsToManysisipkan baris pivot
$user->roles()->sync([...])belongsToManyselaraskan pivot sesuai daftar
$post->push()semuasimpan model + relasi termuat sekaligus

Dengan rangkaian operasi ini, siklus hidup data lewat relasi sudah lengkap dari menautkan, menyimpan, sampai melepas. Bab berikutnya menutup section dengan topik yang menyatukan semuanya: mendeteksi dan mencegah masalah N+1 query — penyakit yang justru paling mudah lahir dari operasi-operasi nyaman di atas.