Создание сложной модели данных для приложения ASP.NET MVC, часть 2
Изменения, касающиеся сущности Course
В Models \ Course . cs замените ранее созданный код на:
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
namespace ContosoUniversity.Models
{
public class Course
{
[DatabaseGenerated(DatabaseGeneratedOption.None)]
[Display(Name = "Number")]
public int CourseID { get; set; }
[Required(ErrorMessage = "Title is required.")]
[MaxLength(50)]
public string Title { get; set; }
[Required(ErrorMessage = "Number of credits is required.")]
[Range(0,5,ErrorMessage="Number of credits must be between 0 and 5.")]
public int Credits { get; set; }
[Display(Name = "Department")]
public int DepartmentID { get; set; }
public virtual Department Department { get; set; }
public virtual ICollection<Enrollment> Enrollments { get; set; }
public virtual ICollection<Instructor> Instructors { get; set; }
}
}
Атрибут DatabaseGenerated
Атрибут DatabaseGenerated с параметром None, указанный для свойства CourseID , определяет то, что значение первичного ключа задаётся пользователем, а не генерируется базой данных.
[DatabaseGenerated(DatabaseGeneratedOption.None)]
[Display(Name = "Number")]
public int CourseID { get; set; }
По умолчанию Entity Framework предполагает автогенерацию первичных ключей базой данных, что и необходимо в большинстве ситуаций. Однако для сущности Course используются численные заданные пользователем значения, такие как 1000 для одного факультета, 2000 для другого и так далее.
Внешний ключ и Navigation Properties
Свойства-внешние ключи и navigation properties в сущности Course отражают следующие связи:
Курс ассоциирован с одним факультетом, таким образом, имеется внешний ключ DepartmentID и Department navigation property:
public int DepartmentID { get; set; }
public virtual Department Department { get; set; }
Курс может посещать неограниченное количество студентов, поэтому имеется Enrollments navigation property:
public virtual ICollection Enrollments { get; set; }
Курс может вестись различными преподавателями, поэтому имеется Instructors navigation property:
public virtual ICollection<Instructor> Instructors { get; set; }
Создание сущности Department
Создайте Models \ Department . cs со следующим кодом:
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
namespace ContosoUniversity.Models
{
public class Department
{
public int DepartmentID { get; set; }
[Required(ErrorMessage = "Department name is required.")]
[MaxLength(50)]
public string Name { get; set; }
[DisplayFormat(DataFormatString="{0:c}")]
[Required(ErrorMessage = "Budget is required.")]
[Column(TypeName="money")]
public decimal? Budget { get; set; }
[DisplayFormat(DataFormatString="{0:d}", ApplyFormatInEditMode=true)]
[Required(ErrorMessage = "Start date is required.")]
public DateTime StartDate { get; set; }
[Display(Name="Administrator")]
public int? InstructorID { get; set; }
public virtual Instructor Administrator { get; set; }
public virtual ICollection<Course> Courses { get; set; }
}
}
Атрибут Column
Ранее атрибут Column мы использовали для изменения маппинга имени столбца. В коде для сущности Department этот атрибут используется для изменения маппинга типа данных SQL, то есть столбец будет определён в базе данных с типом данных SQL Server:
[Column(TypeName="money")]
public decimal? Budget { get; set; }
Обычно это не нужно, потому что Entity Framework автоматически подбирает наиболее подходящий тип данных исходя из типа CLR, который определён для свойства. Допустим, CLR тип decimal станет SQL Server типом decimal. Но в данном случае вы точно знаете, что свойство будет содержать цифры, связанные с валютой, и тип money будет наиболее подходящим для этого свойства.
Внешний ключ и Navigation Properties
Внешними ключами и navigation properties отражены следующие связи:
Факультет может как содержать, так и не содержать администратора, и администратор всегда = преподаватель. Поэтому свойство InstructorID определено как внешний ключ для сущности Instructor, и знак вопроса после типа int указывает на то, что свойство может быть nullable. Navigation property Administrator содержит сущность Instructor:
public int? InstructorID { get; set; }
public virtual Instructor Administrator { get; set; }
Факультет может иметь множество курсов, поэтому присутствует Courses navigation property:
public virtual ICollection Courses { get; set; }
Note Конвенциями определено, что Entity Framework каскадно удаляет non-nullable внешние ключи и в случаях связи многие-ко-многим. Это может привести к итеративному каскадному удалению, вызвав исключение при запуске кода. Допустим, если не определить Department.InstructorID как nullable, вы получите следующее исключение при: "The referential relationship will result in a cyclical reference that's not allowed."
Изменения, связанные с сущностью Student
В Models \ Student . cs замените код на:
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
namespace ContosoUniversity.Models
{
public class Student
{
public int StudentID { get; set; }
[Required(ErrorMessage = "Last name is required.")]
[Display(Name="Last Name")]
[MaxLength(50)]
public string LastName { get; set; }
[Required(ErrorMessage = "First name is required.")]
[Column("FirstName")]
[Display(Name = "First Name")]
[MaxLength(50)]
public string FirstMidName { get; set; }
[Required(ErrorMessage = "Enrollment date is required.")]
[DisplayFormat(DataFormatString = "{0:d}", ApplyFormatInEditMode = true)]
[Display(Name = "Enrollment Date")]
public DateTime? EnrollmentDate { get; set; }
public string FullName
{
get
{
return LastName + ", " + FirstMidName;
}
}
public virtual ICollection<Enrollment> Enrollments { get; set; }
}
}
Таким образом мы добавили атрибуты, которые уже были обсуждены ранее для других классов.
Изменения, касающиеся сущности Enrollment
В Models \ Enrollment . cs замените код на:
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
namespace ContosoUniversity.Models
{
public class Enrollment
{
public int EnrollmentID { get; set; }
public int CourseID { get; set; }
public int StudentID { get; set; }
[DisplayFormat(DataFormatString="{0:#.#}",ApplyFormatInEditMode=true,NullDisplayText="No grade")]
public decimal? Grade { get; set; }
public virtual Course Course { get; set; }
public virtual Student Student { get; set; }
}
}
Внешние ключи и Navigation Properties
Внешние ключи и navigation properties отражают следующие связи:
Каждой сущности записи на курс соответствует один курс, поэтому присутствует внешний ключ CourseID и Course navigation property:
public int CourseID { get; set; }
public virtual Course Course { get; set; }
Каждой сущности записи на курс соответствует один студент, поэтому присутствует внешний ключ StudentID и Student navigation property:
public int StudentID { get; set; }
public virtual Student Student { get; set; }
Связи многие-ко-многим
Сущности Student и Course связаны друг с другом связью многие-ко-многим, и сущность Enrollment соответствует and the Enrollment entity corresponds to a many-to-many join table with payload in the database. Это значит, что таблица Enrollment содержит дополнительные данные помимо внешних ключей для объединённых таблиц (в нашем случае первичный ключ и свойство Grade).
На изображении ниже представлены связи в виде диаграммы сущностей, сгенерированной Entity Framework designer.
Линия каждой связи имеет 1 на одном конце и * на другом, обозначая связь один-ко-многим.
Если таблица Enrollment не содержит информацию об оценках, необходимо иметь только два внешних ключа CourseID и StudentID. In that case, it would correspond to a many-to-many join table without payload (or a pure join table) in the database, and you wouldn't have to create a model class for it at all. Сущности Instructor и Course связаны подобной связью многие-ко-многим, и, как вы видите, между ними нет класса сущности:
Хотя объединённая таблица необходима:
Entity Framework автоматически создаёт таблицу CourseInstructor, доступ к которой осуществляется косвенно, а именно через Instructor.Courses и Course.Instructors navigation properties.
Атрибут DisplayFormat
Атрибут DisplayFormat для свойства Grade определяет форматирование для элемента:
[DisplayFormat(DataFormatString="{0:#.#}",ApplyFormatInEditMode=true,NullDisplayText="No grade")]
public decimal? Grade { get; set; }
Оценка выводится в формате "3.5" или "4.0".
Такое же форматирование оценка имеет и в режиме.
Если оценки нет, выводится надпись "No grade".
Связи на диаграмме сущностей
Диаграмма ниже демонстрирует систему связей для модели School.
Кроме связей многие-ко-многим (*-*) и один-ко-многим (1-*), можно также увидеть связь один-к-нулю-или-одному (1-0..1) между сущностями Instructor и OfficeAssignment и нуль-к-одному-или-ко-многим (0..1 - *) Department и Instructor.
Настройка контекста базы данных
Дальше мы добавим новые сущности в класс SchoolContext и настроим мапинг. В некоторых случаях необходимо будет использовать методы вместо атрибутов из-за того, что для некоторой функциональности атрибутов просто не сущнствует. В других случаях можно выбирать, использовать методы или атрибуты (некоторые люди предпочитают не использовать атрибуты).
Замените код в DAL \ SchoolContext . cs на:
using System;
using System.Collections.Generic;
using System.Data.Entity;
using ContosoUniversity.Models;
using System.Data.Entity.ModelConfiguration.Conventions;
namespace ContosoUniversity.Models
{
public class SchoolContext : DbContext
{
public DbSet<Course> Courses { get; set; }
public DbSet<Department> Departments { get; set; }
public DbSet<Enrollment> Enrollments { get; set; }
public DbSet<Instructor> Instructors { get; set; }
public DbSet<Student> Students { get; set; }
public DbSet<OfficeAssignment> OfficeAssignments { get; set; }
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
modelBuilder.Conventions.Remove<PluralizingTableNameConvention>();
modelBuilder.Entity<Instructor>()
.HasOptional(p => p.OfficeAssignment).WithRequired(p => p.Instructor);
modelBuilder.Entity<Course>()
.HasMany(c => c.Instructors).WithMany(i => i.Courses)
.Map(t => t.MapLeftKey("CourseID")
.MapRightKey("InstructorID")
.ToTable("CourseInstructor"));
modelBuilder.Entity<Department>()
.HasOptional(x => x.Administrator);
}
}
}
В методе OnModelCreating определяются следующие связи:
Один-к-нулю-или-одному между Instructor и OfficeAssignment:
modelBuilder.Entity<Instructor>()
.HasOptional(p => p.OfficeAssignment).WithRequired(p => p.Instructor);
Многие-ко-многим между Instructor и Course. Код определяет таблицу и столбцы для объединённой таблицы. Code First может настроить связь многие-ко-многим и без кода, но если вы его не вызовете, то для столбцов будут взяты стандартные имена, такие как InstructorInstructorID для InstructorID.
modelBuilder.Entity<Course>()
.HasMany(c => c.Instructors).WithMany(i => i.Courses)
.Map(t => t.MapLeftKey("CourseID")
.MapRightKey("InstructorID")
.ToTable("CourseInstructor"));
Один-к-нулю-или-одному между Department и Instructor, с помощью Department.Administrator navigation property:
modelBuilder.Entity<Department>()
.HasOptional(x => x.Administrator);
Для подробной информации о том, что делается «за сценой», можно прочитать Fluent API в блоге ASP.NET User Education Team.
Заполнение базы данных тестовыми данными
Перед этим вы создавали DAL \ SchoolInitializer . cs для заполнения базы тестовыми данными. Теперь замените старый код на новый, который учитывает присутствие новых сущностей.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Data.Entity;
using ContosoUniversity.Models;
namespace ContosoUniversity.DAL
{
public class SchoolInitializer : DropCreateDatabaseIfModelChanges<SchoolContext>
{
protected override void Seed(SchoolContext context)
{
var students = new List<Student>
{
new Student { FirstMidName = "Carson", LastName = "Alexander", EnrollmentDate = DateTime.Parse("2005-09-01") },
new Student { FirstMidName = "Meredith", LastName = "Alonso", EnrollmentDate = DateTime.Parse("2002-09-01") },
new Student { FirstMidName = "Arturo", LastName = "Anand", EnrollmentDate = DateTime.Parse("2003-09-01") },
new Student { FirstMidName = "Gytis", LastName = "Barzdukas", EnrollmentDate = DateTime.Parse("2002-09-01") },
new Student { FirstMidName = "Yan", LastName = "Li", EnrollmentDate = DateTime.Parse("2002-09-01") },
new Student { FirstMidName = "Peggy", LastName = "Justice", EnrollmentDate = DateTime.Parse("2001-09-01") },
new Student { FirstMidName = "Laura", LastName = "Norman", EnrollmentDate = DateTime.Parse("2003-09-01") },
new Student { FirstMidName = "Nino", LastName = "Olivetto", EnrollmentDate = DateTime.Parse("2005-09-01") }
};
students.ForEach(s => context.Students.Add(s));
context.SaveChanges();
var instructors = new List<Instructor>
{
new Instructor { FirstMidName = "Kim", LastName = "Abercrombie", HireDate = DateTime.Parse("1995-03-11") },
new Instructor { FirstMidName = "Fadi", LastName = "Fakhouri", HireDate = DateTime.Parse("2002-07-06") },
new Instructor { FirstMidName = "Roger", LastName = "Harui", HireDate = DateTime.Parse("1998-07-01") },
new Instructor { FirstMidName = "Candace", LastName = "Kapoor", HireDate = DateTime.Parse("2001-01-15") },
new Instructor { FirstMidName = "Roger", LastName = "Zheng", HireDate = DateTime.Parse("2004-02-12") }
};
instructors.ForEach(s => context.Instructors.Add(s));
context.SaveChanges();
var departments = new List<Department>
{
new Department { Name = "English", Budget = 350000, StartDate = DateTime.Parse("2007-09-01"), InstructorID = 1 },
new Department { Name = "Mathematics", Budget = 100000, StartDate = DateTime.Parse("2007-09-01"), InstructorID = 2 },
new Department { Name = "Engineering", Budget = 350000, StartDate = DateTime.Parse("2007-09-01"), InstructorID = 3 },
new Department { Name = "Economics", Budget = 100000, StartDate = DateTime.Parse("2007-09-01"), InstructorID = 4 }
};
departments.ForEach(s => context.Departments.Add(s));
context.SaveChanges();
var courses = new List<Course>
{
new Course { CourseID = 1050, Title = "Chemistry", Credits = 3, DepartmentID = 3, Instructors = new List<Instructor>() },
new Course { CourseID = 4022, Title = "Microeconomics", Credits = 3, DepartmentID = 4, Instructors = new List<Instructor>() },
new Course { CourseID = 4041, Title = "Macroeconomics", Credits = 3, DepartmentID = 4, Instructors = new List<Instructor>() },
new Course { CourseID = 1045, Title = "Calculus", Credits = 4, DepartmentID = 2, Instructors = new List<Instructor>() },
new Course { CourseID = 3141, Title = "Trigonometry", Credits = 4, DepartmentID = 2, Instructors = new List<Instructor>() },
new Course { CourseID = 2021, Title = "Composition", Credits = 3, DepartmentID = 1, Instructors = new List<Instructor>() },
new Course { CourseID = 2042, Title = "Literature", Credits = 4, DepartmentID = 1, Instructors = new List<Instructor>() }
};
courses.ForEach(s => context.Courses.Add(s));
context.SaveChanges();
courses[0].Instructors.Add(instructors[0]);
courses[0].Instructors.Add(instructors[1]);
courses[1].Instructors.Add(instructors[2]);
courses[2].Instructors.Add(instructors[2]);
courses[3].Instructors.Add(instructors[3]);
courses[4].Instructors.Add(instructors[3]);
courses[5].Instructors.Add(instructors[3]);
courses[6].Instructors.Add(instructors[3]);
context.SaveChanges();
var enrollments = new List<Enrollment>
{
new Enrollment { StudentID = 1, CourseID = 1050, Grade = 1 },
new Enrollment { StudentID = 1, CourseID = 4022, Grade = 3 },
new Enrollment { StudentID = 1, CourseID = 4041, Grade = 1 },
new Enrollment { StudentID = 2, CourseID = 1045, Grade = 2 },
new Enrollment { StudentID = 2, CourseID = 3141, Grade = 4 },
new Enrollment { StudentID = 2, CourseID = 2021, Grade = 4 },
new Enrollment { StudentID = 3, CourseID = 1050 },
new Enrollment { StudentID = 4, CourseID = 1050, },
new Enrollment { StudentID = 4, CourseID = 4022, Grade = 4 },
new Enrollment { StudentID = 5, CourseID = 4041, Grade = 3 },
new Enrollment { StudentID = 6, CourseID = 1045 },
new Enrollment { StudentID = 7, CourseID = 3141, Grade = 2 },
};
enrollments.ForEach(s => context.Enrollments.Add(s));
context.SaveChanges();
var officeAssignments = new List<OfficeAssignment>
{
new OfficeAssignment { InstructorID = 1, Location = "Smith 17" },
new OfficeAssignment { InstructorID = 2, Location = "Gowan 27" },
new OfficeAssignment { InstructorID = 3, Location = "Thompson 304" },
};
officeAssignments.ForEach(s => context.OfficeAssignments.Add(s));
context.SaveChanges();
}
}
}
Обратите внимание на обработку сущности Course, которая связана связью многие-ко-многим с сущностью Instructor:
var courses = new List
{
new Course { CourseID = 1050, Title = "Chemistry", Credits = 3, DepartmentID = 3, Instructors = new List() },
...
};
courses.ForEach(s => context.Courses.Add(s));
context.SaveChanges();
courses[0].Instructors.Add(instructors[0]);
...
context.SaveChanges();
При создании объекта Course как пустая коллекция (Instructors = new List()) , что делает возможным добавление сущностей Instructor, связанных с Course, с помощью метода Instructor.Add(). Если вы не создали пустой List, у вас не получится добавлять подобные отношения, потому что свойство Instructors будет равно null и не будет иметь метода Add.
Note Не забывайте перед развертыванием проекта на production-сервер удалять весь код инициализации базы.
Удаление и пересоздание базы данных
Запустите проект и выберите страницу Student Index.
Страница выглядит также как выглядела раньше, но «за сценой» база данных была удалена и пересоздана.
Если страница не открывается или вы получаете ошибку о том, что файл School.sdf уже используется (изображение ниже), необходимо еще раз открыть Server Explorer и закрыть подключение к базе и затем попробовать снова открыть страницу.
После этого откройте базу в Server Explorer и посмотрите в Tables новые таблицы.
Кроме EdmMetadata обратите внимание на таблицу, для которой вы не создавали класса CourseInstructor. Это объединённая из Instructor и Course таблица.
Щелкните на таблице CourseInstructor и нажмите Show Table Data чтобы убедиться в наличии данных, добавленных ранее Course.Instructors navigation property.
Теперь у вас есть сложная модель данных и соответствующая база данных. Дальше мы научимся разным способам обращения к данным.
--
Это перевод оригинальной статьи Creating a More Complex Data Model for an ASP.NET MVC Application. Благодарим за помощь в переводе Александра Белоцерковского.