Тестирование бд. Как тестировать и отлаживать базы данных

Laravel предоставляет множество полезных инструментов для тестирования ваших приложений, использующих БД. Во-первых, вы можете использовать вспомогательный метод PHP seeInDatabase () для проверки того, что данные в БД соответствуют определённому набору критериев. Например, если вы хотите проверить, что в таблице users есть запись с полем email равным [email protected] , вы можете сделать следующее:

PHP
{
// Осуществление вызова приложения...

$this -> seeInDatabase ("users" , [
"email" => "[email protected]"
]);
}

Само собой, такие методы как PHP seeInDatabase () созданы для удобства. Вы можете использовать любые встроенные методы проверки PHPUnit для дополнения своих тестов.

Сброс БД после каждого теста

Зачастую бывает полезно сбрасывать вашу БД после каждого теста, чтобы данные из предыдущего теста не влияли на последующие тесты.

Использование миграций

Один из способов сброса состояния БД - откатывать БД после каждого теста и мигрировать её перед следующим тестом. Laravel предоставляет простой типаж DatabaseMigrations , который автоматически сделает это для вас. Просто используйте этот типаж на классе вашего теста, и всё будет сделано за вас:

PHP




{
use DatabaseMigrations ;

/**
*
* @return void
*/

{
$this -> visit ("/" )
-> see ("Laravel 5" );
}
}

Использование транзакций

Другой способ сброса состояния БД - обернуть каждый тест-кейс в транзакцию БД. И для этого Laravel также предоставляет удобный типаж DatabaseTransactions , который автоматически сделает это для вас:

PHP

Use Illuminate \ Foundation \ Testing \ WithoutMiddleware ;
use Illuminate \ Foundation \ Testing \ DatabaseMigrations ;
use Illuminate \ Foundation \ Testing \ DatabaseTransactions ;

class ExampleTest extends TestCase
{
use DatabaseTransactions ;

/**
* Пример базового функционального теста.
*
* @return void
*/
public function testBasicExample ()
{
$this -> visit ("/" )
-> see ("Laravel 5" );
}
}

По умолчанию этот типаж будет оборачивать в транзакции только подключение к БД по умолчанию. Если ваше приложение использует несколько подключений к БД, вам надо определить свойство PHP $connectionsToTransact в классе вашего теста. Это свойство должно быть массивом имён подключений для выполнения транзакций над ними.

Создание фабрик

При тестировании вам может понадобиться вставить несколько записей в вашу БД перед выполнением теста. При создании этих данных Laravel позволяет вам вместо указания значений каждого столбца вручную определить стандартный набор атрибутов для каждой из ваших моделей Eloquent с помощью фабрик. Для начала посмотрите на файл database/factories/ModelFactory.php в вашем приложении. Изначально этот файл содержит определение одной фабрики:

PHP $factory -> define (App \ User ::class, function (Faker \ Generator $faker ) {
static $password ;

Return [
"name" => $faker -> name ,
"email" => $faker -> unique ()-> safeEmail ,
"password" => $password ?: $password = bcrypt ("secret" ),
"remember_token" => str_random (10 ),
];
});

В замыкании, которое служит определением фабрики, вы можете вернуть стандартные тестовые значения всех атрибутов модели. Замыкание получит экземпляр PHP-библиотеки Faker , которая позволяет вам удобно генерировать различные случайные данные для тестирования.

Само собой, вы можете добавить свои собственные дополнительные фабрики в файл ModelFactory.php . Также вы можете создать дополнительные файлы фабрик для каждой модели для более понятной организации. Например, вы можете создать файлы UserFactory.php и CommentFactory.php в вашей папке database/factories . Все файлы в папке factories будут автоматически загружены Laravel.

Состояния фабрик

Состояния позволяют вам определить отдельные изменения, которые можно применять к вашим фабрикам моделей в любых комбинациях. Например, ваша модель User может иметь состояние delinquent (нарушитель), которое изменяет стандартное значение одного из атрибутов. Вы можете определить свои преобразования состояния с помощью метода PHP state () :

PHP $factory -> state (App \ User ::class, "delinquent" , function ($faker ) {
return [
"account_status" => "delinquent" ,
];
});

Использование фабрик

Создание моделей

После определения фабрик вы можете использовать глобальную функцию PHP factory () в своих тестах или файлах начальных данных для генерирования экземпляров модели. Итак, давайте рассмотрим несколько примеров создания моделей. Во-первых, мы используем метод PHP make () для создания моделей, но не сохраним их в БД:

PHP public function testDatabase ()
{
$user = factory (App \ User ::class)-> make ();

Также вы можете создать коллекцию моделей или создать модели определённого типа:

PHP $users = factory (App \ User ::class, 3 )-> make ();

Также вы можете применить любые к моделям. Если вы хотите применить к моделям несколько изменений состояния, вам надо указать имя каждого состояния для применения:

PHP $users = factory (App \ User ::class, 5 )-> states ("delinquent" )-> make ();

$users = factory (App \ User ::class, 5 )-> states ("premium" , "delinquent" )-> make ();

Переопределение атрибутов

Если вы хотите переопределить некоторые из стандартных значений ваших моделей, вы можете передать массив значений в метод PHP make () . Будут заменены только указанные значения, а остальные будут заданы в соответствии с указанными в фабрике:

PHP $user = factory (App \ User ::class)-> make ([
"name" => "Abigail" ,
]);

Постоянные модели

Метод PHP create () не только создаёт экземпляры моделей, но также сохраняет их в БД с помощью метода Eloquent PHP save () :

PHP public function testDatabase ()
{
// Создание одного экземпляра App\User...
$user = factory (App \ User ::class)-> create ();

// Создание трёх экземпляров App\User...
$users = factory (App \ User ::class, 3 )-> create ();

// Использование модели в тестах...
}

Вы можете переопределить атрибуты модели, передав массив в метод PHP create () :PHP make ());
});

Отношения и атрибуты замыкания

Также вы можете прикреплять отношения к моделям с помощью атрибутов замыкания в определениях ваших фабрик. Например, если вы хотите создать новый экземпляр модели User при создании Post , то можете сделать так:

PHP $factory ->
return [
"title" => $faker -> title ,
"content" => $faker -> paragraph ,
"user_id" => function () {
return factory (App \ User ::class)-> create ()-> id ;
}
];
});

Это замыкание также получает определённый массив атрибутов фабрики, которая его содержит:

PHP $factory -> define (App \ Post ::class, function ($faker ) {
return [
"title" => $faker -> title ,
"content" => $faker -> paragraph ,
"user_id" => function () {
return factory (App \ User ::class)-> create ()-> id ;
},
"user_type" => function (array $post ) {
return App \ User :: find ($post [ "user_id" ])-> type ;
}
];
});

Многие примеры модульного тестирования начального и среднего уровня на любом языке программирования предполагают, что с помощью простых тестов можно легко протестировать логику приложения. Для приложений, ориентированных на базы данных, это далеко от реальности. При начале использования, например, WordPress, TYPO3 или Symfony с Doctrine или Propel, вы легко столкнётесь с серьёзными проблемами с PHPUnit: просто потому, что база данных тесно связана с этими библиотеками.

Убедитесь, что у вас PHP-расширение pdo и расширения для баз данных, например pdo_mysql , установлены. В противном приведённые ниже примеры не будут работать.

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

  1. Метод, который вы хотите протестировать довольно большую операцию JOIN и затем использует полученные данные для вычисления некоторых важных результатов.
  2. В вашей бизнес-логике выполняются целый рад операторов SELECT, INSERT, UPDATE и DELETE.
  3. Вам необходимо настроить тестовые данные (возможно, значительное количество) в более двух таблиц для получения подходящих первоначальных данных для тестируемых методов.

Расширение DbUnit значительно упрощает настройку базы данных для целей тестирования и позволяет проверять содержимое базы данных после выполнения ряда операций.

Поддерживаемые поставщики для тестирования баз данных

В настоящее время DbUnit поддерживает MySQL, PostgreSQL, Oracle и SQLite. За счёт интеграции в Zend Framework или Doctrine 2 это расширение имеет доступ к другим системам управления баз данных (СУБД), таким как IBM DB2 или Microsoft SQL Server.

Трудности при тестировании баз данных

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

  • Схема и таблицы базы данных
  • Вставка строк, необходимых для теста, в эти таблицы
  • Проверка состояния базы данных после того, как тест был пройден
  • Очистка базы данных для каждого нового теста

