0 follower

Active Record

Хоча Yii DAO справляється практично із будь-якими завданнями, які стосуються роботи з БД, майже напевно 90% часу піде на написання SQL-виразів, що реалізують спільні операції CRUD (створення, читання, оновлення та видалення). Крім того, код, змішаний із SQL-виразами, підтримувати проблематично. Для вирішення цих проблем ми можемо скористатися Active Record.

Active Record реалізує популярний підхід обʼєктно-реляційного проектування (ORM). Кожен клас AR відображає таблицю (або представлення) бази даних, екземпляр AR - рядок в цій таблиці, а загальні операції CRUD реалізовані як методи AR. В результаті, ми можемо працювати із більшою обʼєктно-орієнтованістю. Наприклад, використовуючи наступний код, можна вставити новий рядок у таблицю tbl_post.

$post=new Post;
$post->title='тестовий запис';
$post->content='вміст запису';
$post->save();

Нижче ми покажемо, як налаштувати та використовувати AR для реалізації CRUD-операцій, а в наступному розділі - як використовувати AR для роботи зі звʼязаними таблицями. Для прикладів у цьому розділі ми будемо використовувати наступну таблицю. Зверніть увагу, що при використанні БД MySQL у SQL-виразу нижче AUTOINCREMENT слід замінити на AUTO_INCREMENT.

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
);

Примітка: AR не дає рішення для всіх завдань, що стосуються роботи із базами даних. Найкраще його використовувати для моделювання таблиць у конструкціях PHP і для нескладних SQL-запитів. Для складних випадків слід використовувати Yii DAO.

1. Зʼєднання з базою даних

Для роботи AR потрібне підключення до бази даних. За замовчуванням, передбачається, що компонент додатку db надає необхідний екземпляр класу CDbConnection, який відповідає за підключення до бази. Нижче наведено приклад конфігурації додатку:

return array(
    'components'=>array(
        'db'=>array(
            'class'=>'system.db.CDbConnection',
            'connectionString'=>'sqlite:path/to/dbfile',
            // ввімкнути кешування схем для покращення продуктивності
            // 'schemaCachingDuration'=>3600,
        ),
    ),
);

Підказка: Оскільки для отримання інформації про поля таблиці AR використовує метадані, потрібен якийсь час для їх читання та аналізу. Якщо не передбачається, що схема бази даних буде мінятися, то слід ввімкнути кешування схеми, встановивши для атрибута CDbConnection::schemaCachingDuration будь-яке значення більше нуля.

На даний момент AR підтримує наступні СУБД:

Якщо ви хочете використовувати інший компонент ніж db або припускаєте, використовуючи AR, працювати з кількома БД, то слід перевизначити метод CActiveRecord::getDbConnection(). Клас CActiveRecord є базовим класом для всіх класів AR.

Підказка: Є кілька способів для роботи AR з кількома БД. Якщо схеми використовуваних баз різняться, то можна створити різні базові класи AR із різною реалізацією методу getDbConnection(). В іншому випадку, простіше буде динамічно змінювати статичну змінну CActiveRecord::db.

2. Визначення AR-класу

Для доступу до таблиці БД нам, насамперед, потрібно визначити клас AR шляхом успадкування класу CActiveRecord. Кожен клас AR представляє одну таблицю бази даних, а екземпляр класу — рядок в цій таблиці. Нижче наведено мінімальний код, необхідний для визначення класу AR, що представляє таблицю tbl_post.

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

Підказка: Оскільки класи AR часто зʼявляються у багатьох місцях коду, ми можемо замість включення класів по одному, додати цілу папку з AR-класами. Приміром, якщо AR-класи знаходяться у папці protected/models, ми можемо налаштувати додаток наступним чином:

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

За замовчуванням імʼя AR-класу збігається із назвою таблиці в базі даних. Якщо вони розрізняються, буде потрібно перевизначити метод tableName(). Метод model() оголошується для кожного AR-класу.

Інформація: Для використання префіксів таблиць, метод tableName() AR-класу може бути перевизначений як показано нижче:

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

