База данных

Содержание...

Работа с базой данных реализует слой «модель» в терминологии MVC. В состав фреймворка входит абстрактный класс waModel для описания модели. Классы описания сущностей предметной области приложения наследуются от waModel.

Файлы классов модели размещаются в файловой структуре приложения в подкаталоге приложения или плагина lib/models/.

В описании модели есть только одно обязательное поле — название таблицы в базе данных в переменной $table. Пример класса описания модели:

<?php
class blogModel extends waModel
{
    protected $table = 'blog';
}

Модель инициализируется в коде приложения следующим образом:

$model = new blogModel();

В этом примере blog — это пример идентификатора приложения. После инициализации модель получает от базы данных описание указанной таблицы (с помощью запроса DESCRIBE $table) и кеширует его. На этом этапе определяется главный ключ (primary key) таблицы, определяются поля и их типы.

В коде контроллеров и экшенов все взаимодействие с базой данных происходит исключительно через классы моделей приложения:

// Создаем экземпляр модели для получения данных из БД
$model = new guestbookModel();
// Получаем записи гостевой книги из БД
$records = $model->order('datetime DESC')->fetchAll();

Класс waModel содержит ряд сервисных методов для автоматического построения SQL-запросов, что позволяет достичь независимости кода приложения от конкретного вида используемой СУБД.

Методы автоматического построения SQL-запросов

Запросы SELECT

// Получить запись по значению первичного ключа
$model->getById($id);

// Получить запись по значению одного из полей
$model->getByField('name', $name);  // возвращает первую найденную запись

// Если необходимо вернуть все записи в предыдущем примере,
// то нужно добавить в вызов метода третий параметр - true
$model->getByField('name', $name, true);

// Можно задать условия отбора по значению нескольких полей
// (в запросе они будут объединены через AND)
$model->getByField(['name' => $name, 'contact_id' => $contact_id]);

// Если необходимо провести отбор по нескольким возможным значениям одного поля,
// то вместо одной переменной задается массив, в этом случае при построении запроса
// будет использоваться оператор IN
$model->getByField('name' , [$name1, $name2]);

// Агрегатные запросы типа SELECT COUNT(*) ... WHERE ... создаются методом countByField
// Параметры вызова метода полностью аналогичны getByField
$model->countByField($field, $value);
$model->countByField(['name' => $name, 'contact_id' => $contact_id]);
$model->countByField('name' , [$name1, $name2]);

Запросы UPDATE

// Обновить запись по значению первичного ключа
$model->updateById($id, $data); // $data - ассоциативный массив значений, которые нужно обновить
$model->updateById($id, ['field_1' => $field_1, 'field_2' => $field_2]);

// По аналогии с методом getByField также доступен метод updateByField,
// которому в качестве третьего параметра передается массив обновляемых значений:
$model->updateByField('contact_id', $contact_id, ['published' => true]);

Запросы DELETE

// По аналогии с запросами UPDATE доступны следующие два метода:
$model->deleteById($id);
$model->deleteByField($field, $value);

Запросы INSERT и REPLACE

// Для вставки/замены записи доступны методы:
// Оба эти метода вставляют только одну запись
$model->insert($data); // в случае auto_increment метод вернет id вставленной записи
$model->replace($data);

Выполнение SQL-запросов, записанных в явном виде

Во фреймворке доступны два основных метода модели для выполнения SQL-запросов — query и exec. Первым параметром они принимают SQL-запрос, а вторым — необязательный массив значений для плейсхолдеров.

Пример:

$model->query("SELECT * FROM " . $this->table . " WHERE id = i:id", ['id' => $id]);

Пояснения к примеру:

  • i:id — это именованный плейсхолдер, в котором i указывает на тип Integer (то есть значение, полученное из массива, будет приведено к целочисленному типу);
  • id — ключ в массиве значений;
  • ['id' => $id] — массив значений.

