Active Record

Meskipun Yii DAO secara virtual dapat menangani setiap tugas terkait database, kemungkinan kita akan menghabiskan 90% waktu kita dalam penulisan beberapa pernyataan SQL yang melakukan operasi umum CRUD (create, read, update dan delete) tetap ada. Pemeliharaan kode kita saat dicampur dengan pernyataan SQL juga akan menambah kesulitan. Untuk memecahkan masalah ini, kita dapat menggunakan Active Record.

Active Record (AR) adalah teknik populer Pemetaan Relasional Objek / Object-Relational Mapping (ORM). Setiap kelas AR mewakili tabel database (atau view) yang atributnya diwakili oleh properti kelas AR, dan turunan AR mewakili baris pada tabel tersebut. Operasi umum CRUD diimplementasikan sebagai metode AR. Hasilnya, kita dapat mengakses data dengan cara lebih terorientasi-objek. Sebagai contoh, kita dapat menggunakan kode berikut untuk menyisipkan baris baru ke dalam tabel Post:

$post=new Post;
$post->title='sample post';
$post->content='post body content';
$post->save();

Selanjutnya kita mendalami bagaimana menyiapkan AR dan menggunakannya untuk melakukan operasi CRUD. Kita akan melihat bagaimana untuk menggunakan AR untuk bekerja dengan relasi database dalam seksi berikutnya. Demi kemudahan, kami menggunakan tabel database berikut sebagai contoh kita dalam bagian ini. Harap dicatat bahwa jika Anda menggunakan database MySQL, Anda harus mengganti AUTOINCREMENT dengan AUTO_INCREMENT dalam SQL berikut.

CREATE TABLE tbl_post (
    id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
    title VARCHAR(128) NOT NULL,
    content TEXT NOT NULL,
    create_time INTEGER NOT NULL
);

Catatan: AR tidak ditujukan untuk memecahkan semua tugas-tugas terkait-database. AR paling cocok dipakai untuk memodelkan tabel database dalam konstruksi PHP dan melakukan query yang tidak melibatkan SQL yang kompleks. Yii DAO baru dipakai untuk skenario kompleks tersebut.

1. Membuat Koneksi DB

AR bergantung pada koneksi DB untuk melaksanakan operasi terkait-DB. Secara default, AR menganggap bahwa komponen aplikasi db adalah turunan CDbConnection yang dibutuhkan untuk bertindak sebagai koneksi DB. Konfigurasi aplikasi berikut memperlihatkan sebuah contoh:

return array(
    'components'=>array(
        'db'=>array(
            'class'=>'system.db.CDbConnection',
            'connectionString'=>'sqlite:path/to/dbfile',
            // hidupkan cache schema untuk meningkatkan kinerja
            // 'schemaCachingDuration'=>3600,
        ),
    ),
);

Tip: Karena Active Record bergantung pada metadata mengenai tabel untuk menentukan informasi kolom, dibutuhkan waktu untuk membaca metadata dan menganalisanya. Jika skema database Anda sepertinya kurang perlu diubah, Anda bisa menghidupkan cache skema (schema caching) dengan mengkonfigurasi properti CDbConnection::schemaCachingDuration ke nilai lebih besar daripada 0.

Dukungan terhadap AR dibatasi oleh DBMS. Saat ini, hanya DBMS berikut yang didukung:

Jika Anda ingin menggunakan komponen aplikasi selain db, atau jika Anda ingin bekerja dengan multipel database menggunakan AR, Anda harus meng-override CActiveRecord::getDbConnection(). Kelas CActiveRecord adalah basis kelas untuk semua kelas AR.

Tip: Ada dua cara untuk bekerja dengan multipel database dalam AR. Jika skema database berbeda, Anda dapat membuat basis kelas AR yang berbeda dengan implementasi berbeda pada getDbConnection(). Sebaliknya, secara dinamis mengubah variabel static CActiveRecord::db merupakan ide yang lebih baik.

2. Mendefinisikan Kelas AR

Untuk mengakses tabel database, pertama kita perlu mendefinisikan kelas AR dengan menurun CActiveRecord. Setiap kelas AR mewakili satu tabel database, dan instance AR mewakili sebuah record (baris) dalam tabel tersebut. Contoh berikut memperlihatkan kode minimal yang diperlukan untuk kelas AR yang mewakili tabel Post.