Замість того, щоб повертати повне імʼя таблиці, ми повертаємо імʼя таблиці без префікса і містимо його у подвійні фігурні дужки.

Значення полів у рядку таблиці доступні як атрибути відповідного екземпляра AR-класу. Наприклад, код нижче встановлює значення для атрибуту title:

$post=new Post;
$post->title='тестовий запис';

Хоча ми ніколи не оголошуємо заздалегідь властивість title класу Post, ми, тим не менше, можемо звернутися до нього як у коді вище. Це можливо через те, що title є полем в таблиці tbl_post і CActiveRecord робить його доступним у якості властивості завдяки магічному методу PHP __get(). Якщо аналогічним чином звернутися до неіснуючого поля, буде викликано виключення.

Інформація: У цьому посібнику ми називаємо стовпці і таблиці в нижньому регістрі так як різні СУБД працюють з регістрозалежними іменами по-різному. Наприклад, PostgreSQL вважає імʼя стовпців регістронезалежними за замовчуванням, і ми повинні містити імʼя стовпця у лапки в умовах запиту, якщо імʼя стовпця має великі літери. Використання нижнього регістру допомагає уникнути даної проблеми.

AR спирається на вірно визначені первинні ключі таблиць БД. Якщо у таблиці немає первинного ключа, потрібно вказати у відповідному класі AR стовпці, які будуть використовуватися як первинний ключ. Зробити це можна шляхом перекриття методу primaryKey():

public function primaryKey()
{
    return 'id';
    // Для складеного первинного ключа слід використовувати масив:
    // return array('pk1', 'pk2');
}

3. Створення запису

Для додання нового рядка у таблицю БД, нам необхідно створити новий екземпляр відповідного класу, присвоїти значення атрибутам, асоційованим з полями таблиці, і викликати метод save() для завершення додавання.

$post=new Post;
$post->title='тестовий запис';
$post->content='вміст тестового запису';
$post->create_time=time();
$post->save();

Якщо первинний ключ таблиці автоінкрементний, то після додавання екземпляр АР міститиме оновлене значення первинного ключа. В наведеному вище прикладі властивість id завжди буде містити первинний ключ для нового запису.

Якщо поле задано у схемі таблиці із деяким статичним значенням за замовчуванням (наприклад, рядок або число), то після створення екземпляра відповідна властивість екземпляра AR буде автоматично містити це значення. Один із способів змінити це значення - прописати його у AR-класі.

class Post extends CActiveRecord
{
    public $title='будь ласка, введіть заголовок';
    …
}
 
$post=new Post;
echo $post->title;  // відобразиться: будь ласка, введіть заголовок

До збереження запису (додавання або оновлення) атрибуту може бути присвоєно значення типу CDbExpression. Наприклад, для збереження поточної дати, що повертається функцією MySQL NOW(), можна використовувати наступний код:

$post=new Post;
$post->create_time=new CDbExpression('NOW()');
// $post->create_time='NOW()'; цей варіант працювати не буде
// так як значення 'NOW()' буде сприйнято як рядок
$post->save();

Підказка: Незважаючи на те, що AR дозволяє проводити різні операції без написання громіздкого SQL, часто необхідно знати, який SQL виконується насправді. Цього можна досягти, ввімкнувши журналювання. Приміром, щоб вивести запити SQL в кінці кожної сторінки, ми можемо ввімкнути CWebLogRoute у налаштуваннях додатку. Можна задати параметр CDbConnection::enableParamLogging в true та отримати також значення параметрів запитів.

4. Читання запису

Для читання даних з таблиці бази даних потрібно викликати метод find:

// знайти перший рядок, що задовольняє умові
$post=Post::model()->find($condition,$params);
// знайти рядок із вказаним значенням первинного ключа
$post=Post::model()->findByPk($postID,$condition,$params);
// знайти рядок із зазначеними значеннями атрибута
$post=Post::model()->findByAttributes($attributes,$condition,$params);
// знайти перший рядок, використовуючи деякий вираз SQL
$post=Post::model()->findBySql($sql,$params);

