Как стать автором
Обновить

NerdDinner. Шаг 3: Построение модели

Время на прочтение 14 мин
Количество просмотров 6.2K
Автор оригинала: Scott Gu, Hanselman, Haacked
Это третий шаг бесплатного руководства «NerdDinner», которое показывает, как построить маленькое, но полноценное веб-приложение, используя ASP.NET MVC.

Во фреймворке типа модель-представление-контроллер термин «модель» ссылается на объекты, которые представляют данные приложения, как и соответствующую логику предметной области, которая интегрирует проверку и бизнес правила. Модель во многих случаях является «сердцем» MVC-приложения и, как мы увидим позже, управляет его основным поведением.

ASP.NET MVC фреймворк поддерживает использование любой технологии доступа к данным, следовательно разработчики могут выбирать разные варианты реализации своей модели включая: LINQ to Entities, LINQ to SQL, NHibernate, LLBLGen Pro, SubSonic, WilsonORM или прямой доступ через ADO.NET DataReader и DataSet.

Для нашего NerdDinner приложения мы будем использовать LINQ to SQL для создания простой модели, которая почти аналогична структуре базы данных, а также добавим некоторую логику проверок и бизнес правила. Позже мы реализуем класс-хранилище, который поможет абстрагировать постоянную реализацию хранения данных от остальной части приложения и позволит легко производить с ним юнит-тесты.

LINQ to SQL


LINQ to SQL является ORM(Объектно-реляционная проекция), которая поставляется, как часть .NET 3.5.

LINQ to SQL предоставляет простой способ связывания таблиц базы данных с .NET классами. Для нашего NerdDinner приложения мы будем использовать для связи таблиц Dinners и RSVP и классов Dinner и RSVP. Колонки таблиц Dinners и RSVP будут соответствовать свойствам классов Dinner и RSVP. Каждый Dinner и RSVP объект будет представлять отдельную строчку в таблицах Dinner или RSVP в базе данных.

LINQ to SQL позволяет избегать ручного построения SQL запросов для получения или обновления объектов Dinner и RSVP с данными в базе. Вместо этого, мы объявляем классы Dinner RSVP, как они связываются с базой данных и определяем связи между ними. LINQ to SQL позаботится о создании соответствующей SQL-логики для использования во время работы с объектами.

Мы можем использовать язык LINQ, поддерживаемый в VB и C#, для написания запросов, которые возвращают объекты Dinner и RSVP с базы данных. Это минимизирует размер кода, который нам нужно написать и позволяет строить действительно чистые приложения.

Добавление LINQ to SQL классов в проект


Мы начнем с нажатия правой кнопки на папке “Models” в проекте и выберем пункт Add>New Item:



В окне «Add New Item» отфильтруем по категории “Data” и выберем шаблон “LINQ to SQL Classes”:



Назовем элемент “NerdDinner” и добавим его. Visual Studio добавит файл NerdDinner.dbml в директорию \Models и откроет конструктор связей объекта LINQ to SQL:



Создание классов модели данных с LINQ to SQL


LINQ to SQL позволяет нам быстро создавать классы модели данных с существующей схемы базы данных. Для этого откроем базу данных NerdDinner в Server Explorer и выделим таблицы, для которых нужно создать модель:



Далее перетянем таблицы в конструктор LINQ to SQL. Как только мы это сделаем, LINQ to SQL автоматически создаст классы Dinner и RSVP, используя схему таблиц, включая свойства класса, которые связаны с колонками таблицы базы данных:



По умолчанию конструктор LINQ to SQL автоматически обрабатывает множественное число в именах таблиц и названии колонок во время создания класса. Например: таблица «Dinners» становится классом “Dinner”. Данное именование классов помогает модели придерживаться совместимости с правилами именования .NET, да и когда вы добавляете большое количество таблиц – это удобно. Если вам не нравится имя класса или свойства, которое сгенерировал конструктор, вы можете всегда изменить его на более подходящее через редактирование в конструкторе или в свойствах.

Так же по умолчанию, конструктор LINQ to SQL находит первичные и внешние ключи таблиц и, основываясь на них, автоматически создает связи между разными классами моделей. Например, когда мы перетянули таблицы Dinners и RSVP на конструктор LINQ to SQL была создана связь «один-ко-многим», потому что таблица RSVP содержит внешний ключ на таблицу Dinners (на это указывает стрелка в конструкторе):