class Post extends CActiveRecord
{
    public static function model($className=__CLASS__)
    {
        return parent::model($className);
    }
 
    public function tableName()
    {
        return 'tbl_post';
    }
}

Tip: Karena kelas AR sering dirujuk di banyak tempat, kita dapat mengimpor seluruh direktori yang berisi kelas AR, daripada menyertakannya satu demi satu. Sebagai contoh, jika semua file kelas AR kita di bawah protected/models, kita dapat mengkonfigurasi aplikasi sebagai berikut:

return array(
  'import'=>array(
      'application.models.*',
  ),
);

Secara default, nama kelas AR sama dengan nama tabel database. Meng-override metode tableName() jika berbeda. Metode model() dideklarasikan seperti itu untuk setiap kelas AR (akan dijelaskan kemudian).

Info: Untuk menggunakan fitur prefiks tabel, metode tableName() untuk kelas AR harus di-override demikian,

public function tableName()
{
    return '{{post}}';
}

Alih-alih mengembalikan nama tabel secara lengkap, kita mengembalikan nama tabel tanpa prefiks dan membungkusnya dengan kurung kurawal ganda.

Nilai kolom pada baris tabel dapat diakses sebagai properti turunan kelas AR terkait. Sebagai contoh, kode berikut menyetel kolom title (atribut):

$post=new Post;
$post->title='a sample post';

Meskipun kita tidak pernah mendeklarasikan properti title secara eksplisit dalam kelas Post, kita masih dapat mengaksesnya dalam kode di atas. Ini disebabkan title adalah kolom dalam tabel Post, dan CActiveRecord membuatnya bisa diakses seperti layaknya properti dengan bantuan metode magic PHP __get(). Exception (exception) akan ditampilkan jika kita mencoba mengakses kolom yang tidak ada, dengan cara yang sama.

Info: Di dalam panduan ini, kita menggunakan huruf kecil untuk seluruh nama tabel dan kolom. Ini dikarenakan berbeda-bedanya cara DBMS dalam penanganan case-sensitive. Contohnya PostgreSQL tidak memperlakukan nama kolom case-sensitive (jadi huruf besar sama dengan huruf kecil) secara default, dan kita harus memberi tanda kutip pada kolom dalam query jika namanya memang mengandung campuran huruf besar dan kecil. Menggunakan huruf kecil saja akan menyelesaikan permasalahan ini.

AR bergantung pada pendefinisian primary key tabel yang baik. Jika sebuah tabel tidak memiliki primary key, maka AR membutuhkan kelas AR menentukan kolom mana yang dijadikan primary key dengan meng-overide fungsi primaryKey() sebagai berikut,

public function primaryKey()
{
    return 'id';
    // Kalau composite primary key, mak kembalikan nilai array seperti demikian
    // return array('pk1', 'pk2');
}

3. Membuat Record

Untuk melakukan insert baris baru ke dalam tabel database, kita membuat instance baru dari kelas AR terkait, menyetel propertinya yang berkaitan dengan kolom tabel, dan memanggil metode save() untuk menyelesaikan proses insert.

$post=new Post;
$post->title='sample post';
$post->content='content for the sample post';
$post->createTime=time();
$post->save();

Jika primary key table bersifat auto-increment, setelah insert instance AR maka akan berisi primary key yang baru. Dalam contoh di atas, properti id akan merujuk pada nilai primary key tulisan yang baru saja disisipkan, meskipun kita tidak pernah mengubahnya secara eksplisit.

Jika kolom didefinisikan dengan beberapa nilai standar statis (misalnya string, angka) dalam skema tabel, properti terkait dalam instance AR akan secara otomatis memiliki nilai tersebut setelah instance dibuat. Satu cara untuk mengubah nilai default ini adalah dengan secara eksplisit mendeklarasikan properti dalam kelas AR:

class Post extends CActiveRecord
{
    public $title='please enter a title';
    ......
}
 
$post=new Post;
echo $post->title;  // ini akan menampilkan: please enter a title