Поскольку многие API баз данных, такие как PDO, MySQLi или OCI8, громоздкие в использовании и многословные при написании, выполнение этих шагов вручную может стать настоящим кошмаром.

Тестовый код должен быть как можно более коротким и точным по нескольким причинам:

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

Кроме того, вы должны понимать, что база данных по существу является глобальной переменной, вставленной в ваш код. Два теста в вашем тестовом наборе могут работать с одной и той же базой данных, и, возможно, повторно использовать эти данные несколько раз. Неудачи в одном тесте могут легко повлиять на результат последующих тестов, тем самым затрудняя процесс тестирования. Ранее упомянутый этап очистки имеет большое значение для решения проблемы «база данных - глобально введённая переменная».

DbUnit помогает упростить все эти проблемы при тестировании с базой данных элегантным способом.

С чем PHPUnit вам точно не сможет помочь, так это то, что тесты, использующие базу данных, значительно медленнее по сравнению с тестами, которые её не используют. В зависимости от того, насколько велико взаимодействие с базой данных, выполнение ваших тестов может занять значительное количество времени. Однако, если вы храните небольшой объём данных, используемый для каждого теста и пытаетесь протестировать как можно больше кода, который не взаимодействует с базой данных, то на выполнение всех тестов займёт около одной минуту, даже на больших наборов тестов.

Например, набор тестов проекта Doctrine 2 в настоящее время содержит около 1000 тестов, где почти половина из которых использует базу данных и при этом всём выполнение тестов укладывается в 15 секунд, используя базу данных MySQL на стандартом настольном компьютере.

Четыре этапа теста базы данных

В своей книге «Шаблоны тестирования xUnit» (xUnit Test Patterns) Джерард Месарош (Gerard Meszaros) перечисляет четыре этапа (стадии) модульного тестирования:

    Настройка фикстуры

    Выполнение системы тестирования (System Under Test)

    Проверка результата

    Очистка (teardown)

    Что такое фикстура?

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

Тестирование базы данных требует, по крайней мере, установки и очистки, чтобы очистить и записать необходимые данные фикстуры в ваши таблицы. Тем не менее, у расширения базы данных есть веские основания для возврата к четырём этапам при тестировании, использующем базу данных для формирования рабочего процесса, выполняемого для каждого из тестов:

1. Очистка базы данных

Поскольку всегда есть первый тест, который работает с базой данных, вы точно не знаете, есть ли в таблицах уже какие-нибудь данные. PHPUnit выполнит операцию TRUNCATE для всех таблиц, чтобы вернуть их в пустое состояние.

2. Настройка фикстуры

Затем PHPUnit выполнит итерацию по всем указанным строкам фикстуры и вставит их в соответствующие таблицы.

3–5. Запуск теста, проверка результата и очистка

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

В вашем тесте используйте специальное утверждение assertDataSetsEqual() для целей проверки, однако, это совершенно необязательно. Эта возможность будет объяснена в разделе «Утверждения базы данных».

Конфигурация PHPUnit Database TestCase

Обычно при использовании PHPUnit ваши тесты наследуются от PHPUnit\Framework\TestCase следующим образом:

assertSame(2, 1 + 1); } }

Если вы хотите протестировать код, который использует базу данных, установка такого теста будет немного посложнее, потому что вам нужно отнаследоваться от другого абстрактного класса TestCase, требующего реализацию двух абстрактных методов getConnection() и getDataSet() :

createDefaultDBConnection($pdo, ":memory:"); } /** * @return PHPUnit\DbUnit\DataSet\IDataSet */ public function getDataSet() { return $this->createFlatXMLDataSet(dirname(__FILE__)."/_files/guestbook-seed.xml"); } }

Реализация getConnection()

Для работы функциональности очистки и загрузки фикстур, расширение базы данных PHPUnit требует доступа к соединению с базой данных, которое абстрагируется между поставщиками и библиотекой PDO. Важно отметить, что ваше приложение необязательно должно основываться на PDO для использования расширения базы данных PHPUnit, подключение просто используется для очистки и настройки фикстуры.

В предыдущем примере мы создаём подключение SQLite в памяти и передаём его в метод createDefaultDBConnection , который оборачивает экземпляр PDO и второй параметр (имя базы данных) в очень простой уровень абстракции с базой данных типа PHPUnit\DbUnit\Database\Connection .

Раздел «Использование API подключения к базе данных» объясняет API этого интерфейса и то, как вы можете наилучшим образом его использовать.

Реализация getDataSet()

Метод getDataSet() определяет, каким должно быть первоначальное состояние базы данных перед выполнением каждого теста. Состояние базы данных абстрагируется с помощью двух концепций - DataSet и DataTable, которые представлены интерфейсами PHPUnit\DbUnit\DataSet\IDataSet и PHPUnit\DbUnit\DataSet\IDataTable соответственно. В следующем разделе будет подробно описано, как эти концепции работают и в чём их преимущества при использовании их в тестировании базы данных.

Для реализации нам нужно только знать, что метод getDataSet() вызывается только один раз во время setUp() для извлечения набора данных фикстуры и вставки его в базу данных. В этом примере мы используем фабричный метод createFlatXMLDataSet($filename) , который представляет собой набор данных на основе XML-представления.

Как насчёт схемы базы данных (Database Schema, DDL)?

PHPUnit предполагает, что схема база данных со всеми её таблицами, триггерами, последовательностями и представлениями создаётся до запуска теста. Это означает, что вы как разработчик должны убедиться, что ваша база данных правильно настроена перед выполнением набора тестов.

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

  1. Если вы используете базу данных с постоянным соединением (не SQLite в оперативной памяти), вы можете легко настроить базу данных один раз с помощью таких инструментов, как phpMyAdmin для MySQL, и повторно использовать базу данных при каждом запуске теста.
  2. Если вы используете такие библиотеки как Doctrine 2 или Propel , вы можете использовать их API для создания схемы базы данных, который понадобиться всего один раз до запуска тестов. Вы можете использовать возможности первоначальной (bootstrap) загрузки PHPUnit и конфигурации для выполнения этого кода каждый раз при выполнении тестов.

PHPUnit Database TestCase

Из предыдущего примера реализации вы легко можете увидеть, что метод getConnection() довольно статичен и может повторно использован в различных тестовых классов с использованием базы данных. Кроме того, чтобы повысить производительность тестов и снизить накладные расходы, связанные с базой данных, вы можете немного провести рефакторинг кода для создания общего абстрактного класса для тестов вашего приложения, который по-прежнему всё ещё позволяет указать другую фикстуру с данными для каждого теста:

conn === null) { if (self::$pdo === null) { self::$pdo = new PDO("sqlite::memory:"); } $this->conn = $this->createDefaultDBConnection(self::$pdo, ":memory:"); } return $this->conn; } }

Однако это соединение с базой данных жёстко закодировано в соединении PDO. PHPUnit имеет одну удивительную возможность, которая поможет сделать этот тестовый класс ещё более универсальным. Если вы используете XML-конфигурацию, вы можете сделать подключение к базе данных настраиваемым для каждого запуска теста. Сначала давайте создадим файл «phpunit.xml» в тестовом каталоге tests/ приложения со следующим содержимым:

Теперь мы можем изменить тестовый класс, чтобы он выглядел так:

conn === null) { if (self::$pdo === null) { self::$pdo = new PDO($GLOBALS["DB_DSN"], $GLOBALS["DB_USER"], $GLOBALS["DB_PASSWD"]); } $this->conn = $this->createDefaultDBConnection(self::$pdo, $GLOBALS["DB_DBNAME"]); } return $this->conn; } }

Теперь мы можем запустить набор тестов базы данных, используя различные конфигурации из интерфейса командной строки:

$ user@desktop> phpunit --configuration developer-a.xml MyTests/ $ user@desktop> phpunit --configuration developer-b.xml MyTests/

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

Понимание DataSets и DataTables

Ключевой концепцией расширения базы данных PHPUnit являются DataSets и DataTables. Вы должны попытаться понять эту простую концепцию для освоения тестирования с использованием базы данных с помощью PHPUnit. DataSet и DataTable - это уровни абстракции вокруг строк и столбцов баз данных. Простой API скрывает основное содержимое базы данных в структуре объекта, который также может быть реализован другими источниками, отличными от базы данных.