Данная связь позволит LINQ to SQL добавить строго типизированное «Dinner» свойство в класс RSVP, которое смогут использовать разработчики для доступа к Dinner, привязанному к RSVP. Это также позволит классу Dinner содержать коллекцию свойств RSVP, что дает разработчикам возможность получать или изменять объекты RSVP привязанные к Dinner.

Ниже представлен пример intellisense в VisualStudio, когда мы создаем новый объект RSVP и добавляем его в коллекцию RSVP объекта класса Dinner. Обратите внимание, как LINQ to SQL автоматически добавил коллекцию RSVP объектов для объекта класса Dinner:



Добавляя объект RSVP в коллекцию RSVP объекта Dinner, мы указываем LINQ to SQL связать строку Dinner и RSVP в базе данных через внешний ключ:



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

Класс NerdDinnerDataContext


Visual Studio автоматически создает .NET классы, которые представляют модели и связи, используя конструктор LINQ to SQL. Также для каждого созданного конструктором LINQ to SQL файлом создается класс DataContext. Так как мы назвали наш LINQ to SQL класс «NerdDinner», DataContext класс будет носить имя “NerdDinnerDataContext”, который непосредственно взаимодействует с базой данных.

NerdDinnerDataContext класс содержит два свойства – “Dinners” и “RVSPs”, которые представляют две таблицы, которые мы создали в базе. Мы можем использовать C# для написания LINQ запросов с данными свойствами, для получения Dinner и RSVP объектов из базы данных.

Следующий код демонстрирует работу объекта NerdDinnerDataContext c LINQ запросом, для получения очереди предстоящих ужинов(Dinners). Visual Studio предоставляет полную поддержку intellisense во время написания LINQ запросов. Объекты, которые вернул запрос являются строго типизированными:



Вдобавок NerdDinnerDataContext автоматически отслеживает любые изменения в объектах Dinner и RSVP.Вы можем использовать данный функционал для легкого сохранения изменений в базу данных, без надобности писать SQL UPDATE код.

Например, давайте рассмотрим, как использовать LINQ запрос для получения одного объекта класса Dinner из базы данных, изменить два Dinner свойства и потом сохранить изменения обратно в базу:
NerdDinnerDataContext db = new NerdDinnerDataContext();

// Возвращает объект Dinner, который представляет строку с DinnerID равным 1
Dinner dinner = db.Dinners.Single(d => d.DinnerID == 1);

// Изменяем два свойства Dinner объекта
dinner.Title = "Changed Title";
dinner.Description = "This dinner will be fun";

// Сохраняем данные в базу
db.SubmitChanges();

Объект NerdDinnerDataContext автоматически отслеживает изменения, которые мы внесли в свойства полученного объекта Dinner. Когда мы вызовем метод “SubmitChanges()”, он выполнит соответствующий SQL “UPDATE” запрос, чтобы сохранить изменения в базе данных.

Создаем класс DinnerRepository


Для небольших приложений иногда удобно, когда контроллеры работают напрямую с LINQ to SQL DataContext классом и LINQ запросы находятся в контроллерах. Но с ростом приложения данный поход становится громоздким и неудобным для тестирования. Мы также будем сталкиваться с дублированием LINQ кода в разных местах.

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

Для нашего проекта NerdDinner мы объявим класс DinnerRepositary со следующей сигнатурой:
public class DinnerRepository {
  // Методы запросов
  public IQueryable<Dinner> FindAllDinners();
  public IQueryable<Dinner> FindUpcomingDinners();
  public Dinner       GetDinner(int id);

  // Вставка/удаление
  public void Add(Dinner dinner);
  public void Delete(Dinner dinner);

  // Сохранение
  public void Save();
}

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

Для реализации этого класса, нажимаем правой кнопкой по папке “Models” и выбираем Add>New Item. В открывшимся окне, выбираем шаблон “Class” и называем файл “DinnerRepository.cs”:



Далее наполняем наш класс DinnerRepository следующим кодом:
public class DinnerRepository {
  private NerdDinnerDataContext db = new NerdDinnerDataContext();
  
  //
  // Методы запросов
  public IQueryable<Dinner> FindAllDinners() {
    return db.Dinners;  
  }

  public IQueryable<Dinner> FindUpcomingDinners() {
    return from dinner in db.Dinners
        where dinner.EventDate > DateTime.Now
        orderby dinner.EventDate
        select dinner;
  }