Atribut dapat menempatkan nilai tipe CDbExpression sebelum record disimpan (baik skenario insert ataupun update) ke database. Sebagai contoh, untuk menyimpan timestamp yang dihasilkan oleh fungsi MySQL NOW(), kita dapat menggunakan kode berikut:

$post=new Post;
$post->createTime=new CDbExpression('NOW()');
// $post->createTime='NOW()'; tidak akan bekerja karena
// 'NOW()' akan dianggap sebagai string
$post->save();

Tip: Karena AR mengijinkan kita untuk melakukan operasi database tanpa menulis sejumlah pernyataan SQL, seringkali kita ingin mengetahui pernyataan SQL apa yang dijalankan oleh AR. Ini bisa dilakukan dengan menghidupkan fitur pencatatan pada Yii. Sebagai contoh, kita dapat menghidupkan CWebLogRoute dalam konfigurasi aplikasi, dan kita akan melihat pernyataan SQL yang dijalankan untuk ditampilkan di akhir setiap halaman Web. Kita dapat menyetel CDbConnection::enableParamLogging menjadi true dalam konfigurasi aplikasi agar nilai parameter yang diikat ke pernyataan SQL juga dicatat.

4. Membaca Record

Untuk membaca data dalam tabel database, kita memanggil salah satu metode find seperti berikut.

// cari baris pertama sesuai dengan kondisi yang ditetapkan
$post=Post::model()->find($condition,$params);
// cari baris dengan primary key yang ditetapkan
$post=Post::model()->findByPk($postID,$condition,$params);
// cari baris dengan nilai atribut yang ditetapkan
$post=Post::model()->findByAttributes($attributes,$condition,$params);
// cari baris pertama menggunakan pernyataan SQL yang ditetapkan
$post=Post::model()->findBySql($sql,$params);

Dalam contoh di atas, kita memanggil metode find dengan Post::model(). Ingat bahwa metode statis model() diperlukan oleh setiap kelas AR. Metode ini menghasilkan instance AR yang dipakai untuk mengakses metode tingkat kelas (mirip dengan metode kelas static) dalam konteks obyek.

Jika metode find menemukan baris yang sesuai dengan kondisi query, ia akan mengembalikan turunan Post yang propertinya berisi nilai kolom terkait dari baris table. Kemudian kita dapat membaca nilai yang diambil sepert yang kita lakukan pada properti obyek, sebagai contoh, echo $post->title;.

Metode find akan menghasilkan null jika tidak ada yang ditemukan dalam database dengan kondisi query yang diberikan.

Ketika memanggil find, kita menggunakan $condition dan $params untuk menetapkan kondisi query. Di sini, $condition dapat berupa string yang mewakili klausul WHERE dalam pernyataan SQL, dan $params adalah array parameter yang nilainya harus diikat ke tempat di dalam $condition. Sebagai contoh,

// cari baris dengan postID=10
$post=Post::model()->find('postID=:postID', array(':postID'=>10));

Catatan: Dalam contoh di atas, kita mungkin perlu meng-escape referensi pada kolom postID untuk DBMS tertentu. Sebagai contoh, jika kita menggunakan PostgreSQL, kita harus menulis kondisi sebagai "postID"=:postID, karena PostgreSQL standarnya akan memperlakukan nama kolom tidak case-sensitive.

Kita juga bisa menggunakan $condition untuk menetapkan kondisi query lebih kompleks Alih-alih mengisi sebuah string, kita dapat mengatur $condition menjadi instance CDbCriteria yang mengijinkan kita untuk menetapkan kondisi selain klausul WHERE. Sebagai contoh,

$criteria=new CDbCriteria;
$criteria->select='title';  // hanya memilih kolom 'title'
$criteria->condition='postID=:postID';
$criteria->params=array(':postID'=>10);
$post=Post::model()->find($criteria); // $params tidak diperlukan

Catatan, saat menggunakan CDbCriteria sebagai kondisi query, parameter $params tidak lagi diperlukan karena ia bisa ditetapkan dalam CDbCriteria, seperti dicontohkan di atas.

Cara alternatif terhadap CDbCriteria adalah dengan mengoper array ke metode find. Kunci dan nilai array masing-masing berhubungan dengan properti kriteria nama dan nilai. Contoh di atas dapat ditulis ulang seperti berikut

