Примечание
Для доступа к этой странице требуется авторизация. Вы можете попробовать войти или изменить каталоги.
Для доступа к этой странице требуется авторизация. Вы можете попробовать изменить каталоги.
Приступаем к работе с Oak: взаимодействие с базой данных
С возвращением. Мы прошли подготовку к работе с Oak — это динамический подход к веб-разработке, который включает идеи из Ruby и Node.js, в то же время позволяя использовать все возможности языка C#, инфраструктуры ASP.NET MVC и всего остального в Microsoft .NET Framework, что вам нравится. Суть Oak, выражаясь словами автора этого проекта, Амира Раджана (Amir Rajan), заключается в «быстрой обратной связи, разработке, лишенной трения, и минимуме формальностей».
В прошлый раз мы остановились на том, что Oak жаловался: ему не удается подключиться к базе данных. Сегодня я покажу, что делать дальше: как связать Oak с экземпляром SQL Server, добавить другой релевантный тип в систему (комментарии по записям в блоге), создать базу данных в Oak и связать два типа с помощью Oak. Кроме того, вы увидите, как Oak и его система сборки обрабатывают взаимодействие с базой данных.
Когда мы оставили своего героя…
При выполнении Oak в прошлый раз мы увидели сообщение об ошибке с поясняющим текстом, фрагмент которого показан на рис. 1. (Напомню: если вы следовали моим действиям в предыдущей статье [msdn.microsoft.com/magazine/dn532208], вам понадобится запустить сервер и sidekick.) Прежде чем двигаться дальше, обратите внимание на то, что Oak не просто отображает трассировку стека исключения, оставляя меня гадать, что могло вызвать проблему. Он пытается диагностировать проблему и предложить потенциальные решения: в данном случае — «Update the web.config with your server instance» (Обновите web.config вашим экземпляром сервера).
Рис. 1. Окно справки в проекте Oak после возникновения ошибки
Так и есть, заглянув в файл web.config, я увидел, что там находится просто шаблон подстановки (placeholder), указывающий на источник данных «(local)»; такой вариант мог бы сработать для множества конфигураций в зависимости от того, как сконфигурирован локальный экземпляр SQL Server. С другой стороны, изменить строку подключения LocalDB столь же тривиально, как и строку подключения к удаленному экземпляру SQL Server (или к Microsoft Azure SQL Database, если вы хотите хранить данные в облаке). Я предпочитаю использовать LocalDB, поскольку такая строка подключения вроде «Server=(localdb)\v11.0;Integrated Security=true» очень удобна для исследований — она короткая и легко воспринимается. Каким бы SQL-соединением вы ни пользовались, просто включите ее в элемент <configuration>/<connectionStrings>/<add> и обновите страницу. По сути, в Oak есть задача rake (средство сборки в Ruby), которая сделает это за вас по всей кодовой базе: «rake update_db_server[(localdb)\\v11.0]» (обратный слеш должен сопровождаться esc-символом).
Хьюстон?
К сожалению, при повторном запуске Oak действует так, будто ничего не изменилось. Также печально, что это так и есть. Sidekick, похоже, не отслеживает изменения в web.config — только в файлах исходного кода. Учитывая, насколько редко вы пользуетесь файлом web.config, это еще не конец света, но все же добавляет ложку дегтя в бочку меда, т. е. в весьма продуманный в остальном процесс. Чтобы заставить sidekick распознавать изменения в проекте, вы должны просто инициировать сохранение какого-нибудь важного файла, например HomeController.cs или другого файла исходного кода. Сделать это можно двумя способами: переключиться на этот файл, наугад выбрать в нем любое место, нажать пробел и удалить его (это убедит Visual Studio в том, что файл изменился и что его нужно сохранить) или просто вручную запустить rake из окна командной строки.
После этого обновление страницы в браузере дает другую ошибку («Invalid object name ‘Blogs’»), и вы попадаете прямо в мир миграций базы данных.
Данные, данные, данные!
Как и его концептуальные предшественники, Oak хочет управлять базами данных и их схемами за вас, поэтому база данных может оставаться более-менее «скрытой» из вашей поля зрения. В этом случае Oak вовсе не жаждет самостоятельно создавать базу данных из ничего, понимая, что у вас скорее всего есть свое мнение насчет того, как должна выглядеть схема (а даже если и нет, то уж у администратора баз данных точно есть). (Кто победит в этой битве или кому следовало бы победить, эта дискуссия лучше всего пойдет за кружкой пива, но желательно после того, как проект закончен.)
В Oak инициализация (seeding) базы данных выполняется определенным контроллером, SeedController; он вложен прямо за HomeController в SeedController.cs. Этот файл уже содержит определение для SeedController, но, что важнее для наших целей, в нем также есть класс Schema, который упростит процедуры, необходимые для формирования схемы и при необходимости поможет поместить какие-нибудь образцы данных. Кстати, учтите, что по умолчанию в Oak действует соглашение, по которому объекты хранятся в таблице базы данных с именем по множественном числе, поэтому объекты Blog будут помещаться в таблицу Blogs. Класс Schema приведен на рис. 2.
Рис. 2. Класс Schema
public class Schema
{
// Это метод, который вы захотите изменить
public IEnumerable<Func<dynamic>> Scripts()
{
// Замените все содержимое в методе Scripts() этой строкой:
yield return CreateBlogsTable; // просто возвращает // указатель на функцию
}
public string CreateBlogsTable() // здесь определение функции
{
// Это пример, имя вашей таблицы может отличаться.
// Подробнее о генерации схемы см. Oak wiki.
return Seed.CreateTable("Blogs",
Seed.Id(),
new { Name = "nvarchar(255)" },
new { Body = "nvarchar(max)" }
);
}
public void SampleEntries()
{
}
public Seed Seed { get; set; }
public Schema(Seed seed) { Seed = seed; }
}
Обратите внимание на необычное использование yield return в методе Scripts. Для тех, кто никогда не знал, что это выражение было введено в C# 2.0, поясню, что yield return создает анонимный «поток» объектов и возвращает IEnumerable<T>, указывающий на этот поток (stream). В данном конкретном случае это поток функций, причем в этом сценарии это поток из одной функции (CreateBlogsTable). По сути, вы подготавливаете поток операций (экземпляры Func<dynamic>), представляющих, как создать базу данных, и Oak будет принимать каждую операцию или функцию, возвращаемую из этого потока, и выполнять ее. Благодаря этому, когда в силу вступит новое изменение в схеме, данное изменение может быть захвачено как новая функция (скажем, CreateCommentsTable, если хотите) и просто добавлено в список. При желании вы могли бы управлять «версиями» схемы базы данных, вызывая сначала функцию Version1, потом Version2 и т. д. В Oak wiki вы найдете более подробную информацию о миграциях базы данных по ссылке bit.ly/1bgods5.
(Если вы помните мою серию статей «Мультипарадигматическая .NET», которая началась с msdn.microsoft.com/magazine/ff955611, то, да, это весьма функционально-ориентированный подход к этой задаче.)
Функция Seed.Id создает каноническое поле основного ключа: автоматически приращиваемое целое значение, помеченное как основной ключ. В Oak wiki (bit.ly/1iKfcIb) есть ссылка на создание базы данных, где для этого используется класс Seed, но при желании всегда можно сделать это напрямую через SQL:
public IEnumerable<string> AdHocChange()
{
var reader = "select * from SampleTable".ExecuteReader();
while (reader.Read())
{
// Здесь делаем всякие вещи вроде возврата строк // через выражение yield return
}
var name = "select top 1 name from sysobjects".
ExecuteScalar() as string;
yield return "drop table SampleTable";
yield return "drop table AnotherSampleTable";
}
Добавьте метод AdHocChange как еще один метод «yield return» для вызова, и Oak с удовольствием будет выполнять и эти команды. (Заметьте: если вы потеряете эту ссылку на wiki, то ничего страшного — Oak включает ее в отображаемые им сообщения об ошибках или в справочные сообщения.)
Я не могу делать кирпичи без глины!
Кстати, если базе данных нужны какие-то начальные данные, это тоже входит в обязанности SeedController, и он вновь обращается к классу Schema, на этот раз через метод SampleEntries. Поскольку в моей системе никакие реальные начальные данные не требуются, я оставлю его в покое.
Как только SeedController скомпилирован, sidekick заново развернет проект, но, как и большинство контроллеров, он не активируется, пока не получит HTTP-запрос. Если вы не изучили SeedController, взгляните на него сейчас. Он экспортирует четыре конечные точки POST: PurgeDB, Exports, All и SampleEntries. Хотя вы определенно могли бы сами указывать эти конечные точки, это такая повторяющаяся задача, в которой лучше всего положиться на автоматизацию, в случае Oak — на rake. Разумеется, rake reset удалит все таблицы и сгенерирует схему заново (она выполняет POST к /seed/PurgeDB, а затем еще одну POST к /seed/all), а rake sample удалит таблицы, сгенерирует схему заново и создаст образцы данных (POST к /seed/SampleEntries). Ну и для полноты картины rake export (или POST к /seed/export) вернет SQL-выражения, используемые для выполнения этих действий.
(Curl или Ruby, кстати, могут выполнять POST из командной строки. В файле Rakefile.rb есть примеры того, как выполнять POST, используя Net::HTTP::post_from, и их можно легко вырезать и вставить в другой файл .rb, если вы не хотите искать их в дебрях Rakefile. Или использовать эти примеры, чтобы начать осваивать Ruby. Ваше дело.)
Он жив!
При условии, что опечаток не допущено, после выполнения rake reset обновление в браузере предоставляет рабочую веб-страницу с простым текстовым полем (для названия блога) и кнопкой submit (для передачи данных). Однако ввод названия нового блога приводит к генерации ошибки: хотя Blogs существует, предполагается (на основе контента в представлении Index.cshtml), что с Blogs сопоставлена такая штука, как Comments.
В простой реляционной модели, доминирующей в современных механизмах поддержки блогов, блоги имеют отношения с комментариями «один ко многим». Мне нужно смоделировать в своей системе и это, а значит, первым делом требуется некий тип данных для объектов Comment, что опять же предельно просто. Фактически, так как Comment, в общем-то, являются (подобно объектам Blog) динамическими объектами без каких-либо интересных элементов, они даже не нуждаются в определении класса модели — подойдет готовый полностью динамический объект (типа Gemini — для тех, кто помнит мою статью «Going Dynamic with the Gemini Library» за август 2013 г.; см. по ссылке msdn.microsoft.com/magazine/dn342877).
(И да, если у вас до сих пор не возникло впечатление вроде «Тото, у меня такое ощущение, что мы больше не в Канзасе», то сейчас самое время остановиться и поразмыслить: вы работаете с бизнес-объектом, чей тип вы не удосужились определить.)
Но, чтобы описать отношение Comments к Blogs, нужно сделать две вещи. Прежде всего для Comments необходим какой-то репозитарий (как для Blogs в прошлой статье):
public class Comments : DynamicRepository
{
}
Еще важнее то, что класс Blog требуется слегка модифицировать под отношение «один ко многим» — как напрямую (Blog владеет набором объектов Comment, в данном случае объект Comments является полем), так и косвенно (чтобы Oak знал, что объекты Blog и Comment связаны в базе данных определенным образом). Для этого я ввожу новый метод, Associates, который описывает отношение, как показано на рис. 3.
Рис. 3. Описание отношения «один ко многим» между Blogs и Comments
public class Blog : DynamicModel
{
Blogs blogs = new Blogs();
// Определяем comments
Comments comments = new Comments();
public Blog() { }
public Blog(object dto) : base(dto) { }
// Вводим метод Associates, добавляющий метод Comments()
IEnumerable<dynamic> Associates()
{
// Определяем связи...
// За примерами других связей обращайтесь в Oak wiki
yield return new HasMany(comments);
}
}
Как видите, это не особо далеко от того, что я сказал о том, что вам нужно сделать: модель довольно близко соответствует абстрактной концепции блога. Именно здесь можно воочию наблюдать красоту и мощь применения динамических методов. Одно изменение (метод Associates, выполняющий yield return для HasMany) на самом деле инициирует добавление в Blog трех новых методов — Comments, CommentIds и NewComment — для поддержки отношения объектов Comment к объектам Blog. Все это делается исключительно по шаблону, и при использовании обычного, нединамического кода на C# вам пришлось бы самостоятельно писать все эти методы. Естественно, чтобы все это работало с базой данных, вы должны предоставить описание таблицы комментариев в базе данных, а это вновь возвращает нас к SeedController (и последующему rake reset), как показано на рис. 4.
Рис. 4. Описание таблицы Comments в базе данных
public class Schema{
public IEnumerable<Func<dynamic>> Scripts()
{
yield return CreateBlogsTable;
yield return CreateCommentsTable;
}
public string CreateBlogsTable() // определение функции
{
return Seed.CreateTable("Blogs",
Seed.Id(),
new { Name = "nvarchar(255)" },
new { Body = "nvarchar(max)" }
);
}
public string CreateCommentsTable() // определение функции
{
return Seed.CreateTable("Comments",
Seed.Id(),
new { BlogId = "int", ForeignKey = "Blogs(Id)" },
new { Body = "nvarchar(max)" }
);
}
}
Раз уж я здесь, то заодно добавлю парочку записей в блог, просто чтобы продемонстрировать, как пользоваться методом SampleEntries. Это намного легче, чем вы могли подумать (рис. 5).
Рис. 5. Добавление записей в блог
public void SampleEntries(){
var blog1 = new // сохраняем ID
{
Name = "Hello, Oak Blog",
Body = "Lorem ipsum dolor sit amet, consectetur adipiscing elit."
}.InsertInto("Blogs");
new { Body = "great job!", BlogId = blog1 }.InsertInto("Comments");
new
{
Name = "Here are cat pictures!",
Body = "Meowem hisum collar sit amet, addipisces lick."
}.InsertInto("Blogs");
}
И вновь из-за использование динамических объектов и методов расширения кажется, что это уже не C#. (В данном случае вы создаете не столько динамический объект, сколько экземпляр анонимного объекта с автоматически генерируемыми свойствами Title и Body, а затем с помощью метода расширения InsertInto выполняете саму вставку.) И кстати, единственная причина захвата объекта в локальную переменную blog1 — поддержка возможности его использования в качестве значения идентификатора блога для комментариев по нему.
Движемся дальше. Обновите базу данных командой rake sample, а затем обновите страницу в браузере.
В следующий раз: проверка пользовательского ввода
Начинаются некоторые интересные вещи, если уже не начались. Вы не определили типы для модели, тем не менее модель работает, равно как и хранилище в базе данных для нее. Никакие SQL-скрипты не используются (хотя вполне логично предположить, что многократно используемые специализированные методы в классе Schema можно было бы легко преобразовать в SQL-скрипты), и важно отметить, что весь код, делающий все это, скрывается (пока) в виде исходного кода в папке Oak в поддерживаемом проекте, — это на случай, если потребуется отладка или если вам захочется просмотреть этот код.
Мы еще не все сделали, например не включили проверку пользовательского ввода, чтобы быть уверенным в его корректности перед сохранением, но об этом мы поговорим в следующий раз.
Удачи в кодировании!
Тэд Ньюард (Ted Neward) — глава компании Neward & Associates LLC. Автор и соавтор многочисленных книг, в том числе «Professional F# 2.0» (Wrox, 2010), более сотни статей, часто выступает на многих конференциях по всему миру; кроме того, имеет звание Microsoft MVP в области F#. С ним можно связаться по адресу ted@tedneward.com, если вы заинтересованы в сотрудничестве. Также читайте его блог blogs.tedneward.com и заметки в twitter.com/tedneward.
Выражаю благодарность за рецензирование статьи эксперту Амиру Раджану (Amir Rajan) (автор проекта Oak).