Эта абстракция необходима для сравнения текущего содержимого базы данных с ожидаемым. Ожидаемое содержимое может быть представлено в виде файлов формата XML, YAML, CSV или массива PHP, например. Интерфейсы DataSet и DataTable позволяют сравнивать эти концептуально разные источники путём эмуляции хранилища реляционных баз данных в семантически подобном подходе.

Рабочий процесс для утверждений базы данных в ваших тестах, таким образом, состоит из трёх простых шагов:

  • Указать одну или более таблиц в базе данных по имени таблицы (фактический набор данных)
  • Указать ожидаемый набор данных в предпочтительном формате (YAML, XML, ..)
  • Проверить утверждение, что оба представления набора данных равны друг другу (эквивалентны).

Утверждения это не единственный вариант использования для DataSet и DataTable в расширении базы данных PHPUnit. Как показано в предыдущем разделе, они также описывают первоначальное содержимое базы данных. Вы вынуждены определять набор данных фикстуры в Database TestCase, который затем используется для:

  • Удаления всех строк из таблиц, указанных в наборе данных.
  • Записи всех строк в таблицы данных в базе данных.

Доступные реализации

Существует три различных типов наборов данных/таблиц данных:

  • DataSets и DataTables на основе файлов
  • DataSet и DataTable на основе запросов
  • Фильтр и объединение DataSets и DataTables

Файловые наборы данных и таблиц обычно используются для первоначальной фикстуры и описывают ожидаемое состояние базы данных.

Flat XML DataSet

Наиболее распространённый набор называется Flat XML. Это очень простой (flat) XML-формат, где тег внутри корневого узла представляет ровно одну строку в базе данных. Имена тегов соответствуют таблице, куда будут добавляться строки (записи), а атрибуты тега представляют столбцы записи. Пример для приложения простой гостевой книги мог бы выглядеть подобным образом:

Это, очевидно, легко писать. В этом примере - имя таблицы, в которую добавляются две строки с четырьмя столбцами «id», «content», «user» и «created» с соответствующими им значениями.

Однако за эту простоту приходиться платить.

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

Обработка значений NULL в простых наборах данных XML утомительна. Значение NULL отличается от пустого строкового значения почти в любой базе данных (Oracle - исключение), что трудно описать в обычном формате XML. Вы можете представить значение NULL, опуская атрибут из строки (записи). Если наша гостевая книга разрешает анонимные записи, представленные значением NULL в столбце «user», гипотетическое состояние таблицы гостевой книги может быть таким:

В нашем случае вторая запись добавлена анонимна. Однако это приводит к серьёзной проблеме определения столбцов. Во время утверждений о равенстве данных каждый набор данных должен указывать, какие столбцы хранятся в таблице. Если атрибут указан NULL для всех строк таблицы данных, как расширение базы данных определит, что столбец должен быть частью таблицы?

Обычный набор данных XML делает сейчас решающе важное предположение, объявляя, что атрибуты в первой определённой строке таблицы определяют столбцы этой таблицы. В предыдущем примере это означало бы, что «id», «content“, «user» и «created» будет столбцами таблицы гостевой книги. Для второй строки, где пользователь («user») не определён, в базу данных в столбец «user» будет вставлено значение NULL.

Когда первая запись гостевой книги удаляется из набора данных, только «id», «content» и «created» будут столбцами таблицы гостевой книги, поскольку столбец «user» не определён.

Чтобы эффективно использовать набор данных Flat XML, когда значения NULL имеют важное значение, первая строка каждой таблицы не должна содержать значения NULL, и только последующие строки могут пропускать атрибуты. Это может быть неудобно, поскольку порядок строк является значимым фактором для утверждений базы данных.

В свою очередь, если вы укажете только подмножество столбцов таблицы в наборе данных Flat XML, все пропущенные значения будут установлены в значения по умолчанию. Это приведёт к ошибкам, только если один из пропущенных столбцов определён как «NOT NULL DEFAULT NULL».

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

Вы можете создать экземпляр обычного набора данных XML внутри Database TestCase, вызвав метод createFlatXmlDataSet($filename) :

createFlatXmlDataSet("myFlatXmlFixture.xml"); } }

XML DataSet

Есть ещё один структурированный набор данных XML, который немного более многословный при записи, но не имеет проблем с NULL-значениями из набора данных Flat XML. Внутри корневого узла вы можете указать теги

, , , и . Эквивалентный набор данных для ранее определённой гостевой книги с использованием Flat XML, будет выглядеть так:

idcontentusercreated 1 Привет, дружище! joe 2010-04-24 17:15:23 2 Мне нравится это! 2010-04-26 12:14:20

Любой определённый тег

имеет имя и требует определение всех столбцов с их именами. Он может содержать ноль или любое положительное число вложенных элементов . Отсутствие элементов ``означает, что таблица пуста. Теги `` и должны быть указаны в порядке, ранее заданных элементов . Тег , очевидно, означает, что значение равно NULL.

Вы можете создать экземпляр набора данных XML внутри Database TestCase, вызвав метод createXmlDataSet($filename) :

createXMLDataSet("myXmlFixture.xml"); } }

MySQL XML DataSet

Этот новый XML-формат специально предназначен для сервера баз данных MySQL . Его поддержка была добавлена в PHPUnit 3.5. Файлы в этом формате могут быть сгенерированы с помощью утилиты mysqldump . В отличие от наборов данных CSV, которые mysqldump также поддерживает, один файл в этом XML-формате может содержать данные для нескольких таблиц. Вы можете создать файл в этом формате, запустив mysqldump следующим образом:

$ mysqldump --xml -t -u --password= > /path/to/file.xml

Этот файл можно использовать в вашем Database TestCase, путём вызова метода createMySQLXMLDataSet($filename) :

createMySQLXMLDataSet("/path/to/file.xml"); } }

YAML DataSet

Кроме того, вы можете использовать набор данных YAML для примера гостевой книги:

guestbook: - id: 1 content: "Привет, дружище!" user: "joe" created: 2010-04-24 17:15:23 - id: 2 content: "Мне нравится это!" user: created: 2010-04-26 12:14:20

Этот формат прост и удобен, а главное он решает проблему с NULL в похожем наборе данных Flat XML. NULL в YAML - это просто имя столбца без указанного значения. Пустая строка указывается таким образом - column1: "" .

В настоящее время набор данных YAML не имеет фабричного метода в Database TestCase, поэтому вам необходимо создать его самим:

CSV DataSet

Ещё один файловый набор данных на основе формата CSV. Каждая таблица набора данных представлена одним CSV-файлом. Для нашего примера с гостевой книгой мы определяем файл guestbook-table.csv:

Id,content,user,created 1,"Привет, дружище!","joe","2010-04-24 17:15:23" 2,"Мне нравится это!","nancy","2010-04-26 12:14:20"

Хотя это очень удобно для редактирования через Excel или OpenOffice, вы не можете указать значения NULL в наборе данных CSV. Пустой столбец приведёт к тому, что в столбец в базе данных будет вставлено пустое значение.

Вы можете создать CSV DataSet следующим образом:

addTable("guestbook", dirname(__FILE__)."/_files/guestbook.csv"); return $dataSet; } }

Array DataSet

В расширении базы данных PHPUnit не существует (пока) массива на основе DataSet, но мы может легко реализовать свой собственный. Пример гостевой книги должен выглядеть так:

[ [ "id" => 1, "content" => "Привет, дружище!", "user" => "joe", "created" => "2010-04-24 17:15:23" ], [ "id" => 2, "content" => "Мне нравится это!", "user" => null, "created" => "2010-04-26 12:14:20" ], ], ]); } }

DataSet PHP имеет очевидные преимущества перед всеми другими наборами данных на основе файлов:

  • Массивы PHP, очевидно, могут обрабатывать значения NULL .
  • Вам не нужны дополнительные файлы для утверждений, и вы можете непосредственно использовать их в TestCase.

Чтобы этот набор выглядел как Flat XML, CSV или YAML, ключи первой указанной строки определяют имена столбцов таблицы, в предыдущем случае это были бы «id», «content», «user» и «created».

Реализация массива DataSet проста и понятна:

$rows) { $columns = ; if (isset($rows)) { $columns = array_keys($rows); } $metaData = new DefaultTableMetaData($tableName, $columns); $table = new DefaultTable($metaData); foreach ($rows as $row) { $table->addRow($row); } $this->tables[$tableName] = $table; } } protected function createIterator($reverse = false) { return new DefaultTableIterator($this->tables, $reverse); } public function getTable($tableName) { if (!isset($this->tables[$tableName])) { throw new InvalidArgumentException("$tableName не является таблицей в текущей базе данных."); } return $this->tables[$tableName]; } }

