Mempelajari Active Record Yii: model dengan attributes dan aturan validasi, query AR dengan findBy/where/join, definisi relasi antar tabel, perbedaan Active Query dengan Query Builder, serta praktik CRUD lengkap yang aman dari SQL injection karena pemakaian parameter binding.

Sejauh ini aplikasi kalian masih "bisu" — halaman statis tanpa data. Di episode 5 ini kita membuka jalur utama antara aplikasi dan database: Active Record (AR). Inilah komponen yang paling sering kalian pakai sehari-hari di Yii, dan memahaminya secara benar akan menentukan kualitas kode kalian — baik dari sisi kebenaran maupun keamanan.
Inti pemikiran AR sederhana: satu baris tabel = satu object PHP. Kalian tidak menulis SQL mentah untuk CRUD sederhana; kalian membuat object, mengisi atributnya, lalu menyimpannya. Yii juga menyediakan Query Builder untuk query kompleks yang tetap aman karena memakai parameter binding. Di episode ini kita bedah keduanya, lalu membangun CRUD lengkap.
Model AR di Yii adalah class yang mewarisi yii\db\ActiveRecord. Untuk tabel posts:
<?php
namespace app\models;
use yii\db\ActiveRecord;
class Post extends ActiveRecord
{
public static function tableName(): string
{
return 'posts';
}
public function rules(): array
{
return [
[['title', 'body'], 'required'],
[['title'], 'string', 'max' => 255],
[['body'], 'string'],
[['status'], 'in', 'range' => ['draft', 'published']],
];
}
}tableName() memberi tahu Yii tabel mana yang diwakili.rules() berisi aturan validasi — daftar ini dipakai oleh form (episode 7), oleh save(), dan oleh API. Inilah satu-satunya tempat aturan didefinisikan, tidak tersebar.$id, $title, dst.Terkadang satu model dipakai di konteks berbeda dengan aturan berbeda (misal saat create title wajib, saat update boleh kosong). Scenarios menjawab ini:
<?php
namespace app\models;
use yii\db\ActiveRecord;
class Post extends ActiveRecord
{
public const SCENARIO_DRAFT = 'draft';
public function rules(): array
{
return [
[['title'], 'required', 'on' => self::SCENARIO_DRAFT],
[['title', 'body'], 'safe', 'on' => self::SCENARIO_DRAFT],
];
}
}
// pemakaian
$post = new Post(['scenario' => Post::SCENARIO_DRAFT]);
$post->load(Yii::$app->request->post());Yii membedakan dua cara menulis query: ActiveQuery (berbasis model AR, mengembalikan object model) dan Query Builder (berbasis tabel, mengembalikan array). Keduanya dibangun di atas yii\db\Query sehingga sintaksnya nyaris sama.
use app\models\Post;
// mencari satu record
$post = Post::findOne(5); // WHERE id = 5
$post = Post::find()->where(['id' => 5])->one(); // bentuk eksplisit
// kondisi dasar
$posts = Post::find()
->where(['status' => 'published'])
->andWhere(['like', 'title', 'Yii'])
->orderBy(['created_at' => SORT_DESC])
->limit(10)
->all();
// menghitung
$count = Post::find()->where(['status' => 'published'])->count();Kunci keamanan yang sering diabaikan: jangan pernah menulis query mentah dengan interpolasi string:
$posts = Post::find()->where("title = '$title'")->all(); // berbahaya!$posts = Post::find()->where(['title' => $title])->all(); // nilai di-bind otomatis
$posts = Post::find()->where('title = :t', ['t' => $title])->all(); // binding eksplisitDengan bentuk array, Yii meng-escape nilai secara otomatis. SQL injection di Yii biasanya terjadi karena kalian menulis string mentah tanpa binding — kita bahas lengkap di episode 17.
Daya tarik utama AR adalah mendeklarasikan relasi sebagai method. Ambil contoh tabel posts dan comments:
<?php
namespace app\models;
use yii\db\ActiveRecord;
class Post extends ActiveRecord
{
public function getComments(): \yii\db\ActiveQuery
{
return $this->hasMany(Comment::class, ['post_id' => 'id']);
}
public function getAuthor(): \yii\db\ActiveQuery
{
return $this->hasOne(User::class, ['id' => 'user_id']);
}
}$post = Post::findOne(5);
$comments = $post->comments; // komentar milik post 5
$author = $post->author; // penulis post 5Dua aturan ajaib: relasi diakses sebagai properti ($post->comments), dan kualitas method menentukan tipe relasi — hasOne untuk satu, hasMany untuk banyak, dengan pasangan kunci ['kolom_tabel_lawan' => 'kolom_tabel_sendiri'].
Untuk menghindari masalah N+1, gabungkan relasi dalam satu query dengan eager loading:
$posts = Post::find()->with('comments')->all();
// query relasi digabung: 1 query posts + 1 query commentsUntuk agregasi atau query lintas tabel, Query Builder lebih ekspresif:
use yii\db\Query;
$rows = (new Query())
->select(['user_id', 'COUNT(*) AS total'])
->from('comments')
->where(['status' => 'approved'])
->groupBy('user_id')
->having(['>', 'total', 5])
->orderBy(['total' => SORT_DESC])
->all();Hasilnya array associative — bukan object model. Gunakan Query Builder saat kalian hanya butuh data mentah, dan Active Query saat kalian butuh object model beserta relasinya.
Tip
Aturan praktis memilih: Active Query ketika hasilnya akan dipakai dengan relasi atau diproses lebih lanjut sebagai object; Query Builder ketika hanya butuh satu baris data untuk laporan atau agregasi. Untuk debugging query, aktifkan components.db.enableQueryLog atau pakai Debug Toolbar (episode 13) agar SQL yang dihasilkan terlihat transparan.
Mari satukan semuanya dalam satu alur CRUD. Controller:
<?php
namespace app\controllers;
use app\models\Post;
use yii\web\Controller;
use yii\web\NotFoundHttpException;
class PostController extends Controller
{
public function actionIndex(): string
{
$posts = Post::find()
->where(['status' => 'published'])
->orderBy(['created_at' => SORT_DESC])
->all();
return $this->render('index', ['posts' => $posts]);
}
public function actionCreate()
{
$post = new Post();
if ($post->load(Yii::$app->request->post()) && $post->save()) {
Yii::$app->session->setFlash('success', 'Post disimpan.');
return $this->redirect(['post/view', 'id' => $post->id]);
}
return $this->render('create', ['model' => $post]);
}
public function actionDelete(int $id)
{
$post = $this->findModel($id);
$post->delete();
Yii::$app->session->setFlash('success', 'Post dihapus.');
return $this->redirect(['post/index']);
}
protected function findModel(int $id): Post
{
$post = Post::findOne($id);
if ($post === null) {
throw new NotFoundHttpException('Post tidak ditemukan.');
}
return $post;
}
}Perhatikan pola load() + save(): load() mengisi atribut model dari request POST sesuai safe attributes, lalu save() menjalankan validasi dan menyimpan jika lolos. Satu method untuk create maupun update — prinsip yang membuat kode CRUD tetap pendek.
Inti yang harus dibawa pulang:
tableName(), rules(), dan method relasi.['title' => $title]) — jangan interpolasi string.hasOne/hasMany diakses sebagai properti; gunakan with() untuk eager loading.load() + save() adalah pola standar CRUD; findOne() + NotFoundHttpException untuk detail.Di episode 6 selanjutnya, kita membahas migrations & database — bagaimana skema database dikelola sebagai kode yang bisa di-versioning, membuat dan mengubah tabel dengan yii migrate, serta seeding data awal. Inilah fondasi yang membuat kolaborasi tim dan deployment otomatis menjadi mulus!