   public Dinner GetDinner(int id) {
    return db.Dinners.SingleOrDefault(d => d.DinnerID == id);
  }

  //
  // Методы Insert/Delete
  public void Add(Dinner dinner) {
    db.Dinners.InsertOnSubmit(dinner);
  }

  public void Delete(Dinner dinner) {
    db.RSVPs.DeleteAllOnSubmit(dinner.RSVPs);
    db.Dinners.DeleteOnSubmit(dinner);
  }
  
  //
  // Сохранение
  public void Save() {
    db.SubmitChanges();
  }
}

Получение, изменение, добавление и удаление данных, используя класс DinnerRepository


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

Примеры запросов


Следующий код возвращает один ужин(Dinner) используя значение DinnerID:
DinnerRepository dinnerRepository = new DinnerRepository();

// Возвращение определенного ужина по DinnerID
Dinner dinner = dinnerRepository.GetDinner(5);


Код ниже, возвращает все предстоящие ужины и проходит циклом по каждому:
DinnerRepository dinnerRepository = new DinnerRepository();

// Возвращает все предстоящие ужины
var upcomingDinners = dinnerRepository.FindUpcomingDinners();

// Проходит циклом по каждому предстоящему ужину и выводит на экран его загаловок
foreach (Dinner dinner in upcomingDinners) {
  Response.Write("Title" + dinner.Title);
}


* This source code was highlighted with Source Code Highlighter.

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


Следующий код демонстрирует добавление двух новых ужинов. Любые добавления или изменения репозитория не применяются до тех пор, пока не вызовем метод “Save()”. LINQ to SQL автоматически обертывает все изменения в транзакцию базы данных, следовательно выполнятся или все изменения, или ни одного:

DinnerRepository dinnerRepository = new DinnerRepository();

// Создание первого ужина
Dinner newDinner1 = new Dinner();
newDinner1.Title = "Dinner with Scott";
newDinner1.HostedBy = "ScotGu";
newDinner1.ContactPhone = "425-703-8072";

// Создание второго ужина
Dinner newDinner2 = new Dinner();
newDinner2.Title = "Dinner with Bill";
newDinner2.HostedBy = "BillG";
newDinner2.ContactPhone = "425-555-5151";

// Добавления ужинов в репозиторий
dinnerRepository.Add(newDinner1);
dinnerRepository.Add(newDinner2);

// Сохранение изменений
dinnerRepository.Save();

Код ниже, возвращает существующий объект Dinner и изменят два его свойства. Свойства сохраняются в базу данных после вызова метода репозитория “Save()”:
DinnerRepository dinnerRepository = new DinnerRepository();

// Получаем определенный ужин по DinnerID
Dinner dinner = dinnerRepository.GetDinner(5);

// Обновляем свойства Dinner
dinner.Title = "Update Title";
dinner.HostedBy = "New Owner";

// Применяем изменения
dinnerRepository.Save();

Следующий код возвращает ужин и добавляет к нему RSVP. Он делает это через коллекцию RSVP объекта Dinner, которая была создана LINQ to SQL (на основе внешних ключей). Данные изменения будут отправлены в базу, как новая строчка таблицы RSVP, когда мы вызовем метод “Save()”:
DinnerRepository dinnerRepository = new DinnerRepository();

// Получаем определенный ужин по DinnerID
Dinner dinner = dinnerRepository.GetDinner(5);

// Создаем новый объект RSVP
RSVP myRSVP = new RSVP();
myRSVP.AttendeeName = "ScottGu";

// Добавляем RSVP в коллекцию RSVP объекта Dinner
dinner.RSVPs.Add(myRSVP);

// Применяем изменения
dinnerRepository.Save();

Пример удаления


Код ниже, возвращает существующий объект Dinner и далее помечает его, как удаленный. Когда мы вызовем метод “Save()” он удалится из базы:
DinnerRepository dinnerRepository = new DinnerRepository();

// Получаем определенный ужин по DinnerID
Dinner dinner = dinnerRepository.GetDinner(5);

// Помечаем объект на удаление
dinnerRepository.Delete(dinner);

// Применяем изменения
dinnerRepository.Save();

Интегрирование проверок и правил бизнес логики в классы моделей


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

Проверка схемы


