#c# #entity-framework #sqlite
#c# #entity-framework #sqlite
Вопрос:
Я пытаюсь заставить Entity Framework (6.4.4. новейшая версия на лето 2020) работать вместе с SQLite (1.0.113.1, также последняя версия на лето 2020).
Я нашел много информации о том, как это сделать, но эта информация не всегда была полезной, довольно часто они противоречили друг другу.
Теперь, когда я узнал, как это сделать, я решил кратко описать, как я это сделал.
В вопросе описываются классы и таблицы, в ответе будет описано, как это сделать.
Я описываю базу данных для школ, где в каждой школе ноль или более учеников и учителей (один ко многим), у каждого ученика и каждого учителя ровно один адрес (один к одному), учителя обучают ноль или более учеников, в то время как учеников обучают ноль или более учителей (многие ко многим)
Итак, у меня есть несколько таблиц:
- Простой: адреса
- Простое: школы
- Учащиеся с внешним ключом к школе, которую они посещают
- Учителя с внешним ключом к школе, в которой они преподают.
- TeachersStudents: таблица соединений для реализации отношения «многие ко многим» между учащимися и преподавателями
Классы:
Адрес и школа:
public class Address
{
public long Id { get; set; }
public string Street { get; set; }
public int Number { get; set; }
public string Ext { get; set; }
public string ExtraLine { get; set; }
public string PostalCode { get; set; }
public string City { get; set; }
public string Country { get; set; }
}
public class School
{
public long Id { get; set; }
public string Name { get; set; }
// Every School has zero or more Students (one-to-many)
public virtual ICollection<Student> Students { get; set; }
// Every School has zero or more Teachers (one-to-many)
public virtual ICollection<Teacher> Teachers { get; set; }
}
Преподаватели и студенты:
public class Teacher
{
public long Id { get; set; }
public string Name { get; set; }
// Every Teacher lives at exactly one Address
public long AddressId { get; set; }
public virtual Address Address { get; set; }
// Every Teacher teaches at exactly one School, using foreign key
public long SchoolId { get; set; }
public virtual School School { get; set; }
// Every Teacher Teaches zero or more Students (many-to-many)
public virtual ICollection<Student> Students { get; set; }
}
public class Student
{
public long Id { get; set; }
public string Name { get; set; }
// Every Student lives at exactly one Address
public long AddressId { get; set; }
public virtual Address Address { get; set; }
// Every Student attends exactly one School, using foreign key
public long SchoolId { get; set; }
public virtual School School { get; set; }
// Every Student is taught by zero or more Teachers (many-to-many)
public virtual ICollection<Teacher> Teachers { get; set; }
}
И, наконец, DbContext:
public class SchoolDbContext : DbContext
{
public DbSet<Address> Addresses { get; set; }
public DbSet<School> Schools { get; set; }
public DbSet<Student> Students { get; set; }
public DbSet<Teacher> Teachers { get; set; }
}
При использовании entity Framework вам не нужно определять таблицу соединений TeachersStudents в вашем DbContext. Конечно, это не означает, что вам это не понадобится.
Если вы используете Microsoft SQL server, этого было бы достаточно, чтобы позволить entity framework идентифицировать таблицы и отношения между таблицами.
Увы, с SQLite этого недостаточно.
Итак: как заставить это работать. Переходим к ответу!
Ответ №1:
Итак, я использовал Visual Studio для создания пустого решения и добавил проект DLL: SchoolSQLite. Чтобы проверить, работает ли это, я также добавил консольное приложение, которое будет обращаться к базе данных с помощью entity Framework.
Для полноты я добавил несколько модульных тестов. Это выходит за рамки этого ответа.
В проекте DLL, который я использовал References-Manage NUGET Packages
для поиска System.Data.SQLite
. Это версия, которая добавляет код, необходимый как для Entity Framework, так и для SQLite. При необходимости: обновите до новейшей версии.
Добавьте классы, описанные в вопросе: Address, School, Teacher, Student, SchoolDbContext.
Теперь начинается часть, которая показалась мне самой сложной: строка подключения в файле App.Config
вашего консольного приложения.
Чтобы заставить это работать, мне понадобились следующие части в App.Config:
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<configSections>
<!-- For more information on Entity Framework configuration, visit ... -->
<section name="entityFramework"
type="System.Data.Entity.Internal.ConfigFile.EntityFrameworkSection, EntityFramework,
Version=6.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"
requirePermission="false"/>
</configSections>
Позже в App.Config раздел EntityFramework:
<entityFramework>
<providers>
<provider invariantName="System.Data.SqlClient"
type="System.Data.Entity.SqlServer.SqlProviderServices, EntityFramework.SqlServer"/>
<provider invariantName="System.Data.SQLite.EF6"
type="System.Data.SQLite.EF6.SQLiteProviderServices, System.Data.SQLite.EF6"/>
</providers>
</entityFramework>
<system.data>
<DbProviderFactories>
<remove invariant="System.Data.SQLite.EF6" />
<add name="SQLite Data Provider"
invariant="System.Data.SQLite.EF6"
description=".NET Framework Data Provider for SQLite"
type="System.Data.SQLite.SQLiteFactory, System.Data.SQLite" />
</DbProviderFactories>
</system.data>
И, наконец, строка подключения. Файл, в котором находится моя база данных, является C:UsersHaraldDocumentsDbSchools.sqlite
. Конечно, вы можете выбрать свое собственное местоположение.
<connectionStrings>
<add name="SchoolDbContext"
connectionString="data source=C:UsersHaralDocumentsDbSchools.sqlite"
providerName="System.Data.SQLite.EF6" />
(может быть больше строк подключения к другим базам данных)
Это должно скомпилироваться, но вы пока не можете получить доступ к базе данных. Летом 2020 Entity Framework не создает таблицы, поэтому вам придется делать это самостоятельно.
As I thought this was part of the SchoolDbContext I added a method. For this you need a little knowledge of SQL, but I think you get the gist:
protected void CreateTables()
{
const string sqlTextCreateTables = @"
CREATE TABLE IF NOT EXISTS Addresses
(
Id INTEGER PRIMARY KEY NOT NULL,
Street TEXT NOT NULL,
Number INTEGER NOT NULL,
Ext TEXT,
ExtraLine TEXT,
PostalCode TEXT NOT NULL,
City TEXT NOT NULL,
Country TEXT NOT NULL
);
CREATE INDEX IF NOT EXISTS indexAddresses ON Addresses (PostalCode, Number, Ext);
CREATE TABLE IF NOT EXISTS Schools
(
Id INTEGER PRIMARY KEY NOT NULL,
Name TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS Students
(
Id INTEGER PRIMARY KEY NOT NULL,
Name TEXT NOT NULL,
AddressId INTEGER NOT NULL,
SchoolId INTEGER NOT NULL,
FOREIGN KEY(AddressId) REFERENCES Addresses(Id) ON DELETE NO ACTION,
FOREIGN KEY(SchoolId) REFERENCES Schools(Id) ON DELETE CASCADE
);
CREATE TABLE IF NOT EXISTS Teachers
(
Id INTEGER PRIMARY KEY NOT NULL,
Name TEXT NOT NULL,
AddressId INTEGER NOT NULL,
SchoolId INTEGER NOT NULL,
FOREIGN KEY(AddressId) REFERENCES Addresses(Id) ON DELETE NO ACTION,
FOREIGN KEY(SchoolId) REFERENCES Schools(Id) ON DELETE CASCADE
);
CREATE TABLE IF NOT EXISTS TeachersStudents
(
TeacherId INTEGER NOT NULL,
StudentId INTEGER NOT NULL,
PRIMARY KEY (TeacherId, StudentId)
FOREIGN KEY(TeacherId) REFERENCES Teachers(Id) ON DELETE NO ACTION,
FOREIGN KEY(StudentId) REFERENCES Students(Id) ON DELETE NO ACTION
)";
var connectionString = this.Database.Connection.ConnectionString;
using (var dbConnection = new System.Data.SQLite.SQLiteConnection(connectionString))
{
dbConnection.Open();
using (var dbCommand = dbConnection.CreateCommand())
{
dbCommand.CommandText = sqlTextCreateTables;
dbCommand.ExecuteNonQuery();
}
}
}
Some things are worth mentioning:
- Table Address has an extra index, so the search for an address using PostalCode House Number ( extension) will be faster. «What is your PostCode?» «Well it is 5473TB, house number 6». The index will immediately show the complete address.
- Although the SchoolDbcontext doesn’t mention the junction table TeachersStudents, I still need to create it. The combination [TeacherId, StudentId] will be unique, so this can be used as primary key
- If a School is removed, all its Teachers and Students need to be removed: ON DELETE CASCADE
- Если учитель покидает школу, учащиеся не должны пострадать. Если ученик покидает школу, учителя продолжают преподавать: ПРИ УДАЛЕНИИ НИКАКИХ ДЕЙСТВИЙ
Когда ваше приложение выполняет запрос entity Framework в первый раз после его запуска, вызывается метод OnModelCreating
. Итак, это хороший момент, чтобы проверить, существуют ли таблицы, и, если нет, создать их.
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
this.CreateTables();
Конечно, вы должны использовать OnModelCreating для информирования entity Framework о ваших таблицах и связях между таблицами. Это можно сделать после создания таблиц.
Продолжаем создавать модель:
this.OnModelCreatingTable(modelBuilder.Entity<Address>());
this.OnModelCreatingTable(modelBuilder.Entity<School>());
this.OnModelCreatingTable(modelBuilder.Entity<Teacher>());
this.OnModelCreatingTable(modelBuilder.Entity<Student>());
this.OnModelCreatingTableRelations(modelBuilder);
base.OnModelCreating(modelBuilder);
}
Для тех, кто знаком с entity Framework, моделирование этих таблиц довольно простое.
Адрес; пример простой таблицы
private void OnModelCreatingTable(EntityTypeConfiguration<Address> addresses)
{
addresses.ToTable(nameof(SchoolDbContext.Addresses)).HasKey(address => address.Id);
addresses.Property(address => address.Street).IsRequired();
addresses.Property(address => address.Number).IsRequired();
addresses.Property(address => address.Ext).IsOptional();
addresses.Property(address => address.ExtraLine).IsOptional();
addresses.Property(address => address.PostAlCode).IsRequired();
addresses.Property(address => address.City).IsRequired();
addresses.Property(address => address.Country).IsRequired();
// The extra index, for fast search on [PostalCode, Number, Ext]
addresses.HasIndex(address => new {address.PostAlCode, address.Number, address.Ext})
.HasName("indexAddresses")
.IsUnique();
}
Schools также прост:
private void OnModelCreatingTable(EntityTypeConfiguration<School> schools)
{
schools.ToTable(nameof(this.Schools))
.HasKey(school => school.Id);
schools.Property(school => school.Name)
.IsRequired();
}
Учителя и учащиеся: у них есть требуемый внешний ключ для школ, в каждой школе ноль или более учеников / преподавателей:
private void OnModelCreatingTable(EntityTypeConfiguration<Teacher> teachers)
{
teachers.ToTable(nameof(SchoolDbContext.Teachers))
.HasKey(teacher => teacher.Id);
teachers.Property(teacher => teacher.Name)
.IsRequired();
// Specify one-to-many to Schools using foreign key SchoolId
teachers.HasRequired(teacher => teacher.School)
.WithMany(school => school.Teachers)
.HasForeignKey(teacher => teacher.SchoolId);
}
private void OnModelCreatingTable(EntityTypeConfiguration<Student> students)
{
students.ToTable(nameof(SchoolDbContext.Students))
.HasKey(student => student.Id);
students.Property(student => student.Name)
.IsRequired();
// Specify one-to-many to Schools using foreign key SchoolId
students.HasRequired(student => student.School)
.WithMany(school => school.Students)
.HasForeignKey(student => student.SchoolId);
}
Примечание: по умолчанию: если школа удалена, это произойдет каскадно: все ее учителя и учащиеся будут удалены.
Осталось только одно отношение к таблице: таблица соединений. Если бы я хотел, я мог бы определить отношения «один ко многим» между школами и учителями, а также школами и учащимися также здесь. Я уже делал это при определении преподавателей и студентов. Поэтому они здесь не нужны. Я оставил код в качестве примера, если вы хотите разместить их здесь.
private void OnModelCreatingTableRelations(DbModelBuilder modelBuilder)
{
//// School <--> Teacher: One-to-Many
//modelBuilder.Entity<School>()
// .HasMany(school => school.Teachers)
// .WithRequired(teacher => teacher.School)
// .HasForeignKey(teacher => teacher.SchoolId)
// .WillCascadeOnDelete(true);
//// School <--> Student: One-To-Many
//modelBuilder.Entity<School>()
// .HasMany(school => school.Students)
// .WithRequired(student => student.School)
// .HasForeignKey(student => student.SchoolId)
// .WillCascadeOnDelete(true);
// Teacher <--> Student: Many-to-many
modelBuilder.Entity<Teacher>()
.HasMany(teacher => teacher.Students)
.WithMany(student => student.Teachers)
.Map(manyToMany =>
{
manyToMany.ToTable("TeachersStudents");
manyToMany.MapLeftKey("TeacherId");
manyToMany.MapRightKey("StudentId");
});
}
Здесь объясняется сопоставление «многие ко многим»
Теперь мы почти закончили. Все, что нам нужно сделать, это убедиться, что база данных не будет удалена и воссоздана заново. Обычно это делается в:
Database.SetInitializer<SchoolDbContext>(null);
Поскольку я хотел скрыть, что мы используем SQLite, я добавил это как метод в SchoolDbContext:
public class SchoolDbContext : DbContext
{
public static void SetInitializeNoCreate()
{
Database.SetInitializer<SchoolDbContext>(null);
}
public SchoolDbContext() : base() { }
public SchoolDbContext(string nameOrConnectionString) : base(nameOrConnectionString) { }
// etc: add the DbSets, OnModelCreating and CreateTables as described earlier
}
Иногда я вижу, что люди устанавливают инициализатор в конструкторе:
public SchoolDbContext() : base()
{
Database.SetInitializer<SchoolDbContext>(null);
}
However, this constructor will be called very often. I thought it a bit of a waste to do this every time.
Of course there are patterns to automatically set the initializer once when the SchoolDbContext is constructed for the first time. For simplicity I didn’t use them here.
The console app
static void Main(string[] args)
{
Console.SetBufferSize(120, 1000);
Console.SetWindowSize(120, 40);
Program p = new Program();
p.Run();
// just for some neat ending:
if (System.Diagnostics.Debugger.IsAttached)
{
Console.WriteLine();
Console.WriteLine("Fin");
Console.ReadKey();
}
}
Program()
{
// Set the database initializer:
SchoolDbContext.SetInitializeNoCreate();
}
And now the fun part: Add a School, add a Teacher and a Student and let the Teacher teach this Student.
void Run()
{
// Add a School:
School schoolToAdd = this.CreateRandomSchool();
long addedSchoolId;
using (var dbContext = new SchoolDbContext())
{
var addedSchool = dbContext.Schools.Add(schoolToAdd);
dbContext.SaveChanges();
addedSchoolId = addedSchool.Id;
}
Добавьте преподавателя:
Teacher teacherToAdd = this.CreateRandomTeacher();
teacherToAdd.SchoolId = addedSchoolId;
long addedTeacherId;
using (var dbContext = new SchoolDbContext())
{
var addedTeacher = dbContext.Teachers.Add(teacherToAdd);
dbContext.SaveChanges();
addedTeacherId = addedTeacher.Id;
}
Добавьте ученика.
Student studentToAdd = this.CreateRandomStudent();
studentToAdd.SchoolId = addedSchoolId;
long addedStudentId;
using (var dbContext = new SchoolDbContext())
{
var addedStudent = dbContext.Students.Add(studentToAdd);
dbContext.SaveChanges();
addedStudentId = addedStudent.Id;
}
Почти готово: только отношение «многие ко многим» между преподавателями и студентами:
Студент решает, что его должен учить учитель:
using (var dbContext = new SchoolDbContext())
{
var fetchedStudent = dbContext.Find(addedStudentId);
var fetchedTeacher = dbContext.Find(addedTeacherId);
// either Add the Student to the Teacher:
fetchedTeacher.Students.Add(fetchedStudent);
// or Add the Teacher to the Student:
fetchedStudents.Teachers.Add(fetchedTeacher);
dbContext.SaveChanges();
}
Я также пытался удалить учителей из школ и увидел, что это не повредило учащимся. Также, если ученик покидает школу, учителя продолжают преподавать. Наконец: если я удаляю школу, все ученики и учителя удаляются.
Итак, теперь я показал вам:
- простые таблицы, такие как адреса и школы;
- таблицы с отношениями «один ко многим»: преподаватели и учащиеся;
- отношения «многие ко многим»: студенты-преподаватели.
Есть одно отношение, которое я не показал: самоссылка: внешний ключ к другому объекту в той же таблице. Я не смог придумать хороший пример для этого в базе данных School. Если у кого-нибудь есть хорошая идея, пожалуйста, отредактируйте этот ответ и добавьте таблицу с самоссылками.
Надеюсь, это было полезно для вас.