Entity Framework和SQLite,终极教程

7

我正在尝试让Entity Framework(版本为6.4.4,是2020年夏季的最新版本)与SQLite(版本为1.0.113.1,同样是2020年夏季的最新版本)一起工作。

我找到了很多关于如何做到这一点的信息,但这些信息并不总是有用的,经常互相矛盾。

现在我已经找到了如何实现它的方法,决定记录下来。

问题描述了类和表,答案将描述如何实现。

我描述了一个学校数据库,每个学校都有零个或多个学生和老师(一对多),每个学生和每个老师都有一个地址(一对一),老师教零个或多个学生,而学生被零个或多个老师教授(多对多)。

所以我有几个表:

  • 一个简单的:Addresses
  • 一个简单的:Schools
  • 具有指向他们就读学校的外键的学生
  • 具有指向他们所授课学校的外键的老师。
  • TeachersStudents:实现学生和老师之间多对多关系的连接表

类:

Address和School:

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; }
}

在使用实体框架时,您不需要在DbContext中定义联接表TeachersStudents。当然这并不意味着您不需要它。

如果您使用Microsoft SQL Server,这将足以让实体框架识别表和表之间的关系。

遗憾的是,对于SQLite来说,这还不够。

那么,如何使其工作?接下来给出答案!

1个回答

11

我使用Visual Studio创建了一个空解决方案,并添加了一个DLL项目:SchoolSQLite。为了验证是否正常工作,我还添加了一个控制台应用程序,使用实体框架访问数据库。

为了完整起见,我添加了一些单元测试。这超出了本回答的范围。

在DLL项目中,我使用“引用-管理NUGET包”来搜索“System.Data.SQLite”。这是添加实体框架和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:\Users\Harald\Documents\DbSchools.sqlite,当然您可以选择自己的位置。

<connectionStrings>
  <add name="SchoolDbContext"
     connectionString="data source=C:\Users\Haral\Documents\DbSchools.sqlite"
     providerName="System.Data.SQLite.EF6" />

(可能还有连接到其他数据库的更多连接字符串)

这样编译应该是没问题的,但你还不能访问数据库。2020年夏季实体框架不会创建表,所以你需要自己来完成这个过程。

由于我认为这部分属于SchoolDbContext,所以我添加了一个方法。为此,你需要一些关于SQL的知识,但我想你能理解大意:

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();
        }
    }
}

有几点值得一提:

  • Table Address拥有额外的索引,所以使用PostalCode + House Number (+ extension)来搜索地址会更快。“你的邮政编码是什么?”“嗯,它是5473TB,6号房子”。索引将立即显示完整地址。
  • 虽然SchoolDbcontext没有提及junction table TeachersStudents,但我仍需要创建它。组合[TeacherId,StudentId]将是唯一的,因此可以用作主键。
  • 如果删除一个学校,则需要删除其所有教师和学生:ON DELETE CASCADE
  • 如果一个老师离开学校,那么学生不应该受到伤害。如果一个学生离开学校,那么老师仍然继续教学:ON DELETE NO ACTION

当您的应用程序在启动后第一次执行实体框架查询时,方法OnModelCreating将被调用。因此,这是检查表是否存在,如果不存在,则创建它们的好时机。

protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
    this.CreateTables();

当然,您应该使用 OnModelCreating 来告知实体框架有关表以及表之间关系的信息。这可以在创建表后完成。

继续 OnModelCreating:

    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);
}

对于熟悉实体框架的人来说,对这些表进行建模是相当简单的。

Address;一个简单表的例子

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();
    }

学校也很简单:

    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);
}

然而,这个构造函数会被频繁调用。我认为每次都这样做有点浪费。

当然,有自动设置初始化程序的模式,可以在第一次构建SchoolDbContext时设置它们。为简单起见,我没有在这里使用它们。

控制台应用程序

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();
}

现在就开始愉快的部分:添加一个学校,添加一个老师和一个学生,并让老师教授这个学生。

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();
}

我还尝试了从学校中删除老师,并发现这并没有伤害到学生。另外,如果一个学生离开学校,老师仍然会继续教学。最后:如果我删除一所学校,所有的学生和老师都将被删除。

现在我给你展示了:

  • 像地址和学校这样的简单表格;
  • 具有一对多关系的表格:教师和学生;
  • 具有多对多关系的表格:学生教师。

还有一种关系我没有展示:自引用关系:指向同一张表中另一个对象的外键。我无法在学校数据库中想出一个好的例子。如果有人有好的想法,请编辑此答案并添加自引用表格。

希望这对你有用。


肯定很有帮助,谢谢。我注意到即使在调用SaveChanges()之后,*.db文件仍然没有被写入,但是db-*文件却被更新了。你如何在不退出进程的情况下更新主数据库文件呢? - Taylor Kendall
我不知道。或许使用临时文件可以解决?我刚刚确认在我的程序中更改是被记录下来的,如果我重新启动程序,这些更改仍然存在。 - Harald Coppoolse
如果你在上下文构造函数中添加this.Database.ExecuteSqlRaw("PRAGMA journal_mode = 'delete';");,它将会将数据写入数据库而不是临时文件。你可以在这里找到更多信息。尽管这种方法不适用于高性能应用程序,但在我的情况下正是我所需要的。 - Taylor Kendall

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接