Вище ми викликаємо метод find через Post::model(). Запамʼятайте, що статичний метод model() обовʼязковий для кожного AR-класу. Цей метод повертає екземпляр AR, що використовується для доступу до методів рівня класу (дещо схоже зі статичними методами класу) у контексті обʼєкту.

Якщо метод find знаходить рядок, відповідний умовам запиту, він повертає екземпляр класу Post, властивості якого містять значення відповідних полів рядка таблиці. Далі ми можемо читати завантажені значення аналогічно звичайним властивостями обʼєктів, наприклад, echo $post->title;.

У випадку, якщо в базі немає даних, що відповідають умовам запиту, метод find поверне значення null.

Параметри $condition та $params використовуються для уточнення запиту. В даному випадку $condition може бути рядком, що відповідає оператору WHERE у SQL-виразі, а $params — масивом параметрів, значення яких повинні бути привʼязані до маркерів, зазначених у $condition. Наприклад:

// знайдемо рядок, де postID=10
$post=Post::model()->find('postID=:postID', array(':postID'=>10));

Примітка: У наведеному вище прикладі, нам може знадобитися взяти у лапки звернення до стовпця postID для деяких СУБД. Наприклад, якщо ми використовуємо СУБД PostgreSQL, нам слід писати умову як "postID"=:postID, бо PostgreSQL за замовчуванням вважає імʼя стовпця регістронезалежним.

Крім того, можна використовувати $condition для вказівки складних умов запиту. Замість рядка параметр $condition може бути екземпляром класу CDbCriteria, який дозволяє вказати інші умови ніж WHERE вираз. Наприклад:

$criteria=new CDbCriteria;
$criteria->select='title';  // вибираємо тільки поле 'title'
$criteria->condition='postID=:postID';
$criteria->params=array(':postID'=>10);
$post=Post::model()->find($criteria); // параметр $params не потрібен

Зверніть увагу, якщо у якості умови запиту використовується CDbCriteria, то параметр $params вже не потрібен, оскільки його можна вказати безпосередньо у CDbCriteria, як показано вище.

Крім використання CDbCriteria, є інший спосіб вказати умову - передати методу масив ключів і значень, відповідних іменам і значень властивостей критерію. Приклад вище можна переписати таким чином:

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

Інформація: У випадку, коли умова укладається відповідно значень деяких полів, можна скористатися методом findByAttributes(), де параметр $attributes являє собою масив значень, проіндексованих по імені поля. У деяких фреймворках це завдання вирішується шляхом використання методів типу findByNameAndTitle. Хоча такий спосіб і виглядає привабливо, часто він викликає плутанину і проблеми, повʼязані з чутливістю імен полів до регістру.

У разі, якщо умові запиту відповідає безліч рядків, ми можемо отримати їх всі, використовуючи методи findAll, наведені нижче. Як ми відзначили раніше, кожен з цих методів findAll має find аналог.

// знайдемо всі рядки, що задовольняють умові
$posts=Post::model()->findAll($condition,$params);
// знайдемо всі рядки із зазначеними значеннями первинного ключа
$posts=Post::model()->findAllByPk($postIDs,$condition,$params);
// знайдемо всі рядки із зазначеними значеннями атрибута
$posts=Post::model()->findAllByAttributes($attributes,$condition,$params);
// знайдемо всі рядки, використовуючи SQL-вираз
$posts=Post::model()->findAllBySql($sql,$params);

На відміну від find, метод findAll у випадку, якщо немає жодного рядка, що задовольняє запиту, повертає не null, а порожній масив.

Крім методів find та findAll описаних вище, для зручності також доступні наступні методи:

// одержимо кількість рядків, які відповідають умові
$n=Post::model()->count($condition,$params);
// одержимо кількість рядків з використанням зазначеного SQL-виразу
$n=Post::model()->countBySql($sql,$params);
// перевіримо, чи є хоча б один рядок, що задовольняє умові
$exists=Post::model()->exists($condition,$params);

5. Оновлення запису

Заповнивши екземпляр AR значеннями полів, ми змінюємо ці значення і зберігаємо їх назад у БД.

$post=Post::model()->findByPk(10);
$post->title='new post title';
$post->save(); // зберігаємо зміни у базу даних