Доступные для использования типы значений плейсхолдеров:

  • 's' — String
  • 'i' — Integer
  • 'b' — Boolean
  • 'f' — Float/Double

Если тип плейсхолдера явно не указан (например, в запросе просто используется :id, то по умолчанию используется тип String). Использование плейсхолдеров упрощает построение запросов, а главное, исключает возможность написания небезопасных SQL-запросов, подверженных SQL-инъекциям.

Использование плейсхолдеров не является обязательным, однако в случае отказа от них разработчик должен самостоятельно позаботиться о подстановке данных в запрос. Здесь может быть полезным метод модели escape, который экранирует строки (при использовании СУБД MySQL его работа аналогична результату выполнения функции mysql_real_escape_string):

$model->query("SELECT * FROM " . $this->table . " WHERE id = " . (int)$id);
$model->query("SELECT * FROM " . $this->table . " WHERE name LIKE '" . $this->escape($name) . "'");

Единственное отличие между методами query и exec заключается в результате, который они возвращают.

Подробнее об использовании разных видов плейсхолдеров — в описании класса waModel ›

Метод query($sql, $params = null)

Метод выполняет SQL-запрос и возвращает объект результата. Тип возвращаемого объекта зависит от типа запроса:

  • для SELECT-запросов возвращается объект типа waDbResultSelect,
  • для INSERT-запросов — waDbResultInsert
  • для UPDATE-запросов — waDbResultUpdate
  • для DELETE-запросов — waDbResultDelete

Запросы SELECT

Возвращается объект результата типа waDbResultSelect, который можно использовать, например, в цикле foreach. Примеры работы с waDbResultSelect:

$result = $model->query("SELECT * FROM ".$this->table." WHERE text LIKE '%тест%'");
// Получаем количество записей в результате (аналогично функции mysql_num_rows)
$result->count();

// Проходим по всем записям результата в цикле:
foreach ($result as $row) {
    ...
}

// Получить все записи в массив
$data = $result->fetchAll();

// Если в методе fetchAll указать первым параметром название поля, то результатом будет
// ассоциативный массив с ключами, равными значению этого поля
$data = $result->fetchAll('id');

// Если известно, что в результате только одна запись, либо если нужно получить данные
// только одной записи, то будут полезны следующие методы:
// аналог mysql_fetch_assoc
$result->fetch();

// выполняет fetch и возвращает значение по переданному ключу, либо значение первого
// элемента массива, если ключ не указан
$result->fetchField('id');

// например, так можно получить результат запроса COUNT
$n = $model->query("SELECT count(*) FROM ".$this->table." WHERE ...")->fetchField();

Запросы DELETE и UPDATE

Возвращается объект результата типа waDbResultDelete и waDbResultUpdate.

$result = $model->query("UPDATE ".$this->table." SET name = 'no name' WHERE name = ''");
// Получить количество затронутых записей (функция mysql_affected_rows)
$result->affectedRows()

Запросы INSERT

Возвращается объект типа waDbResultInsert.

// Получить значение первичного ключа созданной записи в случае
// auto_increment(функция mysql_insert_id)
$result->lastInsertId()

Метод exec($sql, $params = null)

Метод выполняет SQL-запрос и возвращает результат, полученный от адаптера работы с БД (в случае с MySQL возвращается результат выполнения функции mysql_query). Этот метод стоит использовать в тех случаях, когда результат выполнения запроса не нужен или достаточно проверить, успешно ли выполнился запрос.

Конструктор SELECT-запросов

SELECT-запросы можно строить с помощью простого конструктора запросов, например:

// Выбрать все записи отсортированные по полю datetime:
$records = $model->order('datetime DESC')->fetchAll();

// Получить ассоциативный массив id => name
$data = $model->select('id, name')->fetchAll('id', true);

// В текущей версии доступны методы select, where, order
$model
    ->select('id, name, datetime')
    ->where('contact_id = ' . (int)$contact_id)
    ->order('datetime DESC')
    ->fetchAll('id');