$post=Post::model()->find(array(
    'select'=>'title',
    'condition'=>'postID=:postID',
    'params'=>array(':postID'=>10),
));

Info: Saat kondisi query menemukan beberapa kolom dengan nilai yang ditetapkan, kita dapat menggunakan findByAttributes(). Kita biarkan parameter $attributes menjadi array nilai yang diindeks oleh nama kolom. Dalam beberapa framework, tugas ini bisa dilaksanakan dengan memanggil metode seperti findByNameAndTitle. Meskipun pendekatan ini terlihat atraktif, sering menyebabkan kebingungan, konflik dan masalah seperti sensitifitas-jenis huruf pada nama-nama kolom.

Ketika lebih dari satu baris data memenuhi kondisi query yang ditetapkan, kita dapat mengambilnya sekaligus menggunakan metode findAll, masing-masing memiliki pasangan metode find, seperti yang sudah kami jelaskan.

// cari seluruh baris yang sesuai dengan kondisi yang ditetapkan
$posts=Post::model()->findAll($condition,$params);
// cari seluruh baris dengan primary key yang ditetapkan
$posts=Post::model()->findAllByPk($postIDs,$condition,$params);
// cari seluruh baris dengan nilai atribut yang ditetapkan
$posts=Post::model()->findAllByAttributes($attributes,$condition,$params);
// cari seluruh baris dengan pernyataan SQL yang ditetapkan
$posts=Post::model()->findAllBySql($sql,$params);

Jika tidak ada yang sama dengan kondisi query, findAll akan mengembalikan array kosong. Ini berbeda dengan find yang akan mengembalikan null jika tidak menemukan apapun.

Selain metode find dan findAll seperti dijelaskan di atas, metode berikut juga disediakan demi kenyamanan:

// ambil sejumlah baris yang sesuai dengan kondisi yang ditetapkan
$n=Post::model()->count($condition,$params);
// ambil sejumlah baris menggunakan pernyataan SQL yang ditetapkan
$n=Post::model()->countBySql($sql,$params);
// periksa apakah ada satu baris yang sesuai denga kondisi yang ditetapkan
$exists=Post::model()->exists($condition,$params);

5. Mengupdate Record

Setelah instance AR diisi dengan nilai kolom, kita dapat mengubah dan menyimpannya kembali ke tabel database.

$post=Post::model()->findByPk(10);
$post->title='new post title';
$post->save(); // simpan perubahan ke database

Seperti yang kita lihat, kita menggunakan metode save() yang sama untuk melakukan operasi insert maupun update. Jika instance AR dibuat menggunakan operator new, pemanggilan save() akan menyisipkan baris record baru ke dalam tabel database; jika turunan AR adalah hasil dari beberapa pemanggilan metode find atau findAll, memanggil save() akan mengupdate baris yang sudah ada dalam tabel. Bahkan, kita dapat menggunakan CActiveRecord::isNewRecord untuk mengetahui apakah turunan AR baru atau tidak.

Dimungkinkan juga untuk mengupdate satu atau beberapa baris dalam sebuah tabel database tanpa memanggilnya lebih dulu. AR menyediakan metode tingkat-kelas yang nyaman untuk tujuan ini:

// mutakhirkan baris yang sama seperti kondisi yang ditetapkan
Post::model()->updateAll($attributes,$condition,$params);
// mutakhirkan baris yang sama seperti kondisi dan primary key yang ditetapkan
Post::model()->updateByPk($pk,$attributes,$condition,$params);
// mutakhirkan kolom counter dalam baris yang sesuai dengan kondisi yang ditetapkan
Post::model()->updateCounters($counters,$condition,$params);

Dalam contoh di atas, $attributes adalah array nilai kolom yang diindeks oleh nama kolom; $counters adalah array nilai inkremental yang diindeks oleh nama kolom; sedangkan $condition dan $params seperti yang sudah dijelaskan dalam sub-bab sebelumnya.

6. Menghapus Record

Kita juga bisa menghapus baris data jika turunan AR sudah diisi dengan baris ini.

$post=Post::model()->findByPk(10); // menganggap ada tulisan yang memiliki ID = 10
$post->delete(); // hapus baris dari tabel database

Catatan, setelah penghapusan, turunan AR tetap tidak berubah, tapi baris terkait dalam tabel database sudah tidak ada.

