КЛАССЫ И МАППИНГИ

Уроки по 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);
	}
}


Небольшое объяснение
  1. var books = session.QueryOver<Book>() — подобно выполнению скрипта SQL: Select * From Book;
  2. .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
  3. .TransformUsing(Transformers.DistinctRootEntity) — Подобно выполнению скрипта SQL: SELECT distinct Book.Id..., (убирает дублирующие записи с одинаковыми id)


Виды объединений
.JoinAlias(p => p.Genres, () => genreAl, JoinType.LeftOuterJoin)
  1. LeftOuterJoin — выбирает все записи из левой таблицы (Book), а затем присоединяет к ним записи правой таблицы (Genre). Если не найдена соответствующая запись в правой таблицы, отображает её как Null
  2. RightOuterJoin действует в противоположность LEFT JOIN — выбирает все записи из правой таблицы (Genre), а затем присоединяет к ним записи левой таблицы (Book)
  3. 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)


  1. imbeat
    18.08.2015 09:45
    +2

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


    1. QSandrew
      18.08.2015 10:14

      Хорошо… Исправлю и обновлю.


  1. HomoLuden
    18.08.2015 11:31

    В основном будет код… и комментарии. И небольшое объяснение перед созданием таблиц. А то невероятно длинная статья получится.

    А Вы код спрячьте в спойлеры и статья сильно сократится.


  1. King_Lamer
    18.08.2015 14:29
    +2

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


  1. QSandrew
    31.08.2015 18:35

    Статья изменена. Третья часть также будет изменена.