Query (SQL) DataSet

Для утверждений базы данных вам нужен не только набор данный на основе файлов, но также набор данных на основе запросов (Query)/SQL, содержащий фактическое содержимое базы данных. Здесь показан Query DataSet:

getConnection()); $ds->addTable("guestbook");

Добавление таблицы просто по имени - это неявный способ определения таблицы данных со следующим запросом:

getConnection()); $ds->addTable("guestbook", "SELECT * FROM guestbook");

Вы можете использовать его, указав произвольные запросы для своих таблиц, например, ограничивая количество строк, столбцов или добавление предложение ORDER BY :

getConnection()); $ds->addTable("guestbook", "SELECT id, content FROM guestbook ORDER BY created DESC");

В разделе «Утверждения базы данных» будет приведена подробная информация о том, как использовать Query DataSet.

Database (DB) Dataset

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

Вы можете либо создать набор данных для полной базы данных, как показано в testGuestbook() , либо ограничится набором указанных имён таблиц с помощью белого списка, как показано в методе testFilteredGuestbook() .

createDefaultDBConnection($pdo, $database); } public function testGuestbook() { $dataSet = $this->getConnection()->createDataSet(); // ... } public function testFilteredGuestbook() { $tableNames = ["guestbook"]; $dataSet = $this->getConnection()->createDataSet($tableNames); // ... } }

Замена DataSet

Я говорил о проблемах с NULL в наборах данных Flat XML и CSV, но есть несколько сложное обходное решение для получения обоих наборов данных, работающих с NULL.

Замена DataSet - декоратор для существующего набора данных, позволяющий заменять значения в любом столбце набора данных другим заменяющим значением. Для получения примера нашей гостевой книги, работающим со значениями NULL, мы указываем файл следующим образом:

Затем мы оборачиваем Flat XML DataSet в Replacement DataSet:

createFlatXmlDataSet("myFlatXmlFixture.xml"); $rds = new PHPUnit\DbUnit\DataSet\ReplacementDataSet($ds); $rds->addFullReplacement("##NULL##", null); return $rds; } }

DataSet Filter

Если у вас большой файл фикстуры, вы можете использовать фильтрацию набора данных для создания белого и чёрного списка таблиц и столбцов, которые должны содержаться поднаборе. Это особенно удобно в сочетании с DB DataSet для фильтрации столбцов набора данных.

getConnection()->createDataSet(); $filterDataSet = new PHPUnit\DbUnit\DataSet\DataSetFilter($dataSet); $filterDataSet->addIncludeTables(["guestbook"]); $filterDataSet->setIncludeColumnsForTable("guestbook", ["id", "content"]); // .. } public function testExcludeFilteredGuestbook() { $tableNames = ["guestbook"]; $dataSet = $this->getConnection()->createDataSet(); $filterDataSet = new PHPUnit\DbUnit\DataSet\DataSetFilter($dataSet); $filterDataSet->addExcludeTables(["foo", "bar", "baz"]); // only keep the guestbook table! $filterDataSet->setExcludeColumnsForTable("guestbook", ["user", "created"]); // .. } }

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

Составной DataSet

Составной DataSet очень полезен для объединения (агрегирования) нескольких уже существующих наборов данных в один набор данных. Когда несколько наборов данных содержат одну и ту же таблицу, строки добавляются в указанном порядке. Например, если у нас есть два набора данных - fixture1.xml :

и fixture2.xml :

Используя составной DataSet, мы можем объединить оба файла фикстуры:

createFlatXmlDataSet("fixture1.xml"); $ds2 = $this->createFlatXmlDataSet("fixture2.xml"); $compositeDs = new PHPUnit\DbUnit\DataSet\CompositeDataSet(); $compositeDs->addDataSet($ds1); $compositeDs->addDataSet($ds2); return $compositeDs; } }

Остерегайтесь внешних ключей

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

Реализация собственного DataSets/DataTables

Для понимания внутренностей DataSets и DataTables, давайте взглянем на интерфейс DataSet. Вы можете пропустить эту часть, если не планируете реализовать собственный DataSet или DataTable.

Общедоступный интерфейс используется внутри утверждения assertDataSetsEqual() в Database TestCase для проверки качества набора данных. Из интерфейса IteratorAggregate IDataSet наследует метод getIterator() для итерации по всем таблицах набора данных. Обратный итератор позволяет PHPUnit очистить строки таблицы, противоположные порядку их создания для удовлетворения ограничений внешнего ключа.

В зависимости от реализации применяются различные подходы для добавления экземпляров таблиц в набор данных. Например, таблицы добавляются внутри структуры во время создания из исходного файла во все файловые наборы данных, таких как YamlDataSet , XmlDataSet или FlatXmlDataSet .

Таблица также представлена следующим интерфейсом:

За исключением метода getTableMetaData() , который говорит сам за себя. Используемые методы необходимы для различных утверждений расширения базы данных, которые поясняются в следующей главе. Метод getTableMetaData() должен возвращать реализацию интерфейса PHPUnit\DbUnit\DataSet\ITableMetaData , который описывает структуру таблицы. В нём содержится следующая информация:

  • Имя таблицы
  • Массив имён столбцов таблицы, упорядоченных по их появлению в результирующем наборе.
  • Массив столбцов первичных ключей.

Этот интерфейс также имеет утверждение, которое проверяет, равны ли два экземпляра табличных метаданных (Table Metadata) друг другу, которое используется утверждением равенства набора данных.

Использование API подключения к базе данных

В интерфейсе Connection есть три интересных метода, которые необходимо вернуть из метода getConnection() в Database TestCase:

  1. Метод createDataSet() создаёт набор данных базы данных (Database (DB) DataSet), как описано в разделе реализации DataSet.
getConnection()->createDataSet(); } }

2. Метод createQueryTable() может использоваться для создания экземпляров QueryTable, передавая им имя результат и SQL-запроса. Это удобный метод, когда дело доходит до утверждений результата/таблицы, как будет показано в следующем разделе «API утверждений базы данных».

getConnection()->createQueryTable("guestbook", "SELECT * FROM guestbook"); } }

3. Метод getRowCount() - это удобный способ получения доступа к количеству строк в таблице, необязательно отфильтрованное дополнительным предложением where. Это можно использовать с простым утверждением равенства:

assertSame(2, $this->getConnection()->getRowCount("guestbook")); } }

API утверждений базы данных

Инструмент тестирования расширения базы данных, безусловно, содержит утверждения, которые вы можете использовать для проверки текущего состояния базы данных, таблиц и подсчёта строк таблиц. В этом разделе подробно описывается эта функциональность:

Утверждение количество строк таблицы

Часто бывает полезно проверить, содержит ли таблица определённое количество строк. Вы можете легко достичь этого без дополнительного кода, используя API Connection. Предположим, мы хотим проверить, что после вставки строк в нашу гостевую книгу мы имеем не только две первоначальные записи, которые были во всех предыдущих примерах, но а также третью, только что добавленную:

assertSame(2, $this->getConnection()->getRowCount("guestbook"), "Pre-Condition"); $guestbook = new Guestbook(); $guestbook->addEntry("suzy", "Hello world!"); $this->assertSame(3, $this->getConnection()->getRowCount("guestbook"), "Inserting failed"); } }

Утверждение состояния таблицы

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

Для этого нам нужно определить экземпляр таблицы запроса (Query Table), который выводит содержимое по имени таблицы и SQL-запроса и сравнивает его с набором данных на основе файлов/массивов:

addEntry("suzy", "Hello world!"); $queryTable = $this->getConnection()->createQueryTable("guestbook", "SELECT * FROM guestbook"); $expectedTable = $this->createFlatXmlDataSet("expectedBook.xml") ->getTable("guestbook"); $this->

Теперь для этого утверждения мы должны создать обычный XML-файл expectedBook.xml :

Это утверждение будет успешным только в том случае, если оно будет запущено точно в 2010–05–01 21:47:08 . Даты представляют собой особую проблему при тестировании с использованием базы данных, и мы может обойти эту ошибку, опуская столбец «created» в утверждении.

Скорректированный файл Flat XML expectedBook.xml , вероятно, теперь должен выглядеть следующим образом для прохождения утверждения:

