0 follower

Active Record

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

Active Record реализует популярный подход объектно-реляционного проецирования (ORM). Каждый класс AR отражает таблицу (или представление) базы данных, экземпляр AR - строку в этой таблице, а общие операции CRUD реализованы как методы AR. В результате, мы можем работать с большей объектно-ориентированностью. Например, используя следующий код, можно вставить новую строку в таблицу Post.

$post=new Post;
$post->title='тестовый пост';
$post->content='содержимое поста';
$post->save();

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

CREATE TABLE Post (
    id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
    title VARCHAR(128) NOT NULL,
    content TEXT NOT NULL,
    createTime 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 поддерживается следующими СУБД:

Примечание: Microsoft SQL Server поддерживается начиная с версии 1.0.4; Oracle поддерживается начиная с версии 1.0.5.

Если вы хотите использовать другой компонент нежели db или предполагаете, используя AR, работать с несколькими БД, то следует переопределить метод CActiveRecord::getDbConnection(). Класс CActiveRecord является базовым классом для всех классов AR.

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

2. Определение AR-класса

Для доступа к таблице БД нам прежде всего требуется определить класс AR путем наследования класса CActiveRecord. Каждый класс AR представляет одну таблицу базы данных, а экземпляр класса - строку в этой таблице. Ниже приведен минимальный код, требуемый для определения класса AR, представляющего таблицу Post.

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

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

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

По умолчанию имя AR-класса совпадает с названием таблицы в базе данных. Если они различаются, потребуется переопределить метод tableName(). Метод model() объявляется для каждого AR-класса.

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

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

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

Информация: В данном руководстве мы называем столбцы, используя горбатыйРегистр (например, createTime). Это делается из-за того, что столбцы, к которым происходит обращение, называются так же, как имена свойств объектов, соответствующих таблице, которые также используют горбатыйРегистр. Хотя при использованиии горбатогоРегистра наш PHP код выгядит более последовательно, это может вызвать проблемы регистрозависимости в некоторых СУБД. Например, PostgreSQL считает имя столбцов регистронезависимыми по умолчанию, и мы должны заключать имя столбца в кавычки в условиях запроса, если имя столбца имеет заглавные буквы. По этой причине, возможно, целесообразным было бы называть столбцы (а также таблицы), используя только буквы в нижнем регистре (например, create_time), чтобы избежать возможных проблем регистрозависимости.

3. Создание записи

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

$post=new Post;
$post->title='тестовый пост';
$post->content='содержимое тестового поста';
$post->createTime=time();
$post->save();

Если первичный ключ таблицы автоинкрементный, то после добавления экземпляр AR будет содержать обновленное значение первичного ключа. В примере выше, свойство id всегда будет содержать первичный ключ для новой записи.

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

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

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

$post=new Post;
$post->createTime=new CDbExpression('NOW()');
// $post->createTime='NOW()'; этот вариант работать не будет
// т.к. значение 'NOW()' будет воспринято как строка
$post->save();

Подсказка: Несмотря на то, что AR позволяет производить различные операции без написания громоздкого SQL, часто необходимо знать, какой SQL выполняется на самом деле. Этого можно добиться, включив журналирование. К примеру, чтобы вывести запросы SQL в конце каждой страницы, мы можем включить CWebLogRoute в настройках приложения. Начиная с версии 1.0.5, можно задать параметр 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()). Данный метод доступен с версии 1.0.9.

  • 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();
}
catch(Exception $e)
{
    $transaction->rollBack();
}

11. Именованные группы условий

Примечание: именованные группы условий поддерживаются, начиная с версии 1.0.5. Идея групп условий позаимствована у 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'=>'createTime DESC',
                'limit'=>5,
            ),
        );
    }
}

Каждая группа описывается массивом, который может быть использован для инициализации экземпляра CDbCriteria. К примеру, recently определяет, что условие order будет createTime DESC, а limit будет равен 5. Вместе эти условия означают, что будут выбраны 5 последних публикаций.

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

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

В общем случае, именованные группы условий должны располагаться левее вызова find. Каждая группа определяет критерий запроса, который совмещается с остальными критериями, включая переданные непосредственно методу find. Конечный результат получается применением к запросу набора фильтров.

Начиная с версии 1.0.6, именованные группы условий могут быть использованы в методах update и delete. К примеру, следующий код удалит все недавно опубликованные записи:

Post::model()->published()->recently()->delete();

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

Именованные группы условий с параметрами

Именованные группы условий могут быть параметризованы. К примеру, нам понадобилось задать число публикаций для группы recently. Чтобы это сделать, вместо того, чтобы описывать группу в методе CActiveRecord::scopes, нам необходимо описать новый метод с таким же именем, как у группы условий:

public function recently($limit=5)
{
    $this->getDbCriteria()->mergeWith(array(
        'order'=>'createTime 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 запросах.