Когда классы моделей объявлены с помощью конструктора LINQ to SQL, типы данных свойств класса модели соответствуют типам данных таблицы базы данных. Например: если колонка “EventData” в таблице Dinners типа “datetime”, то класс модели данных, созданный LINQ to SQL, будет типа “DateTime”, который является родным типом данных в .NET. В итоге, мы получим ошибки на этапе компиляции, если попытаемся присвоить данному свойству целое число или bool, или же произойдет ошибка, если вы попытаетесь неявно преобразовать строку неверного формата во время выполнения программы.

LINQ to SQL также автоматически обрабатывает escape-последовательности, помогая защитить приложение от SQL-инъекций.

Валидация и правила бизнес-логики


Валидация схемы полезна, как первый этап, но этого не достаточно. Многие реальные ситуации требуют возможность определить более подробный алгоритм для валидации, который может включать в себя несколько свойств, выполнение определенного кода, получение информации у модели о состоянии (например: это было создано/изменено/удалено или, согласно спецификации доменов, «заархивировано»). Существуют множество различных паттернов и фреймворков, которые можно использовать для определения и применения правил валидации для класса модели, все это реализовано у сторонних компонентов основанных на .NET. Вы можете использовать любой из них в ASP.NET MVC приложениях.

Целью нашего NerdDinner приложения, будет использование достаточно простого и доступного для понимания паттерна, где мы расширим свойство IsValid и метод GetRuleViolations() нашего объекта модели Dinner. Свойство IsValid будет возвращать true или false, в зависимости от успеха прохождения валидации и бизнес-правил. Метод GetRuleViolations() будет возвращать список возникших ошибок.

Мы реализуем IsValid и GetRuleViolations() для модели Dinner, добавив partial класс в наш проект. Partial классы используются для добавления методов/свойств/событий для классов, который управляются VS -конструктором (как класс Dinner, сгенерированный конструкторов LINQ to SQL) и помогают избежать хаоса в нашем коде. Мы можем добавить новый partial класс в наш проект, нажав правой кнопкой по папке \Models, выбрав пункт "Add New Item". Далее выбрав шаблон “Class” и назвать его Dinner.cs.



Нажав на кнопку "Add", файл Dinner.cs добавится в наш проект и откроется в IDE. Далее определим базовый фреймворк правил и валидации, используя следующий код:
public partial class Dinner {

  public bool IsValid {

    get { return (GetRuleViolations().Count() == 0); }

  }

  public IEnumerable<RuleViolation> GetRuleViolations() {

    yield break;

  }

  partial void OnValidate(ChangeAction action) {

    if (!IsValid)

      throw new ApplicationException("Rule violations prevent saving");

  }

}

public class RuleViolation {

  public string ErrorMessage { get; private set; }

  public string PropertyName { get; private set; }

  public RuleViolation(string errorMessage, string propertyName) {

    ErrorMessage = errorMessage;

    PropertyName = propertyName;

  }

}

Несколько замечаний по коду:
  • Класс Dinner начинается с ключевого слова “partial” – это означает, что размещенный в нем код, будет объединён с классом, сгенерированным LINQ to SQL конструктором и скомпилирован в единый класс.
  • Класс RuleViolation является вспомогательным классом, который позволяет нам предоставлять больше деталей о нарушениях правил.
  • Метод Dinner.GetRuleViolations() позволяет оценить, были ли соблюдены все правила и бизнес-логика (мы реализуем его буквально через пару минут). Он возвращает последовательность RuleViolation объектов, которые снабжают нас более детальной информацией о любой нарушении правил.
  • Dinner.IsValid – удобное вспомогательное свойство, которое указывает, есть ли активные RuleViolation или нет. Разработчик может проверить его, через объект Dinner в любое время (оно не вызовет никаких исключений).
Partial-метод Dinner.OnValidate() — хук, предоставленный LINQ to SQL, которые уведомляет нас в любое время, что объект Dinner готов к сохранению в базе данных. Наша реализация OnValidate(), удостоверяется, что Dinner не содержит никаких RuleViolation до сохранения. Если состояние не валидно, то выкинется исключение, которое прервет транзакцию LINQ to SQL.