Мы должны исправить вызов таблицы запроса (Query Table):

getConnection()->createQueryTable("guestbook", "SELECT id, content, user FROM guestbook");

Утверждение результата запроса

Вы также можете утверждать результат сложных запросов с помощью подхода Query Table, просто указав имя результата с запросом и сравнивая его с набором данным:

getConnection()->createQueryTable("myComplexQuery", "SELECT complexQuery..."); $expectedTable = $this->createFlatXmlDataSet("complexQueryAssertion.xml") ->getTable("myComplexQuery"); $this->assertTablesEqual($expectedTable, $queryTable); } }

Утверждение состояния нескольких таблиц

Конечно, вы можете утверждать состояние одновременно нескольких таблиц и сравнивать запрос набора результата с файловым набором данных. Для утверждений DataSet существует два разных способа.

  1. Вы можете использовать базу данных (Database, DB) DataSet из Connection и сравнить её с набором данных на основе файлов.

getConnection()->createDataSet(["guestbook"]); $expectedDataSet = $this->createFlatXmlDataSet("guestbook.xml"); $this->assertDataSetsEqual($expectedDataSet, $dataSet); } }

2. Вы можете создать DataSet самостоятельно:

    addTable("guestbook", "SELECT id, content, user FROM guestbook"); // additional tables $expectedDataSet = $this->createFlatXmlDataSet("guestbook.xml"); $this->assertDataSetsEqual($expectedDataSet, $dataSet); } }

Часто задаваемые вопросы

Будет ли PHPUnit (повторно) создавать схему базу данных для каждого теста?

Нет, PHPUnit требует, чтобы все объекты базы данных были доступны при запуске набора. Перед запуском набора тестов необходимо создать базу данных, таблицы, последовательности, триггеры и представления.

1) Цели и задачи ……………………………………………………………... 3

2) Описание базы данных …………………………………………………... 4

3) Работа с базой данных …………………………………………………… 6

4) Нагрузочное тестирование базы данных ………………………………...11

5) Вывод ……………………………………………………………………....15

6) Литература ………………………………………………………………....16

Цели и задачи

Цель: создать базу данных эликсиров по игре Ведьмак 3, которая будет содержать в себе информацию о виде эликсиров, их свойствах, из чего они изготавливаются, местах где их можно найти и о чудовищах, против которых их можно применять. Создать оптимизированные запросы к данной базе данных и протестировать ее на нагрузку.

Задачи:

· Создать в MYSQL Workbench схему базы данных минимум с 5 сущностями. Описать эти сущности и их связи.

· Описать использование этой БД, расписать основные запросы, посмотреть на их время исполнения и сделать выводы

· Оптимизация БД

· Выполнить нагрузочное тестирование с помощью apache-jmeter. Использовать для него расширения для построения графиков.

Описание базы данных

В курсовой работе используется созданная база данных Witcher1, основными сущностями которой являются таблицы:

Рис.1 Схематическое отображение базы данных Witcher1

В таблице Ingridients содержатся необходимые ингредиенты для создания эликсиров в игре, которые описаны в таблице Elixirs. Для создания эликсира используется несколько ингредиентов, но каждый из них уникален для своего эликсира. Именно по этой причине между этими таблицами была установлена связь 1: n (один ко многим), что и показано на схеме базы данных (Рис.1).

Так же в таблице Ingridients содержится информация о названиях ингредиентов (Discription) и о том, где можно найти данный ингредиент (WhereFind). Колонка idElixirs является связующей колонкой для таблиц Ingridients и Elixirs.

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

В таблице Locations содержится информация о том, в каком именно месте или около какого города можно найти конкретный ингредиент.

Таблица IL содержит объединённую информацию о том, где и как найти конкретный ингредиент в данной местности и что он из себя представляет. Между таблицами Ingridients и Locations была установлена связь n: m (многие ко многим), так как несколько ингредиентом могут быть найдены в нескольких локациях, о чем и указано в дочерней таблице IL.

В таблице Monsters содержится информация о видах чудовищ в

«Witcher 3», о том, как распознать то или иное чудовище и характерные для них имена.

Таблица ML является дочерней таблицей к объединению связью n: m таблиц Location и Monsters и содержит в себе конкретную информацию о том, как победить именно данное чудище и какие эликсиры для этого могут быть использованы, включая специальные ведьмачьи знаки, а так же в какой местности и по каким признакам искать именно этот вид чудища.

Работа с базой данных

База данных Witcher1 содержит в себе информацию о том, какие эликсиры против каких чудовищ необходимо использовать, специальную тактику для особо опасных чудовищ, таких как: Моровая дева, Черт, Бес, Леший и т.д. Анализировать информацию из каждой таблицы по порядку займет очень много времени, поэтому создадим специальные запросы к базе данных, которые максимально будут полезны для пользователя.

· Запрос о том, как найти конкретное чудище.

Данный запрос будет содержать в себе ключевое слово JOIN, благодаря которому и будет осуществляться обращение к таблицам ML и Monsters базы данных Witcher1.

Данный запрос будет выглядеть следующим образом:

ml JOIN monsters ON monsters.idMonsters=ml.idMonsters;

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

monsters.MonstersName, monsters.MonstersDiscription,

ml.DiscriptionHowFind, ml.idLocations

ml JOIN monsters ON monsters.idMonsters=ml.idMonsters

where monsters.MonstersName=’Hym’;

Какому чудовищу соответствует тот или иной ID можно узнать из запроса к таблицам Monsters или ML. Запросы будут выглядеть следующим образом:

SELECT SELECT

IdMonsters, MonstersName idMonsters, MonstersName

FROM ml; FROM monsters;

Для проверки соответствия можно сделать запрос к обеим таблицам ML и Monsters, предварительно объединив их по idMonsters.

ml.idMonsters, monsters.MonstersName

ml JOIN monsters ON

ml.idMonsters=monsters.idMonsters

ORDER BY monsters.idMonsters;

· Запрос о том, какой эликсир подходит к данному чудовищу.

Для реализации данного запроса будет использован JOIN. Запрос будет обращен к двум таблицам Elixirs и Monsters и будет содержать в себе информацию о том, когда и какой эликсир выпить в борьбе с чудовищем:

monsters.MonstersName ,elixirs.ElixirName, elixirs.ElixirDiscription

elixirs JOIN monsters ON

elixirs.idElixirs=monsters.idElixirs;

· Запрос о том, какой ингредиент находится в той или иной местности.

Для реализации данного запроса будет использован JOIN. Запрос будет обращен к двум таблицам Ingridients и Locations и будет содержать в себе информацию о том, какой ингредиент в какой локации находится и информацию о его виде:

ingridients.Discription, locations.Discription, ingridients.WhereFind

ingridients JOIN locations ON

ingridients.idIngridients=locations.idIngridients

ORDER BY ingridients.Discription;

· Запросы UPDATE

Данный запрос реализуем для чудовища в таблице Monsters по имени Хим (Hym). Допустим, мы хотим поменять ему имя на Him:

monsters

SET MonstersName="Him"

where idMonsters=1;

Но, так как в английском варианте верно Hym то вернем все обратно:

monsters

SET MonstersName="Hym"

where idMonsters=1;

Рис.2. Реализация запросов UPDATE

· Запросы «агрегации». COUNT и COUNT(DISTINCT)

Функция COUNT подсчитывает количество не пустых строк (внутри них не NULL) в данной таблице. COUNT имеет оптимизированную версию для вывода количества строк, если она используется для 1 таблицы. Например:

Рис.3. Подсчет строк в таблицах Elixirs, Monsters и объединенной таблице Monsters JOIN elixirs.

Функция же COUNT(DISTINCT) используется для вывода количества не повторяющихся строк в таблицах и является более оптимизированной версией семейства функций COUNT:

Рис.4. Подсчет неповторяющихся эликсиров в таблице Monsters.

· Функция DELETE.

Добавим в таблицу Elixirs еще одну строчку, используя INSERT:

INSERT INTO elixirs VALUES (6,’ForDelete’,’DiscriptionDelete’);

Рис.5. Добавление строки в таблицу Elixirs.

Теперь сделаем запрос на удаление этой строки, так как нет необходимости в эликсире, который никак не поможет в борьбе с чудищами:

DELETE FROM elixirs WHERE idElixirs=6;

Рис.6. Удаление добавленной строки.

Нагрузочное тестирование базы данных