Як можна було помітити, ми використовуємо метод save() для додавання та оновлення записів. Якщо екземпляр AR створений з використанням оператора new, то виклик методу save() призведе до додавання нового рядка у базу даних. У разі ж, якщо екземпляр AR створений як результат виклику методів find або findAll, виклик методу save() оновить дані існуючого рядка в таблиці. Насправді, можна використовувати властивість CActiveRecord::isNewRecord для вказівки, чи є екземпляр AR новим або ні.

Крім того, можна оновити один або кілька рядків у таблиці без їх попереднього завантаження. Для цього в AR існують такі методи рівня класу:

// оновимо рядки, що відповідають заданій умові
Post::model()->updateAll($attributes,$condition,$params);
// оновимо рядки, що задовольняють заданій умові і первинному ключу (або декільком ключам)
Post::model()->updateByPk($pk,$attributes,$condition,$params);
// оновимо поля-лічильники у рядках, що задовольняють заданим умовам 
Post::model()->updateCounters($counters,$condition,$params);

Тут $attributes - це масив значень полів, проіндексованих по імені поля, $counters - масив інкрементних значень, проіндексованих по імені поля, $condition та $params аналогічно опису вище.

6. Видалення запису

Ми можемо видалити рядок, якщо екземпляр AR був заповнений значеннями цього рядка.

$post=Post::model()->findByPk(10); // припускаємо, що запис з ID=10 існує
$post->delete(); // видаляємо рядок з таблиці

Зверніть увагу, що після видалення екземпляр AR не змінюється, але відповідного запису у таблиці вже немає.

Наступні методи використовуються для видалення рядків без їх попереднього завантаження:

// видалимо рядки, відповідні вказаній умові
Post::model()->deleteAll($condition,$params);
// видалимо рядки, відповідні вказаній умові і первинному ключу (або декільком ключам)
Post::model()->deleteByPk($pk,$condition,$params);

7. Перевірка даних

Часто під час додавання або оновлення рядка, нам потрібно перевірити, чи відповідають значення полів деякими правилами. Особливо це важливо у випадку даних, що надходять зі сторони клієнта, - у переважній більшості випадків цим даним не можна довіряти.

AR здійснює перевірку даних автоматично у момент виклику методу save(). Перевірка заснована на правилах, заданих у методі AR-класу rules(). Детально ознайомитися з тим, як задаються правила перевірки, можна в розділі Визначення правил перевірки. Нижче наведемо типовий порядок обробки у момент збереження запису:

if($post->save())
{
    // дані коректні та успішно додані/оновлені
}
else
{
    // дані некоректні, повідомлення про помилки можуть бути отримані через виклик методу getErrors()
}

У момент, коли дані для додання або оновлення відправляються користувачем через форму вводу, нам потрібно привласнити їх відповідним властивостям AR. Це можна зробити наступним чином:

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

Якщо полів буде багато, ми отримаємо довгий перелік із подібних присвоювань. Цього можна уникнути, якщо використовувати властивість attributes як показано нижче. Подробиці можна знайти в розділах Безпечне присвоювання значень атрибутам та Створення дії.

// припускаємо, що $_POST['Post'] є масивом значень полів, проіндексованих іменем поля
$post->attributes=$_POST['Post'];
$post->save();

8. Порівняння записів

Екземпляри AR ідентифікуються унікальним чином за значеннями первинного ключа, аналогічно рядкам таблиці, тому для порівняння двох екземплярів нам потрібно просто порівняти значення їх первинних ключів, припускаючи, що обидва екземпляри одного AR-класу. Однак, можна зробити це ще простіше, викликавши метод CActiveRecord::equals().

Інформація: На відміну від реалізації AR в інших фреймворках, Yii підтримує в AR складені первинні ключі. Складений первинний ключ складається з двох і більше полів таблиці. Відповідно, первинний ключ в Yii представлений як масив, а властивість primaryKey містить значення первинного ключа для екземпляра AR.

9. Тонке налаштування