Metode tingkat kelas berikut disediakan untuk menghapus baris tanpa haris mengambilnya lebih dulu:

// hapus baris yang sesuai dengan kondisi yang ditetapkan
Post::model()->deleteAll($condition,$params);
// hapus baris yang sesuai dengan kondisi dan primary key yang ditetapkan
Post::model()->deleteByPk($pk,$condition,$params);

7. Validasi Data

Ketika melakukan insert atau update baris, seringkali kita harus memeriksa apakah nilai kolom sesuai dengan aturan tertentu. Hal ini penting terutama jika nilai kolom diberikan oleh pengguna akhir. Pada dasarnya, kita seharusnya tidak boleh mempercayai apapun yang berasal dari sisi klien.

AR melakukan validasi data secara otomatis ketika save() sedang dipanggil. Validasi didasarkan pada aturan yang ditetapkan dalam metode rules() pada kelas AR. Untuk lebih jelasnya mengenai bagaimana untuk menetapkan aturan validasi, silahkan merujuk ke bagian Mendeklarasikan Aturan Validasi. Di bawah ini adalah alur kerja umum yang diperlukan oleh penyimpanan record:

if($post->save())
{
    // data benar dan insert/update dengan sukses
}
else
{
    // data tidak benar. panggil getErrors() untuk mengambil pesan kesalahan
}

Ketika data untuk insert atau update dikirimkan oleh pengguna akhir dalam bentuk HTML, kita perlu menempatkannya ke properti AR terkait. Kita apat melakukannya seperti berikut:

$post->title=$_POST['title'];
$post->content=$_POST['content'];
$post->save();

Jika terdapat banyak kolom, kita akan melihat daftar yang panjang dari penempatan tersebut. Ini dapat dipersingkat dengan pemakaian properti attributes seperti ditampilkan di bawah. Rincian dapat ditemukan dalam seksi Mengamankan Penempatan Atribut dan seksi Membuat Aksi.

// anggap $_POST['Post'] adalah array nilai kolom yang diindeks oleh nama kolom
$post->attributes=$_POST['Post'];
$post->save();

8. Membandingkan Record

Seperti baris tabel, turunan AR secara unik diidentifikasi dengan nilai primary key. Oleh karena itu, untuk membandingkan dua instance AR, kita perlu membandingkan nilai primary key-nya, menganggap keduanya milik kelas AR yang sama. Cara paling sederhana adalah dengan memanggil CActiveRecord::equals().

Info: Tidak seperti implementasi AR dalam framework lain, Yii mendukung primary key composite dalam turunan AR-nya. Kunci primer terdiri dari dua atau lebih kolom. Secara bersamaan, nilai primary key disajikan sebagai array dalam Yii. Properti primaryKey memberikan nilai primary key pada turunan AR.

9. Kustomisasi

CActiveRecord menyediakan beberapa metode penampungan yang dapat di-overide dalam anak kelas untuk mengkustomisasi alur kerjanya.

  • beforeValidate dan afterValidate: ini dipanggil sebelum dan sesudah validasi dilakukan.

  • beforeSave dan afterSave: ini dipanggil sebelum dan sesudah penyimpanan instance AR.

  • beforeDelete dan afterDelete: ini dipanggil sebelum dan sesudah instance AR dihapus.

  • afterConstruct: ini dipanggil untuk setiap instance AR yang dibuat menggunakan operator new.

  • beforeFind: ini dipanggil sebelum pencari AR dipakai untuk melakukan query (misal find(), findAll()).

  • afterFind: ini dipanggil untuk setiap instance AR yang dibuat sebagai hasil dari query.

10. Menggunakan Transaksi dengan AR

Setiap instance AR berisi properti bernama dbConnection yang merupakan turunan dari CDbConnection. Kita dapat menggunakan fitur transaksi yang disediakan oleh Yii DAO jika diinginkan saat bekerja dengan AR:

$model=Post::model();
$transaction=$model->dbConnection->beginTransaction();
try
{
    // cari dan simpan adalah dua langkah yang bisa diintervensi oleh permintaan lain
    // oleh karenanya kita menggunakan transaksi untuk memastikan konsistensi dan integritas
    $post=$model->findByPk(10);
    $post->title='new post title';
    $post->save();
    $transaction->commit();
}
catch(Exception $e)
{
    $transaction->rollBack();
}