Теперь, когда выполнены запросы и обращение к базе данных налажено, её можно протестировать по нескольким параметрам:

· Response Times Over Time или Времена отклика в зависимости от времени – данная проверка отображает информацию для каждого запроса его среднее время отклика в миллисекундах.

· Response Times Distribution или Распределение времени отклика - данная проверка отображает количество ответов в определенный интервал времени, во время которого выполнялся запрос.

· Response Time Percentiles или Процентили времени отклика – данная проверка отображает процентили для значений времени отклика. На графике по оси X будут располагаться проценты, по оси Y- время отклика.

Для максимально правдоподобных тестов зададим определенные

параметры:

Рис.7. Параметры тестирования

Number of Threads(users) – Число виртуальных пользователей. В нашем случае поставим 1000, чтобы максимально нагрузить нашу базу данных.

Ramp-Up Period – период, в течение которого все пользователи будут задействованы.

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

Последние 3 пункта – это графопостроители тех проверок, по которым мы будем тестировать базу данных.

·
Проверка Response Times Over Time

Рис.7. Результат выполнения запросов при тесте Response Times Over Time

Как видно из графика, самый трудновыполнимый запрос был «Monsters&Locations» и потребовал больше всего времени на его отклик. Убедиться в причине длительного выполнения запроса можно выполнив запрос в консоли. Основная причина такой задержки объясняется тем, что и в таблице Monsters, и в таблице ML содержатся объемные пояснения к чудовищам или к местам, где их найти. Из-за этого запрос выполняется достаточно длительное время.

·
Проверка Response Times Distribution

Рис.8. Результат выполнения запросов при тесте Response Times Distribution.

По данному графику можно сделать вывод, что количество ответов для каждого из наших запросов в один и тот же период времени одинаково.

·
Проверка Response Time Percentiles

По оси ординат указано время выполнения, а по оси абсцисс размещены проценты от общего кол-ва. По графику можно заключить, что 90% запросов выполняются в интервал времени от 0 до 340 миллисекунд, причем с 5% до 15% количество запросов возрастает линейно, а далее по экспоненте с очень малым коэффициентом степени возрастания.

Оставшиеся 10% выполняются в интервал времени от 340 миллисекунд до 700 миллисекунд, благодаря чему можно сделать вывод, что осуществляется очень большая нагрузка на базу данных.

Вывод

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

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

Необходимо отметить, что сама БД была создана в качестве лишь учебной, поэтому она не столь объемна.

Еще одной важной характеристикой является безопасность: пароли, если будет создана такая таблица, нужно хранить в зашифрованном виде и оберегать от неправомерного доступа.

Литература

1. http://phpclub.ru/mysql/doc/ - интернет ресурс «MySQL - справочное руководство»

2. Шварц Б., Зайцев П., Ткаченко В. и др. - MySQL. Оптимизация производительности (2-е издание)

3. Талманн Л., Киндал М., Белл Ч. – «Обеспечение высокой доступности систем на основе MySQL»

4. Сапковский Анджей – «Ведьмак (большой сборник)», Количество страниц: 571

5. CD PROJECT RED, GOG COM. «Ведьмак 3: Дикая Охота».


Похожая информация.


Несколько лет назад оказалось, что SQL внезапно устарел. И начали появляться и множиться NoSQL-решения, отбросившие язык SQL и реляционную модель хранения данных. Основные аргументы в поддержку такого подхода: возможность работы с большими данными (те самые Big Data), хранения данных в самых экзотичных структурах и, самое главное, возможность все это делать очень быстро. Давай посмотрим, насколько это получается у самых популярных представителей мира NoSQL.

За счет чего достигается скорость в NoSQL? В первую очередь, это следствие совсем другой парадигмы хранения данных. Парсинг и трансляция SQL-запросов, работа оптимизатора, объединение таблиц и прочее сильно увеличивают время ответа. Если взять и выкинуть все эти слои, упростить запросы, читать с диска прямо в сеть или хранить все данные в оперативной памяти, то можно выиграть в скорости. Уменьшается как время обработки каждого запроса, так и количество запросов в секунду. Так появились key-value БД, самым типичным и широко известным представителем которых является memcached. Да, этот кеш, широко применяемый в веб-приложениях для ускорения доступа к данным, тоже является NoSQL.

Типы NoSQL

Можно выделить четыре основные категории NoSQL-систем:

  • Ключ - значение (key-value). Большая хеш-таблица, где допустимы только операции записи и чтения данных по ключу.
  • Колоночные (column). Таблицы, со строками и колонками. Вот только в отличие от SQL количество колонок от строки к строке может быть переменным, а общее число колонок может измеряться миллиардами. Также каждая строка имеет уникальный ключ. Можно рассматривать такую структуру данных как хеш-таблицу хеш-таблицы, первым ключом является ключ строки, вторым - имя колонки. При поддержке вторичных индексов возможны выборки по значению в колонке, а не только по ключу строки.
  • Документо-ориентированные (document-oriented). Коллекции структурированных документов. Возможна выборка по различным полям документа, а также модификация частей документа. К этой же категории можно отнести поисковые движки, которые являются индексами, но, как правило, не хранят сами документы.
  • Графовые (graph). Специально предназначены для хранения математических графов: узлов и связей между ними. Как правило, позволяют задавать для узлов и связей еще и набор произвольных атрибутов и выбирать узлы и связи по этим атрибутам. Поддерживают алгоритмы обхода графов и построения маршрутов.

Для теста мы взяли представителей первых трех категорий:

Как проводился тест

В распоряжении у нас было четыре серверных машинки. В каждой: восьмиядерный Xeon, 32 Гб ОЗУ, четыре интеловских SSD по 120 Гб каждый.

Тестировали мы с помощью YCSB (Yahoo! Cloud Serving Benchmark). Это специальный бенчмарк, выпущенный командой Yahoo! Research в 2010 году под лицензией Apache. Бенчмарк специально создан для тестирования NoSQL баз данных. И сейчас он остается единственным и довольно популярным бенчмарком для NoSQL, фактически стандартом. Написан, кстати, на Java. Мы добавили к оригинальному YCSB драйвер для Aerospike, слегка обновили драйвер для MongoDB, а также несколько подшаманили с выводом результатов.

INFO

Кроме YCSB, тестировать производительность NoSQL БД можно с помощью, например, JMeter.

Для создания нагрузки на наш маленький кластер потребовалось восемь клиентских машин. По четырехъядерному i5 и 4 Гб ОЗУ на каждой. Одного (и двух, и трех, и четырех...) клиентов оказалось недостаточно, чтобы загрузить кластер. Может показаться странным, но факт.

Все это шевелилось в одной гигабитной локальной сети. Пожалуй, было бы интереснее в десятигигабитной сети, но такого железа у нас не было. Интереснее, потому что, когда количество операций в секунду начинает измеряться сотнями тысяч, мы утыкаемся в сеть. При пропускной способности в гигабит в секунду (10^9 бит/c) сеть может пропустить килобайтных пакетов (~10^4 бит) лишь около 100 000 (10^5) штук. То есть получаем лишь 100k операций в секунду. А нам вообще-то хотелось получить миллион:).

Сетевые карты тоже имеют значение. Правильные серверные сетевухи имеют несколько каналов ввода-вывода, соответственно, каждый с собственным прерыванием. Вот только по умолчанию в линуксе все эти прерывания назначены на одно ядро процессора. Только ребята из Aerospike озаботились этой тонкостью, и их скрипты настройки БД раскидывают прерывания сетевых карт по ядрам процессора. Посмотреть прерывания сетевых карт и то, как они распределены по ядрам процессора, можно, например, такой командой: «cat /proc/interrupts | grep eth».

Отдельно стоит поговорить про SSD. Мы хотели протестировать работу NoSQL БД именно на твердотельных накопителях, чтобы понять, действительно ли эти диски того стоят, то есть дают хорошую производительность. Поэтому старались настроить SSD правильно. Подробнее об этом можно прочитать на врезке.

Настраиваем SSD