Данный подход обеспечивает простой фреймворк, который интегрирует правила валидации и бизнес-правила. А теперь, давайте добавим правила к наш метод GetRuleViolations():
public IEnumerable<RuleViolation> GetRuleViolations() {

  if (String.IsNullOrEmpty(Title))
    yield return new RuleViolation("Title required","Title");

  if (String.IsNullOrEmpty(Description))
    yield return new RuleViolation("Description required","Description");

  if (String.IsNullOrEmpty(HostedBy))
    yield return new RuleViolation("HostedBy required", "HostedBy");

  if (String.IsNullOrEmpty(Address))
    yield return new RuleViolation("Address required", "Address");

  if (String.IsNullOrEmpty(Country))
    yield return new RuleViolation("Country required", "Country");

  if (String.IsNullOrEmpty(ContactPhone))
    yield return new RuleViolation("Phone# required", "ContactPhone");

  if (!PhoneValidator.IsValidNumber(ContactPhone, Country))
    yield return new RuleViolation("Phone# does not match country", "ContactPhone");

  yield break;
}

Мы используем “yield return” C# для возврата любой последовательности RuleViolations. Первые шесть правил проверяют и предписывают, что string-свойства Dinner не могут быть равными null или пустыми. Последнее правило более интересное, оно вызывает вспомогательный метод PhoneValidator.IsValidNumber(), который мы можем добавить в наш проект для соответствия формата номера ContactPhone стране месторасположения обеда.

Для проверки телефонного номера, мы так же можем использовать регулярные выражения. Ниже представлена простая реализация PhoneValidator, который позволяет проверять телефон через Regex:
public class PhoneValidator {

  static IDictionary<string, Regex> countryRegex = new Dictionary<string, Regex>() {
      { "USA", new Regex("^[2-9]\\d{2}-\\d{3}-\\d{4}$")},
      { "UK", new Regex("(^1300\\d{6}$)|(^1800|1900|1902\\d{6}$)|(^0[2|3|7|8]{1}[0-9]{8}$)|(^13\\d{4}$)|(^04\\d{2,3}\\d{6}$)")},
      { "Netherlands", new Regex("(^\\+[0-9]{2}|^\\+[0-9]{2}\\(0\\)|^\\(\\+[0-9]{2}\\)\\(0\\)|^00[0-9]{2}|^0)([0-9]{9}$|[0-9\\-\\s]{10}$)")},
  };

  public static bool IsValidNumber(string phoneNumber, string country) {

    if (country != null && countryRegex.ContainsKey(country))
      return countryRegex[country].IsMatch(phoneNumber);
    else
      return false;
  }

  public static IEnumerable<string> Countries {
    get {

      return countryRegex.Keys;
    }
  }
}

Обработка нарушений валидации и бизнес-логики


Добавив в код валидацию и правила бизнес-логики, каждый раз создавая или изменяя объект Dinner, наши правила валидации будут принудительно задействованы в проверке.

Разработчики могут писать код, как показано ниже, заранее определяя валидный ли объект Dinner и получая список всех нарушений без всяких исключений:
Dinner dinner = dinnerRepository.GetDinner(5);

dinner.Country = "USA";
dinner.ContactPhone = "425-555-BOGUS";

if (!dinner.IsValid) {
  var errors = dinner.GetRuleViolations();
  // действия по устранению ошибки
}

Если мы попытаемся сохранить Dinner с невалидным состоянием, то после вызова метода Save() произойдет выброс исключения в DinnerRepository. Это произойдет, потому что LINQ to SQL вызовет наш partial-метод Dinner.OnValidate() до сохранения изменений, а ранее мы добавили в этот метод выброс исключения, если произойдут нарушения правил в объекте Dinner. Мы можем перехватить данное исключение и мгновенно вернуть список нарушений для исправления:
Dinner dinner = dinnerRepository.GetDinner(5);

try {
  dinner.Country = "USA";
  dinner.ContactPhone = "425-555-BOGUS";
  dinnerRepository.Save();
}
catch {
  var errors = dinner.GetRuleViolations();
  // действия по устранению ошибок
}

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

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

Следующий шаг


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

Давайте теперь добавим несколько контроллеров и представлений в проект.
Теги:
Хабы:
+8
Комментарии 1
Комментарии Комментарии 1

Публикации

Истории

Ближайшие события

Московский туристический хакатон
Дата 23 марта – 7 апреля
Место
Москва Онлайн
Геймтон «DatsEdenSpace» от DatsTeam
Дата 5 – 6 апреля
Время 17:00 – 20:00
Место
Онлайн