Клас CActiveRecord надає кілька методів, які можуть бути перевизначені в дочірніх класах для тонкого налаштування роботи AR.

  • beforeValidate та afterValidate: методи викликаються до і після здійснення перевірки;

  • beforeSave та afterSave: методи викликаються до і після збереження екземпляра AR;

  • beforeDelete та afterDelete: методи викликаються до і після видалення екземпляра AR;

  • afterConstruct: метод викликається для кожного екземпляра AR, створеного за допомогою оператора new;

  • beforeFind: метод викликається перед тим, як finder AR виконає запит (наприклад, find(), findAll());

  • afterFind: метод викликається для кожного екземпляра AR, створеного в результаті виконання запиту.

10. Використання транзакцій з AR

Кожен екземпляр AR містить властивість dbConnection, яка є екземпляром класу CDbConnection. Відповідно, у разі потреби можна використовувати можливість транзакцій, що надається Yii DAO:

$model=Post::model();
$transaction=$model->dbConnection->beginTransaction();
try
{
    // пошук і збереження - кроки, які можна розбити третім запитом
    // відповідно, ми використовуємо транзакцію, щоб переконатися у цілісності
    $post=$model->findByPk(10);
    $post->title='new post title';
    $post->save();
    $transaction->commit();
    if($post->save())
        $transaction->commit();
    else
        $transaction->rollback();
}
catch(Exception $e)
{
    $transaction->rollback();
    throw $e;
}

11. Іменовані групи умов

Інформація: Ідея груп умов запозичена у Ruby on Rails.

Іменована група умов є іменований критерій запиту, який можна використовувати з іншими групами і застосовувати до запитів AR.

Іменовані групи найчастіше описуються у методі CActiveRecord::scopes() парами імʼя-умова. Наведений нижче код описує дві іменовані групи умов для моделі Post: published та recently:

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

Кожна група описується масивом, який може бути використаний для ініціалізації екземпляра CDbCriteria. Приміром, recently визначає, що умова order буде create_time DESC, а limit буде дорівнювати 5. Разом ці умови означають, що будуть обрані 5 останніх публікацій.

Іменовані групи умов зазвичай використовуються як модифікатори для методу find. Можна використовувати декілька груп для отримання більш специфічного результату. Приміром, щоб знайти останні опубліковані записи можна використовувати наступний код:

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

У загальному випадку, іменовані групи умов повинні розташовуватися лівіше виклику find. Кожна група визначає критерій запиту, який поєднується з іншими критеріями, включаючи передані безпосередньо методу find. Кінцевий результат отримується застосуванням до запиту набору фільтрів.

Примітка: Іменовані групи можуть бути використані тільки з методами класу. Таким чином, метод повинен викликатися за допомогою ClassName::model().

Іменовані групи умов з параметрами

Іменовані групи умов можуть бути параметризовані. Приміром, нам знадобилося задати кількість публікацій для групи recently. Щоб це зробити, замість того, щоб описувати групу в методі CActiveRecord::scopes, нам необхідно описати новий метод з таким же імʼям, як у групи умов:

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

Після цього, для того, щоб отримати 3 останніх опублікованих записи, можна використовувати наступний код:

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

Якщо не передати параметром 3, то за замовчуванням будуть обрані 5 останніх опублікованих записів.

Група умов за замовчуванням

Клас моделі може містити групу умов за замовчуванням, яка буде застосовуватися до всіх запитів (включаючи реляційні). Наприклад, на сайті реалізована підтримка декількох мов і вміст відображається на мові, обраній користувачем. Так як запитів, повʼязаних з отриманням даних швидше за все досить багато, для вирішення цього завдання ми можемо визначити групу умов за замовчуванням. Для цього ми перекриваємо метод CActiveRecord::defaultScope наступним чином:

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

Тепер до зазначеного нижче виклику буде автоматично застосовані наші умови:

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

Примітка: Як група умов за замовчуванням, так і іменована група умов застосовуються тільки до запитів типу SELECT та ігноруються при запитах виду INSERT, UPDATE або DELETE. Також, не можна використовувати AR-модель для запитів у методах, що відповідають за оголошення її ж груп умов (як іменованої, так і групи умов за замовчуванням).

Found a typo or you think this page needs improvement?
Edit it on github !