В частности, SSD требуют действий, называемых непереводимым словом overprovisioning. Дело в том, что в SSD присутствует слой трансляции адресов. Адреса блоков, видные операционной системе, совсем не соответствуют физическим блокам во флеш-памяти. Как ты знаешь, число циклов перезаписи у флеш-памяти ограничено. К тому же операция записи состоит из двух этапов: стирания (часто - сразу нескольких блоков) и собственно записи. Поэтому, для обеспечения долговечности накопителя (равномерного износа) и хорошей скорости записи, контроллер диска чередует физические блоки памяти при записи. Когда операционная система пишет блок по какому-то адресу, физически запись происходит на некий чистый свободный блок памяти, а старый блок помечается как доступный для последующего (фонового) стирания. Для всех этих манипуляций контроллеру диска нужны свободные блоки, чем больше, тем лучше. Заполненный на 100% SSD может работать весьма медленно.

Свободные блоки могут получиться несколькими способами. Можно с помощью команды hdparm (с ключом "-N") указать количество секторов диска, видимых операционной системой. Остальное будет в полном распоряжении контроллера. Однако это работает не на всяком железе (в AWS EC2, например, не работает). Другой способ - оставить не занятое разделами место на диске (имеются в виду разделы, создаваемые, например, fdisk). Контроллер достаточно умен, чтобы задействовать это место. Третий способ - использовать файловые системы и версии ядра, которые умеют сообщать контроллеру о свободных блоках. Это та самая команда TRIM. На нашем железе хватило hdparm, мы отдали на растерзание контроллеру 20% от общего объема дисков.

Для SSD важен также планировщик ввода-вывода. Это такая подсистема ядра, которая группирует и переупорядочивает операции ввода-вывода (в основном записи на диск) с целью повысить эффективность. По умолчанию линукс использует CFQ (Completely Fair Queuing), который старается переставить операции записи так, чтобы записать как можно больше блоков последовательно. Это хорошо для обычных вращающихся (так и говорят - spinning:)) дисков, потому что для них скорость линейного доступа заметно выше доступа к случайным блокам (головки нужно перемещать). Но для SSD линейная и случайная запись - одинаково эффективны (теоретически), и работа CFQ только вносит лишние задержки. Поэтому для SSD-дисков нужно включать другие планировщики, например NOOP, который просто выполняет команды ввода-вывода в том порядке, в каком они поступили. Переключить планировщик можно, например, такой командой: «echo noop > /sys/block/sda/queue/scheduler», где sda - твой диск. Справедливости ради стоит упомянуть, что свежие ядра сами умеют определять SSD-накопители и включать для них правильный планировщик.

Любая СУБД любит интенсивно писать на диск, а также интенсивно читать. А Linux очень любит делать read-ahead, упреждающее чтение данных, - в надежде, что, раз ты прочитал этот блок, ты захочешь прочитать и несколько следующих. Однако с СУБД, и особенно при случайном чтении (а этот как раз наш вариант), этим надеждам не суждено сбыться. В результате имеем никому не нужное чтение и использование памяти. Разработчики MongoDB рекомендуют по возможности уменьшить значение read-ahead. Сделать это можно командой «blockdev --setra 8 /dev/sda», где sda - твой диск.

Любая СУБД любит открывать много-много файлов. Поэтому необходимо заметно увеличить лимиты nofile (количество доступных файловых дескрипторов для пользователя) в файле /etc/security/limits.conf на значение сильно больше 4k.

Также возник интересный вопрос: как использовать четыре SSD? Если Aerospike просто подключает их как хранилища и как-то самостоятельно чередует доступ к дискам, то другие БД подразумевают, что у них есть лишь один каталог с данными. (В некоторых случаях можно указать и несколько каталогов, но это не предполагает чередования данных между ними.) Пришлось создавать RAID 0 (с чередованием) с помощью утилиты mdadm. Я полагаю, что можно было бы поиграть с LVM, но производители СУБД описывают только использование mdadm.

Естественно, на всех машинах кластера (как серверных, так и клиентских) часы должны быть синхронизированы с помощью ntpd. Ntpdate тут не годится, потому что требуется бóльшая точность синхронизации. Для всех распределенных систем жизненно важно, чтобы время между узлами было синхронизировано. Например, Cassandra и Aerospike хранят время изменения записи. И если на разных узлах кластера найдутся записи с разным таймстампом, то победит та запись, которая новее.

Сами NoSQL БД настраивались следующим образом. Бралась конфигурация из коробки, и применялись все рекомендации, описанные в документации и касающиеся достижения наибольшей производительности. В сложных случаях мы связывались с разработчиками БД. Чаще всего рекомендации касались подстроек под количество ядер и объем ОЗУ.

Проще всего настраивается Couchbase. У него есть веб-консоль. Достаточно запустить сервис на всех узлах кластера. Затем на одном из узлов создать bucket («корзину» для ключей-значений) и добавить другие узлы в кластер. Все через веб-интерфейс. Особо хитрых параметров настройки у него нет.

Aerospike и Cassandra настраиваются примерно одинаково. На каждом узле кластера нужно создать конфигурационный файл. Эти файлы почти идентичны для каждого узла. Затем запустить демонов. Если все хорошо, узлы сами соединятся в кластер. Нужно довольно хорошо разбираться в опциях конфигурационного файла. Тут очень важна хорошая документация.

Сложнее всего с MongoDB. У других БД все узлы равнозначны. У Mongo это не так. Мы хотели поставить все БД по возможности в одинаковые условия и выставить у всех replication factor в 2. Это означает, что в кластере должно быть две копии данных, для надежности и скорости. В других БД replication factor - это лишь настройка хранилища данных (или «корзины», или «семейства колонок»). В MongoDB количество копий данных определяется структурой кластера. Грамотно настроить кластер MongoDB можно, только дважды прочтя официальную документацию, посвященную этому:). Говоря кратко, нам нужны shards or replica-sets. Шарды (ну ты наверняка слышал термин «шардинг») - это подмножества всего набора данных, а также узлы кластера, где каждое подмножество будет хранится. Реплика-сеты - это термин MongoDB, обозначающий набор узлов кластера, хранящих одинаковые копии данных. В реплика-сете есть главный узел, который выполняет операции записи, и вторичные узлы, на которые осуществляется репликация данных с главного узла. В случае сбоев роль главного узла может быть перенесена на другой узел реплика-сета. Для нашего случая (четыре сервера и желание хранить две копии данных) получается, что нам нужно два шарда, каждый из которых представляет собой реплика-сет из двух серверов с данными. Кроме того, в каждый реплика-сет нужно добавить так называемый арбитр, который не хранит данные, а нужен для участия в выборах нового главного узла. Число узлов в реплика-сете для правильных выборов должно быть нечетным. Еще нужна маленькая конфигурационная БД, в которой будет храниться информация о шардах и о том, какие диапазоны данных на каком шарде хранятся. Технически это тоже MongoDB, только (по сравнению с основными данными) очень маленькая. Арбитры и конфигурационную БД мы разместили на клиентских машинах. И еще на каждом клиенте нужно запустить демон mongos (mongo switch), который будет обращаться к конфигурационной БД и маршрутизировать запросы от каждого клиента между шардами.

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

Набор данных, которыми оперирует YCSB, - это ключ и значение. Ключ - это строка, в которую входит 64-битный хеш. Таким образом, сам YCSB, зная общее количество записей в БД, обращается к ним по целочисленному индексу, а для БД множество ключей выглядит вполне случайным. Значение - десяток полей случайных бинарных данных. По умолчанию YCSB генерирует записи килобайтного размера, но, как ты помнишь, в гигабитной сети это ограничивает нас лишь в 100k операций в секунду. Поэтому в тестах мы уменьшили размер одной записи до 100 байт.

Операции над этими данными YCSB осуществляет тоже простейшие: вставка новой записи с ключом и случайными данными, чтение записи по ключу, обновление записи по ключу. Никаких объединений таблиц (да и вообще подразумевается лишь одна «таблица»). Никаких выборок по вторичным ключам. Никаких множественных выборок по условию (единственная проверка - совпадение первичного ключа). Это очень примитивно, но зато может быть произведено в любой БД.

Непосредственно перед тестированием БД нужно наполнить данными. Делается это самим YCSB. По сути, это нагрузка, состоящая лишь из операций вставки. Мы экспериментировали с двумя наборами данных. Первый гарантированно помещается в оперативную память узлов кластера, 50 миллионов записей, примерно 5 Гб чистых данных. Второй гарантированно не помещается в ОЗУ, 500 миллионов записей, примерно 50 Гб чистых данных.

Сам тест - выполнение определенного набора операций - производится под нагрузкой разного типа. Важным параметром является соотношение операций - сколько должно быть чтений, а сколько обновлений. Мы использовали два типа: интенсивная запись (Heavy Write, 50% чтений и 50% обновлений) и в основном чтение (Mostly Read, 95% чтений и 5% обновлений). Какую операцию выполнить, каждый раз выбирается случайно, проценты определяют вероятность выбора операции.

