КЛАССЫ И МАППИНГИ
Уроки по FluentNHibernate c ASP.NET MVC и SQL Server. Часть 1
Часть 3. Отображение данных из таблицы (Операция LIST)
В предыдущей части были рассмотрены виды связей (один-к-одному, один-ко-многим, многие-ко-многим), а также один класс Book и его маппинг-класс BookMap. Во второй части обновим класс Book, создадим остальные классы и связи между ними, как это было изображено в предыдущей главе в Диаграмме баз данных, расположившейся над подзаголовком 1.3.1 Связи.
Небольшое объяснение
public virtual ISet<Genre> Genres { get; set; }
public virtual ISet<Author> Authors { get; set; }
Почему ISet<Class>, а не, к примеру, привычный многим IList<Class>? Если использовать вместо ISet — IList, и попробовать запустить проект, то разницы особой мы не заметим (Таблицы и классы создадутся). Но когда мы к классу Book LeftJoin-им одновременно таблицу Genre и Authors, да и еще пытаемся вывести неповторяющиеся записи из таблицы Book (Distinct Book.Id) в представление (View), Nhibernate выдаст исключение и ошибку.
Cannot simultaneously fetch multiple bags.
В таких случаях используем ISet, тем более множества для этого и предназначены (игнорируют дублирующие записи).
Отношение многие-ко-многим.
В NHibernate есть понятие, «главной» таблицы. Хотя отношения «многие-ко-многим» между таблицами “Book” и “Автор” равнозначны (У автора может быть много книг, у книги может быть множество авторов), Nhibernate требует, чтобы программист указывал таблицу, которая сохраняется второй (имеет метод .inverse()), то есть вначале будет создана/обновлена/удалена запись в таблице Book, а только потом в таблице Author.
Cascade.All означает выполнение каскадных операций при save-update и delete. То есть когда объект сохраняется, обновляется или удаляется, проверяются и создаются/обновляются/добавляются все зависимые объекты (Ps. Можно прописать вместо Cascade.All -> .Cascade.SaveUpdate().Cascade.Delete())
Метод .Table(«Book_Author»); создает «промежуточную» таблицу “Book_Author” в БД.
Отношение многие-к-одному, один-ко-многим.
Метод References применяется на стороне «Многие-к-одному», на другой стороне «Один-ко-многим» будет метод HasMany.
Отношение один-к-одному
Метод .Constrained() говорит NHibernate, что для записи из таблицы Book должна соответствовать запись из таблицы Mind (id таблицы Mind должен быть равен id таблицы Book)
Если сейчас запустить проект и посмотреть БД Bibilioteca, то появятся новые таблицы с уже сформированными связями.
Далее заполним созданные таблицы данными…
Для этого создадим тестовое приложение, которое будет сохранять данные в БД, обновлять и удалять их, изменив HomeController следующим образом (Ненужные участки кода комментируем):
Небольшое объяснение
Виды объединений
.JoinAlias(p => p.Genres, () => genreAl, JoinType.LeftOuterJoin)
Изменим представление следующим образом:
Проверив поочередно все операции, мы заметим, что:
Маппинг для классов, у которых есть наследование.
А как маппить классы у которых есть наследование? Допустим, имеем такой пример:
В принципе, ничего сложного в этом маппинге нет, мы просто создадим один маппинг для производного класса, то есть таблицы Triangle.
После запуска приложения, в БД Biblioteca появится следующая (пустая) таблица
Уроки по FluentNHibernate c ASP.NET MVC и SQL Server. Часть 1
Часть 3. Отображение данных из таблицы (Операция LIST)
В предыдущей части были рассмотрены виды связей (один-к-одному, один-ко-многим, многие-ко-многим), а также один класс Book и его маппинг-класс BookMap. Во второй части обновим класс Book, создадим остальные классы и связи между ними, как это было изображено в предыдущей главе в Диаграмме баз данных, расположившейся над подзаголовком 1.3.1 Связи.
Код классов и маппингов (С комментариями)
Класс Книга
Класс Автор
Класс Жанр
Класс Мнение:
Класс Цикл(Серия):
public class Book {
//Уникальный идентификатор
public virtual int Id { get; set; }
//Название
public virtual string Name { get; set; }
//Описание
public virtual string Description { get; set; }
//Оценка Мира фантастики
public virtual int MfRaiting { get; set; }
//Номера страниц
public virtual int PageNumber { get; set; }
//Ссылка на картинку
public virtual string Image { get; set; }
//Дата поступления книги (фильтр по новинкам!)
public virtual DateTime IncomeDate { get; set; }
//Жанр (Многие-ко-Многим)
//Почему ISet а не IList? Только одна коллекция (IList) может выбираться с помощью JOIN выборки, если нужно более одной коллекции для выборки JOIN, то лучше их преобразовать в коллекцию ISet
public virtual ISet<Genre> Genres { get; set; }
//Серия (Многие-к-одному)
public virtual Series Series { get; set; }
//Мнение и другое (Один-к-одному)
private Mind _mind;
public virtual Mind Mind {
get { return _mind ?? (_mind = new Mind()); }
set { _mind = value; }
}
//Автор (Многие-ко-многим)
public virtual ISet<Author> Authors { get; set; }
//Заранее инициализируем, чтобы исключение null не возникало.
public Book() {
//Неупорядочное множество (в одной таблице не может присутствовать две точь-в-точь одинаковые строки, в противном случае выбирает одну, а другую игнорирует)
Genres = new HashSet<Genre>();
Authors = new HashSet<Author>();
}
}
//Маппинг класса Book
public class BookMap : ClassMap<Book> {
public BookMap() {
Id(x => x.Id);
Map(x => x.Name);
Map(x => x.Description);
Map(x => x.MfRaiting);
Map(x => x.PageNumber);
Map(x => x.Image);
Map(x => x.IncomeDate);
//Отношение многие-ко-многим
HasManyToMany(x => x.Genres)
//Правила каскадирования All - Когда объект сохраняется, обновляется или удаляется, проверяются и
//создаются/обновляются/добавляются все зависимые объекты
.Cascade.SaveUpdate()
//Название промежуточной таблицы ДОЛЖНО быть как и у класса Genre!
.Table("Book_Genre");
HasManyToMany(x => x.Authors)
.Cascade.SaveUpdate()
.Table("Book_Author");
//Отношение многие к одному
References(x => x.Series);
//Отношение один-к-одному. Главный класс.
HasOne(x => x.Mind).Cascade.All().Constrained();
}
}
Класс Автор
public class Author {
public virtual int Id { get; set; }
//Имя-Фамилия
public virtual string Name { get; set; }
//Биография
public virtual string Biography { get; set; }
//Книжки
public virtual ISet<Book> Books { get; set; }
//Инициализация Авторов
public Author() {
Books=new HashSet<Book>();
}
}
//Маппинг Автора
public class AuthorMap : ClassMap<Author> {
public AuthorMap() {
Id(x => x.Id);
Map(x => x.Name);
Map(x => x.Biography);
//Отношение многие-ко-многим
HasManyToMany(x => x.Books)
//Правила каскадирования All - Когда объект сохраняется, обновляется или удаляется, проверяются и создаются/обновляются/добавляются все зависимые объекты
.Cascade.All()
//Владельцем коллекции явл. другой конец отношения (Book) и он будет сохранен первым.
.Inverse()
//Название промежуточной таблицы ДОЛЖНО быть как и у класса Book!
.Table("Book_Author");
}
}
Класс Жанр
public class Genre {
public virtual int Id { get; set; }
//Название жанра
public virtual string Name { get; set; }
//Английское название жанра
public virtual string EngName { get; set; }
//Книжки
public virtual ISet<Book> Books { get; set; }
//Инициализация книг
public Genre() {
Books=new HashSet<Book>();
}
}
//Маппинг жанра
public class GenreMap : ClassMap<Genre> {
public GenreMap() {
Id(x => x.Id);
Map(x => x.Name);
Map(x => x.EngName);
//Отношение многие-ко-многим
HasManyToMany(x => x.Books)
//Правила каскадирования All - Когда объект сохраняется, обновляется или удаляется, проверяются и создаются/обновляются/добавляются все зависимые объекты
.Cascade.All()
//Владельцем коллекции явл. другой конец отношения (Book) и он будет сохранен первым.
.Inverse()
//Название промежуточной таблицы ДОЛЖНО быть как и у класса Book!
.Table("Book_Genre");
}
}
Класс Мнение:
public class Mind {
public virtual int Id { get; set; }
//Мое мнение
public virtual string MyMind { get; set; }
//Мнение фантлаба
public virtual string MindFantLab { get; set; }
//Книга
public virtual Book Book { get; set; }
}
//Маппинг Мind
public class MindMap:ClassMap<Mind> {
public MindMap() {
Id(x => x.Id);
Map(x => x.MyMind);
Map(x => x.MindFantLab);
//Отношение один к одному
HasOne(x => x.Book);
}
}
Класс Цикл(Серия):
public class Series {
public virtual int Id { get; set; }
public virtual string Name { get; set; } //Я создал IList, а не ISet, потому что кроме Book, Series больше ни с чем не связана, хотя можно сделать и ISet
public virtual IList<Book> Books { get; set; }
//Инициализация книг.
public Series() {
Books = new List<Book>();
}
}
public class SeriesMap : ClassMap<Series> {
public SeriesMap() {
Id(x => x.Id);
Map(x => x.Name);
//Отношение один-ко-многим
HasMany(x => x.Books)
////Владельцем коллекции явл. другой конец отношения (Book) и он будет сохранен первым.
.Inverse()
}
}
Небольшое объяснение
public virtual ISet<Genre> Genres { get; set; }
public virtual ISet<Author> Authors { get; set; }
Почему ISet<Class>, а не, к примеру, привычный многим IList<Class>? Если использовать вместо ISet — IList, и попробовать запустить проект, то разницы особой мы не заметим (Таблицы и классы создадутся). Но когда мы к классу Book LeftJoin-им одновременно таблицу Genre и Authors, да и еще пытаемся вывести неповторяющиеся записи из таблицы Book (Distinct Book.Id) в представление (View), Nhibernate выдаст исключение и ошибку.
Cannot simultaneously fetch multiple bags.
В таких случаях используем ISet, тем более множества для этого и предназначены (игнорируют дублирующие записи).
Отношение многие-ко-многим.
Book | Author |
---|---|
HasManyToMany(x => x.Genres) .Cascade.SaveUpdate() .Table(«Book_Author»); |
HasManyToMany(x => x.Books) .Cascade.All() .Inverse() .Table(«Book_Author»); |
В NHibernate есть понятие, «главной» таблицы. Хотя отношения «многие-ко-многим» между таблицами “Book” и “Автор” равнозначны (У автора может быть много книг, у книги может быть множество авторов), Nhibernate требует, чтобы программист указывал таблицу, которая сохраняется второй (имеет метод .inverse()), то есть вначале будет создана/обновлена/удалена запись в таблице Book, а только потом в таблице Author.
Cascade.All означает выполнение каскадных операций при save-update и delete. То есть когда объект сохраняется, обновляется или удаляется, проверяются и создаются/обновляются/добавляются все зависимые объекты (Ps. Можно прописать вместо Cascade.All -> .Cascade.SaveUpdate().Cascade.Delete())
Метод .Table(«Book_Author»); создает «промежуточную» таблицу “Book_Author” в БД.
Отношение многие-к-одному, один-ко-многим.
Book | Series |
---|---|
References(x => x.Series).Cascade.SaveUpdate(); |
HasMany(x => x.Books) .Inverse(); |
Метод References применяется на стороне «Многие-к-одному», на другой стороне «Один-ко-многим» будет метод HasMany.
Отношение один-к-одному
Book | Mind |
---|---|
HasOne(x => x.Mind).Cascade.All().Constrained(); |
HasOne(x => x.Book); |
Метод .Constrained() говорит NHibernate, что для записи из таблицы Book должна соответствовать запись из таблицы Mind (id таблицы Mind должен быть равен id таблицы Book)
Если сейчас запустить проект и посмотреть БД Bibilioteca, то появятся новые таблицы с уже сформированными связями.
Далее заполним созданные таблицы данными…
Для этого создадим тестовое приложение, которое будет сохранять данные в БД, обновлять и удалять их, изменив HomeController следующим образом (Ненужные участки кода комментируем):
public ActionResult Index()
{
using (ISession session = NHibernateHelper.OpenSession()) {
using (ITransaction transaction = session.BeginTransaction()) {
//Создать, добавить
var createBook = new Book();
createBook.Name = "Metro2033";
createBook.Description = "Постапокалипсическая мистика";
createBook.Authors.Add(new Author { Name = "Глуховский" });
createBook.Genres.Add(new Genre { Name = "Постапокалипсическая мистика" });
createBook.Series = new Series { Name = "Метро" };
createBook.Mind = new Mind { MyMind = "Постапокалипсическая мистика" };
session.SaveOrUpdate(createBook);
//Обновить (По идентификатору)
//var series = session.Get<Series>(1);
//var updateBook = session.Get<Book>(1);
//updateBook.Name = "Metro2034";
//updateBook.Description = "Антиутопия";
//updateBook.Authors.ElementAt(0).Name = "Глуховский";
//updateBook.Genres.ElementAt(0).Name = "Антиутопия";
//updateBook.Series = series;
//updateBook.Mind.MyMind = "11111";
//session.SaveOrUpdate(updateBook);
//Удаление (По идентификатору)
//var deleteBook = session.Get<Book>(1);
//session.Delete(deleteBook);
transaction.Commit();
}
Genre genreAl = null; Author authorAl = null; Series seriesAl = null; Mind mindAl = null;
var books = session.QueryOver<Book>()
//Left Join с таблицей Genres
.JoinAlias(p => p.Genres, () => genreAl, JoinType.LeftOuterJoin)
.JoinAlias(p => p.Authors, () => authorAl, JoinType.LeftOuterJoin)
.JoinAlias(p => p.Series, () => seriesAl, JoinType.LeftOuterJoin)
.JoinAlias(p => p.Mind, () => mindAl, JoinType.LeftOuterJoin)
//Убирает повторяющиеся id номера таблицы Book.
.TransformUsing(Transformers.DistinctRootEntity).List();
return View(books);
}
}
Небольшое объяснение
- var books = session.QueryOver<Book>() — подобно выполнению скрипта SQL: Select * From Book;
- .JoinAlias(p => p.Genres, () => genreAl, JoinType.LeftOuterJoin) — подобно выполнению скрипта SQL:
SELECT *FROM Book
inner JOIN Book_Genre ON book.id = Book_Genre.Book_id
LEFT JOIN Genre ON Book_Genre.Genre_id = Genre.id - .TransformUsing(Transformers.DistinctRootEntity) — Подобно выполнению скрипта SQL: SELECT distinct Book.Id..., (убирает дублирующие записи с одинаковыми id)
Виды объединений
.JoinAlias(p => p.Genres, () => genreAl, JoinType.LeftOuterJoin)
- LeftOuterJoin — выбирает все записи из левой таблицы (Book), а затем присоединяет к ним записи правой таблицы (Genre). Если не найдена соответствующая запись в правой таблицы, отображает её как Null
- RightOuterJoin действует в противоположность LEFT JOIN — выбирает все записи из правой таблицы (Genre), а затем присоединяет к ним записи левой таблицы (Book)
- InnerJoin — выбирает только те записи из левой таблиц (Book) у которой есть соответствующая запись из правой таблицы (Genre), а затем присоединяет к ним записи из правой таблицы
Изменим представление следующим образом:
Представление index
@model IEnumerable<NhibernateMVC.Models.Book>
@{ Layout = null; }
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width" />
<title>Index</title>
<style>
th, td {
border: 1px solid;
}
</style>
</head>
<body>
<p>@Html.ActionLink("Create New", "Create")</p>
<table>
<tr>
<th>@Html.DisplayNameFor(model => model.Name)</th>
<th>@Html.DisplayNameFor(model => model.Mind)</th>
<th>@Html.DisplayNameFor(model => model.Series)</th>
<th>@Html.DisplayNameFor(model => model.Authors)</th>
<th>@Html.DisplayNameFor(model => model.Genres)</th>
<th>Операции</th>
</tr>
@foreach (var item in Model) {
<tr>
<td>@Html.DisplayFor(modelItem => item.Name)</td>
<td>@Html.DisplayFor(modelItem => item.Mind.MyMind)</td>
@{string strSeries = item.Series != null ? item.Series.Name : null;}
<td>@Html.DisplayFor(modelItem => strSeries)</td>
<td>
@foreach (var author in item.Authors) {
string strAuthor = author != null ? author.Name : null;
@Html.DisplayFor(modelItem => strAuthor) <br />
}
</td>
<td>
@foreach (var genre in item.Genres) {
string strGenre = genre!= null ? genre.Name : null;
@Html.DisplayFor(modelItem => strGenre) <br />
}
</td>
<td>
@Html.ActionLink("Edit", "Edit", new { id = item.Id }) |
@Html.ActionLink("Details", "Details", new { id = item.Id }) |
@Html.ActionLink("Delete", "Delete", new { id = item.Id })
</td>
</tr>
}
</table>
</body>
</html>
Проверив поочередно все операции, мы заметим, что:
- При операциях Create и Update обновляются все данные, связанные с таблицей Book (уберите Cascade=«save-update» или cascade=«all» и связанные данные не будут сохранены)
- При удалении удаляются данные из таблиц Book, Mind, Book_Author, а остальные данные не удаляются, потому что у них Cascade=«save-update»
- При удалении удаляются данные из таблиц Book, Mind, Book_Author, а остальные данные не удаляются, потому что у них Cascade=«save-update»
Маппинг для классов, у которых есть наследование.
А как маппить классы у которых есть наследование? Допустим, имеем такой пример:
//Класс Двумерных фигур
public class TwoDShape {
//Ширина
public virtual int Width { get; set; }
//Высота
public virtual int Height { get; set; }
}
//Класс треугольник
public class Triangle : TwoDShape {
//Идентификационный номер
public virtual int Id { get; set; }
//Вид треугольника
public virtual string Style { get; set; }
}
В принципе, ничего сложного в этом маппинге нет, мы просто создадим один маппинг для производного класса, то есть таблицы Triangle.
//Маппинг треугольника
public class TriangleMap : ClassMap<Triangle> {
public TriangleMap() {
Id(x => x.Id);
Map(x => x.Style);
Map(x => x.Height);
Map(x => x.Width);
}
}
После запуска приложения, в БД Biblioteca появится следующая (пустая) таблица
Комментарии (5)
HomoLuden
18.08.2015 11:31В основном будет код… и комментарии. И небольшое объяснение перед созданием таблиц. А то невероятно длинная статья получится.
А Вы код спрячьте в спойлеры и статья сильно сократится.
King_Lamer
18.08.2015 14:29+2Добавьте пожалуйста ссылку на вашу предыдущую статью, чтобы не надо было заходить к вам в профиль и смотреть, почему это вторая часть и где первая.
imbeat
Не очень понятно для чего статья. Ни вводной части, ни обозначения проблем, целей, ни резюме, ни выводов.
QSandrew
Хорошо… Исправлю и обновлю.