11. Named Scope

Catatan: Ide named scope berasal dari Ruby on Rails.

Sebuah named scope mewakili kriteria query bernama yang dapat digabung dengan named scope lain dan diterapkan ke query active record.

Named scope dideklarasikan terutama dalam metode CActiveRecord::scopes() sebagai pasangan nama-kriteria. Kode berikut mendeklarasikan tiga named scope, published dan recently, dalam kelas model Post

class Post extends CActiveRecord
{
    ......
    public function scopes()
    {
        return array(
            'published'=>array(
                'condition'=>'status=1',
            ),
            'recently'=>array(
                'order'=>'createTime DESC',
                'limit'=>5,
            ),
        );
    }
}

Setiap named scope dideklarasikan sebagai sebuah array yang dipakai untuk menginisialisasi instance CDbCriteria. Sebagai contoh, named scope recently menetapkan bahwa properti order adalah createTime DESC dan properti limit adalah 5 yang diterjemahkan ke dalam sebuah kriteria query yang harus menghasilkan paling banyak 5 tulisan terbaru.

Named scope dipakai sebagai pembeda pada pemanggilan metode find. Beberapa named scope dapat dikaitkan bersamaan dan menghasilkan set yang lebih terbatas. Sebagai contoh, untuk mencari tulisan terbaru yang diterbitkan, kita menggunakan kode berikut:

$posts=Post::model()->published()->recently()->findAll();

Secara umum, named scope harus berada di sebelah kiri pemanggilan metode find. Masing-masing menyediakan kriteria query, yang merupakan gabungan dengan kriteria lain, termasuk yang dioper ke pemanggilan metode find. Hal ini mirip dengan menambahkan sebuah daftar filter ke sebuah query.

Catatan: Named scope hanya bisa dipakai dengan metode tingkat-kelas. Yakni, metode harus dipanggil menggunakan ClassName::model().

Parameterisasi Named Scope

Named scope dapat diparameterkan. Sebagai contoh, kita mungkin ingin mengkustomisasi sejumlah tulisan dengan named scope recently. Untuk melakukannya, daripada mendeklarasikan named scope dalam metode CActiveRecord::scopes, kita dapat mendefinisikan sebuah metode baru yang namanya sama seperti named scope tadi:

public function recently($limit=5)
{
    $this->getDbCriteria()->mergeWith(array(
        'order'=>'createTime DESC',
        'limit'=>$limit,
    ));
    return $this;
}

Selanjutnya, kita bisa menggunakan pernyataan berikut untuk mengambil 3 tulisan terbaru yang diterbitkan:

$posts=Post::model()->published()->recently(3)->findAll();

Jika kita tidak melengkapi parameter 3 di atas, secara standar kita akan mengambil 5 tulisan terbaru yang diterbitkan.

Default Scope

Kelas model dapat memiliki default scope yang akan diterapkan untuk semua query (termasuk yang relasional) berkenaan dengan model. Sebagai contoh, website yang mendukung multi bahasa mungkin hanya ingin menampilkan konten yang ditetapkan oleh pengguna saat ini. Karena di sana ada banyak query atas konten situs, kita dapat mendefinisikan default scope guna memecahkan masalah ini. Untuk melakukannya, kita meng-override metode CActiveRecord::defaultScope seperti berikut,

class Content extends CActiveRecord
{
    public function defaultScope()
    {
        return array(
            'condition'=>"language='".Yii::app()->language."'",
        );
    }
}

Sekarang, jika metode di bawah ini dipanggil, Yii akan secara otomatis menggunakan kriteria query seperti didefinisikan di atas:

$contents=Content::model()->findAll();

Note: Default scope dan named scope hanya berlaku pada query SELECT. Mereka akan mengabaikan query INSERT, UPDATE, dan DELETE. Juga, ketika mendeklarasikan scope (baik default maupun named), kelas AR tidak dapat digunakan untuk membuat query DB di dalam method yang mendeklarasikan scope.

$Id: database.ar.txt 3318 2011-06-24 21:40:34Z qiang.xue $

Be the first person to leave a comment

Please to leave your comment.