YCSB может использовать различные алгоритмы выбора записи (ключа) для выполнения операции. Это может быть равномерное распределение (любой ключ из всего множества данных может быть выбран с одинаковой вероятностью), экспоненциальное распределение (ключи «в начале» набора данных будут выбираться значительно чаще) и некоторые другие. Но типичным распределением команда Yahoo выбрала так называемое zipfian. Это равномерное распределение, в котором, однако, отдельные ключи (небольшой процент от общего количества ключей) выбираются значительно чаще, чем другие. Это симулирует популярные записи, скажем в блогах.

YCSB стартует с несколькими потоками, запуская цикл выполнения операций в каждом из них, и все это на одной машине. Имея лишь четыре ядра на одной клиентской машине, довольно грустно пытаться запускать там более четырех потоков. Поэтому мы запускали YCSB на восьми клиентских машинах одновременно. Для автоматизации запуска мы использовали fabric и cron (точнее, at). Небольшой скрипт на Python формирует необходимые для запуска YCSB команды на каждом клиенте, эти команды помещаются в очередь at на одно и то же время на ближайшую минуту в будущем на каждом клиенте. Потом срабатывает at, и YCSB успешно (или не очень, если ошиблись в параметрах) запускается в одно и то же время на всех восьми клиентах. Чтобы собрать результаты (лог файлы YCSB), снова используется fabric.

Результаты

Итак, исходные результаты - это логи YCSB, с каждого клиента. Выглядят эти логи примерно так (показан финальный кусочек файла):

Operations, 1187363 , Retries, 0 , AverageLatency(us), 3876.5493619053314 , MinLatency(us), 162 , MaxLatency(us), 278190 , 95thPercentileLatency(ms), 12 , 99thPercentileLatency(ms), 22 , Return=0, 1187363 , Reconnections, 0.0 , RunTime(ms), 303574.0 , Operations, 1249984.0 , Throughput(ops/sec), 4117.5594747903315

Как видишь, здесь есть количество операций определенного типа (в данном примере - чтения), средняя, минимальная и максимальная задержки, задержка, в которую уложились 95 и 99% операций, количество успешных операций (код возврата 0), общее время теста, общее количество всех операций и среднее количество операций в секунду. Нас больше всего интересует средняя задержка (AverageLatency) и количество операций в секунду (Throughput).

С помощью очередного скрипта на Python данные из кучи логов собирали в табличку, а по табличке строили красивые графики.





Выводы

NoSQL БД разделились на две группы: быстрые и медленные. Быстрыми, как, собственно, и ожидалось, оказались key-value БД. Aerospike и Couchbase сильно опережают соперников.

Aerospike действительно очень быстрая БД. И нам почти получилось дойти до миллиона операций в секунду (на данных в памяти). Aerospike весьма неплохо работает и на SSD, особенно если учитывать, что Aerospike в этом режиме не использует кеширование данных в памяти, а на каждый запрос обращается к диску. Значит, в Aerospike действительно можно поместить большое количество данных (пока хватит дисков, а не ОЗУ).

Couchbase быстр, но быстр только на операциях в памяти. На графиках с тестами SSD показана скорость работы Couchbase на объеме данных лишь чуть больше объема ОЗУ - всего 200 миллионов записей. Это заметно меньше 500 миллионов, с которыми тестировались другие БД. В Couchbase просто не удалось вставить больше записей, он отказывался вытеснять кеш данных из памяти на диск и прекращал запись (операции записи завершались с ошибками). Это хороший кеш, но лишь для данных, помещающихся в ОЗУ.

Cassandra - единственная БД, которая пишет быстрее, чем читает:). Это оттого, что запись в ней успешно завершается (в самом быстром варианте) сразу после записи в журнал (на диске). А вот чтение требует проверок, нескольких чтений с диска, выбора самой свежей записи. Cassandra - это надежный и довольно быстрый масштабируемый архив данных.

MongoDB довольно медленна на запись, но относительно быстра на чтение. Если данные (а точнее, то, что называют working set - набор актуальных данных, к которым постоянно идет обращение) не помещаются в память, она сильно замедляется (а это именно то, что происходит при тестировании YCSB). Также нужно помнить, что у MongoDB существует глобальная блокировка на чтение/запись, что может доставить проблем при очень высокой нагрузке. В целом же MongoDB - хорошая БД для веба.

PS

Давай немного отвлечемся от вопросов производительности и посмотрим на то, как будут развиваться дальше SQL- и NoSQL-решения. На самом деле то, что мы видим сейчас, - это повторение хорошо знакомой истории. Все это уже было в шестидесятых и семидесятых годах двадцатого века: до реляционных БД существовали иерархические, объектные и прочие, и прочие. Потом захотелось стандартизации, и появился SQL. И все серьезные СУБД, каждая из которых поддерживала свой собственный язык запросов и API, переключились на SQL. Язык запросов и реляционная модель стали стандартом. Любопытно, что сейчас тоже пытаются привить SQL к NoSQL, что приводит к созданию как оберток поверх существующих NoSQL, так и совершенно новых БД, которые называют NewSQL.

Если NoSQL решили отказаться от «тяжелого наследия» SQL, пересмотреть подходы к хранению данных и создали совершенно новые решения, то термином NewSQL называют движение по «возрождению» SQL. Взяв идеи из NoSQL, ребята воссоздали SQL-базы на новом уровне. Например, в мире NewSQL часто встречаются БД с хранением данных в памяти, но с полноценными SQL-запросами, объединениями таблиц и прочими привычными вещами. Чтобы все же хранить много данных, в эти БД встраивают механизмы шардинга.

К NewSQL причисляют VoltDB, TokuDB, MemDB и другие. Запомни эти имена, возможно, скоро о них тоже будут говорить на каждой ИТ-конференции.

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

Пример запроса: Вывести список книг, взятых конкретным учеником. ФИО задать как параметр.

Пример запроса: Вывести список книг конкретного автора с указанием мест хранения в библиотеке. ФИО автора задать как параметр.

Пример запроса: Определить по номеру читательского билета в каком классе учится соответствующий ученик и кто его классный руководитель.

Рис. 15. Запрос 3. «Найти ученика по №читательского билета и определить в каком классе он учится»

Пример запроса: Определить по ФИО_Ученика в каком классе учится задолжник и кто его классный руководитель.

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

Рис. 17. Кнопочная форма базы данных

ЗАКЛЮЧЕНИЕ

Выпускная квалификационная работа выполнена на актуальную тему «Разработка информационной системы для сельской школьной библиотеки».

Цель дипломного проектирования разработать информационную систему для школьной библиотеки Саратовской области Федоровского района МОУ СОШ п. Солнечный достигнута.

В ходе выполнения дипломного проекта были решены следующие задачи:

Рассмотреть библиотеку как элемент образовательной среды;

Изучить правительственную концепцию поддержки и развития детского чтения;

Проанализированы технологии работы библиотек общеобразовательных учреждений;

Описана предметная область на основе проведенного обследования;

-разработано техническое задание на разработку информационной системы для библиотеки сельской школы;

-построена функциональная модель деятельности школьной библиотеки;

- описание входные и выходные потоки информации;

разработана информационная система на основе СУБД Acc е ss ;

- протестирована разработанная реляционная база данных.

В выпускной квалификационной работе для построения информационной системы, обеспечивающей автоматизацию ручных операций по обеспечению процессов хранения, поиска, учета выдачи и возврата учениками, на основе анализа результатов обследования предметной области было разработано техническое задание. В техническом задании (ТЗ) нашли отражения требования пользователей системы – библиотечных работников.

На основе ТЗ разработана функциональная модель деятельности сельской школьной библиотеки. Функциональная модель, в свою очередь, послужила материалом для выявления неавтоматизированных участков в работе библиотеки.

Выбор СУБД как среды разработки определялся техническими возможностями сельской библиотеки. В результате на основе СУБД Access построено ядро информационной системы – база данных.

Для удобства работы пользователей разработан кнопочный интерфейс.

Для тестирования базы данных разработаны соответственные запросы. Выполнение этих запросов позволяет судить о нормальной работоспособности информационной системы для сельской школьной библиотеки.

БИБЛИОГРАФИЧЕСКИЙ СПИСОК