// Предыдущий пример выполняет то же самое, что и следующий:
$model->query("SELECT id, name, datetime WHERE contact_id = " . (int)$contact_id . " ORDER BY datetime DESC")->fetchAll('id');

Методы конструктора возвращают экземпляр класса waDbQuery с публичным методом getQuery() — с его помощью удобно проверить, какой SQL-запрос сформировал конструктор. Для этого нужно заменить метод получения результата — fetch(), fetchField(), fetchAssoc(), fetchAll() — на метод getQuery():

$sql = $model
    ->select('id, name, datetime')
    ->where('contact_id = ' . (int)$contact_id)
    ->order('datetime DESC')
    ->getQuery();

echo $sql;
// SELECT id, name, datetime FROM myapp_table_name WHERE contact_id = 42 ORDER BY datetime DESC

Рекомендации по реализации дополнительных методов модели в приложении

Чтобы во всех местах приложения не писать один и тот же код (напрямую используя базовые методы модели), можно в модели вашего приложения объявить методы для реализации часто используемых операций.

Пример:

// метод, возвращающий все записи контакта
public function getByContact($contact_id)
{
    return $this->getByField('contact_id', $contact_id, true);
}

Все методы для работы с одной таблицей реализуются в соответствующем классе модели. В дальнейшем вы можете усовершенствовать эти методы модели (например добавлять кеширование).

Рекомендуется большинство операций с конкретной таблицей БД реализовывать в собственных методах модели, не привязанных к названию полей. Это позволит в случае рефакторинга данной таблицы внести изменения лишь в один файл — в код класса модели.

Подключение к другим базам данных

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

Для подключения к другой базе данных необходимо при создании экземпляра модели в качестве аргумента конструктора указать параметры подключения. Сделать это можно несколькими способами.

Создание постоянной конфигурации подключения

Если вы планируете многократно использовать в вашем приложении подключение к другой (какой-то определенной) базе данных, добавьте параметры подключения в конфигурационный файл фреймворка wa-config/db.php в виде дополнительного массива, как показано в примере:

<?php
return [
    'default' => [
        'host' => 'localhost',
        'user' => 'user1',
        'password' => '12345678',
        'database' => 'webasyst',
        'type' => 'mysqli',
    ],
    'extra' => [
        'host' => 'localhost',
        'user' => 'user2,
        'password' => '87654321',
        'database' => 'wordpress',
        'type' => 'mysqli',
    ],
];

В приведенном выше примере добавлена новая запись с параметрами подключения, условно обозначенная как extra. Для подключения к базе данных wordpress (в данном примере) необходимо в PHP-коде приложения создать экземпляр модели следующим образом:

$extra_model = new waModel('extra');

При использовании методов созданной таким образом модели обращение будет выполняться только к базе данных, имя которой указано в соответствующей конфигурации.

Динамическое указание параметров подключения

Если использовать постоянное подключение к какой-то заранее известной базе данных не планируется, то при создании экземпляра модели параметры подключения можно указать явно — в виде массива данных. Пример создания модели в PHP-коде приложения таким способом показан ниже:

$extra_connection = [
    'host' => 'localhost',
    'user' => 'user2',
    'password' => '87654321',
    'database' => 'somedb',
    'type' => 'mysqli',
];

$extra_model = new waModel($extra_connection);

Оба приведенных выше способа подключения к другой базе данных применимы не только к экземпляру базового класса waModel, но и к экземплярам классов моделей вашего приложения (например, myappSomeModel):

$extra_model = new myappSomeModel('extra');

либо

//массив $extra_connection должен быть заполнен данными подключения к БД
$extra_model = new myappSomeModel($extra_connection);

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

Для подключения к базам данных с различной структурой рекомендуется создавать отдельные классы моделей. Это позволит упростить структуру вашего приложения и облегчит отладку программного кода.