C# EF框架(一)配置

目錄

EF

EF簡介

相關知識複習

lambda表達式

集合常用擴展方法

高級集合擴展方法

EF 的安裝

EF 簡單DataAnnotations 實體配置

EF 模型的兩種配置方式

FluentAPI 配置T_Persons 的方式

EF 的基本增刪改查

EF 原理及SQL 監控

執行原始SQL

不是所有lambda 寫法都能被支持

EF對象的狀態

簡介

應用(*)

EF優化的一個技巧

Fluent API更多配置

HasMaxLength設定字段的最大長度

(有用)字段是否可空

其他一般不用設置的(瞭解即可)

流動起來

一對多關係映射

配置一對多關係

一對多深入:

多對多關係配置


EF

EF簡介

  1. ORM:Object Relation Mapping ,通俗說:用操作對象的方式來操作數據庫。

  2. 插入數據庫不再是執行Insert,而是類似於Person p = new Person();p.Age=3;p.Name="英萊特";db.Save(p);這樣的做法。

  3. ORM工具有很多Dapper、PetaPoco、NHibernate,最首推的還是微軟官方的Entity Framework,簡稱EF。

  4. EF底層仍然是對ADO.Net的封裝。EF支持SQLServer、MYSQL、Oracle、Sqlite等所有主流數據庫。

  5. 使用EF進行數據庫開發的時候有兩個東西建:建數據庫(T_Persons),建模型類(Person)。根據這兩種創建的先後順序有EF的三種創建方法:

    DataBase First(數據庫優先):先創建數據庫表,然後自動生成EDM文件,EDM文件生成模型類。簡單展示一下DataBase First 的使用。

    Model First(模型優先):先創建Edm文件,Edm文件自動生成模型類和數據庫;

    Code First(代碼優先):程序員自己寫模型類,然後自動生成數據庫。沒有Edm。

    DataBase First 簡單、方便,但是當項目大了之後會非常痛苦;

    Code First 入門門檻高,但是適合於大項目。

    Model First……

    無論哪種First,一旦創建好了數據庫、模型類之後,後面的用法都是一樣的。業界都是推薦使用Code First,新版的EF中只支持Code First,因此我們這裏只講Code First。

  6. Code First的微軟的推薦用法是程序員只寫模型類,數據庫由EF 幫我們生成,當修改模型類之後,EF 使用“DB Migration”自動幫我們更改數據庫。但是這種做法太激進,不適合很多大項目的開發流程和優化,只適合於項目的初始開發階段。Java的Hibernate 中也有類似的DDL2SQL 技術,但是也是用的較少。“DB Migration”也不利於理解EF,因此在初學階段,我們將會禁用“DB Migration”,採用更實際的“手動建數據庫和模型類”的方式。

  7. 如果大家用過 NHibernate 等ORM 工具的話,會發現開發過程特別麻煩,需要在配置文件中指定模型類屬性和數據庫字段的對應關係,哪怕名字完全也一樣也要手動配置。使用過Java 中Struts、Spring 等技術的同學也有過類似“配置文件地獄”的感覺。 像ASP.Net MVC 一樣,EF 也是採用“約定大於配置”這樣的框架設計原則,省去了很多配置,能用約定就不要自己配置。

 

在.Net Framework SP1微軟包含一個實體框架(Entity Framework),此框架可以理解成微軟的一個ORM產品。用於支持開發人員通過對概念性應用程序模型編程(而不是直接對關係存儲架構編程)來創建數據訪問應用程序。目標是降低面向數據的應用程序所需的代碼量並減輕維護工作。

Entity Framework應用程序有以下優點:

  1. 應用程序可以通過更加以應用程序爲中心的概念性模型(包括具有繼承性、複雜成員和關係的類型)來工作。

  2. 應用程序不再對特定的數據引擎或存儲架構具有硬編碼依賴性。

  3. 可以在不更改應用程序代碼的情況下更改概念性模型與特定於存儲的架構之間的映射。

  4. 開發人員可以使用可映射到各種存儲架構(可能在不同的數據庫管理系統中實現)的一致的應用程序對象模型。

  5. 多個概念性模型可以映射到同一個存儲架構。

  6. 語言集成查詢支持可爲查詢提供針對概念性模型的編譯時語法驗證。

實體框架Entity Framework是 DO.NET中的一組支持開發面向數據的軟件應用程序的技術。在EF中的實體數據模型(EDM)由以下三種模型和具有相應文件擴展名的映射文件進行定義。

  1. 概念架構定義語言文件 (.csdl) -- 定義概念模型。

  2. 存儲架構定義語言文件 (.ssdl) -- 定義存儲模型(又稱邏輯模型)。

  3. 映射規範語言文件 (.msl) -- 定義存儲模型與概念模型之間的映射。

實體框架使用這些基於XML的模型和映射文件將對概念模型中的實體和關係的創建、讀取、更新和刪除操作轉換爲數據源中的等效操作。EDM甚至支持將概念模型中的實體映射到數據源中的存儲過程。它提供以下方式用於查詢 EDM 並返回對象:

  1. LINQ to Entities--提供語言集成查詢(LINQ)支持用於查詢在概念模型中定義的實體類型。

  2. Entity SQL -- 與存儲無關的SQL方言,直接使用概念模型中的實體並支持諸如繼承和關係等 EDM 功能。

  3. 查詢生成器方法 --可以使用LINQ風格的查詢方法構造 Entity SQL 查詢。

相關知識複習

  1. var類型推斷:var p =new Person();

  2. 匿名類型。var a =new {p.Name,Age=5,Gender=p.Gender,Name1=a.Name};//{p.Name}=={Name=p.Name}

  3. 給新創建對象的屬性賦值的簡化方法:Person p = new Person{Name="tom",Age=5};等價於Person p = new Person();p.Name="tom";p.Age=5;

  4. lambda表達式:

lambda表達式

函數式編程,在Entity framework編程中用的很多

Action<int> al= delegate(int i) { Console.Writeline(i); };

可以簡化成(=>讀作goes to) :

Action< int> a2 = (inti) = > { Console.Writeline(i); };

還可以省略參數類型(編譯器會自動根據委託類型推斷):

Action< int> a3 = (i) = > { Console.Writeline(i); };

如果只有一個參數還可以省略參數的小括號(多個參數不行)

Action<int> a4 = i = > { Console.Writeline(i); };

如果委託有返回值,並且方法體只有一行代碼,這一行代碼還是返回值,那麼就可以連方法的大括號和return都省略:

Func<int,int,string> fl= delegate(int i, int j) { return "結果是" + (i + j); };
Func<int,int,string> f2= (i,j)=>"結果是"+ (i+ j);

集合常用擴展方法

where (支持委託)、Select (支持委託)、Max 、Min 、OrderBy

First (獲取第一個,如果一個都沒有則異常)

FirstOrDefault (獲取第一個,如果—個都沒有則返回默認值)

Single (獲取唯一一個,如果沒有或者有多個則異常)

SingleOrDefoult (獲取唯一一個, 如果沒有則返回默認值,如果有多個則異常)

注意lambda中照樣要避免變量重名的問題:var p =persons.Where(p => p.Name =="yltedu.com").First();

高級集合擴展方法

//學生
public class Person
{
public string Name { get; set; }
public int Age { get; set; }
public bool Gender { get; set; }
public int Salary { get; set; }
public override string ToString()
{
return string.Format("Name={0},Age={1},Gender={2},Salary={3}",Name, Age, Gender, Salary);
}
}
​
//老師
public class Teacher
{
public Teacher()
{
this.Students=new List<Person>();
}
public string Name { get; set; }
public List<Person> Students { get; set; }
}
var s0 =new Person { Name="tom",Age=3,Gender=true,Salary=6000};
var s1 = new Person { Name = "jerry", Age = 8, Gender = true, Salary = 5000 };
var s2 = new Person { Name = "jim", Age = 3, Gender = true, Salary = 3000 };
var s3 = new Person { Name = "lily", Age = 5, Gender = false, Salary = 9000 };
var s4 = new Person { Name = "lucy", Age = 6, Gender = false, Salary = 2000 };
var s5 = new Person { Name = "kimi", Age = 5, Gender = true, Salary = 1000 };
List<Person> list = new List<Person>();
list.Add(s0);
list.Add(s1);
list.Add(s2);
list.Add(s3);
list.Add(s4);
list.Add(s5);
Teacher t1 = new Teacher { Name="英萊特.net"};
t1.Students.Add(s1);
t1.Students.Add(s2);
Teacher t2 = new Teacher { Name = "英萊特Python" };
t2.Students.Add(s2);
t2.Students.Add(s3);
t2.Students.Add(s5);
Teacher[] teachers = { t1,t2};
  1. Any(),判斷集合是否包含元素,返回值是bool,一般比Cout()>0 效率高。Any還可以指定條件表達式。

    bool b = list.Any(p => p.Age > 50); 等價於bool b =list.Where(p=>p.Age>50).Any();

     

  2. Distinct(),剔除完全重複數據。(*)注意自定義對象的Equals 問題:需要重寫Equals 和GetHashCode 方法來進行內容比較。

  3. 排序:升序list.OrderBy(p=>p.Age);降序list.OrderByDescending(p=>p.Age)。指定多個排序規 則,而不是多個OrderBy,而是:list.OrderByDescending(p=>p.Age).ThenBy(p=>p.Salary),也支 持ThenByDescending()。注意這些操作不會影響原始的集合數據。

  4. Skip(n)跳過前n條數據;

  5. Take(n)獲取最多n條數據,如果不足n條也不會報錯。常用來分頁獲取數據。

  6. list.Skip(3).Take(2)跳過前3條數據獲取2條數據。

  7. Except(items1)排除當前集合中在items1中存在的元素。用int數組舉例。

  8. Union(items1)把當前集合和items1中組合。用int 數組舉例。

  9. Intersect(items1) 把當前集合和items1 中取交集。用int 數組舉例。

  10. 分組:

    foreach(var g in list.GroupBy(p => p.Age))
    {
        Console.WriteLine(g.Key+":"+g.Average(p=>p.Salary));
    }

     

  11. SelectMany:把集合中每個對象的另外集合屬性的值重新拼接爲一個新的集合

    foreach(var s in teachers.SelectMany(t => t.Students))
    {
        Console.WriteLine(s);//每個元素都是Person
    }
    }

    注意不會去重,如果需要去重要自己再次調用Distinct()

  12. Join

    class Master
    {
        public long Id { get; set; }
        public string Name { get; set; }
    }
    class Dog
    {
        public long Id { get; set; }
        public long MasterId { get; set; }
       public string Name { get; set; }
    }
    Master m1 = new Master { Id = 1, Name = "英萊特" };
    Master m2 = new Master { Id = 2, Name = "比爾蓋茨" };
    Master m3 = new Master { Id = 3, Name = "周星馳" };
    Master[] masters = { m1,m2,m3};
    Dog d1 = new Dog { Id = 1, MasterId = 3, Name = "旺財" };
    Dog d2 = new Dog { Id = 2, MasterId = 3, Name = "汪汪" };
    Dog d3 = new Dog { Id = 3, MasterId = 1, Name = "京巴" };
    Dog d4 = new Dog { Id = 4, MasterId = 2, Name = "泰迪" };
    Dog d5 = new Dog { Id = 5, MasterId = 1, Name = "中華田園" };
    Dog[] dogs = { d1, d2, d3, d4, d5 };

    Join 可以實現和數據庫一樣的Join 效果,對有關聯關係的數據進行聯合查詢 下面的語句查詢所有Id=1 的狗,並且查詢狗的主人的姓名。

    var result = dogs.Where(d => d.Id > 1).Join(masters, d => d.MasterId, m => m.Id,(d,m)=>new {DogName=d.Name,MasterName=m.Name});
    foreach(var item in result)
    {
        Console.WriteLine(item.DogName+","+item.MasterName);
    }

     

EF 的安裝

  1. 基礎階段用控制檯項目。使用NuGet 安裝EntityFramework。會自動在App.config中增加兩個entityFramework 相關配置段;

  2. 在 web.config 中配置連接字符串

    <add name="conn1" connectionString="Data Source=.;Initial Catalog=test1;UserID=sa;Password=123" providerName="System.Data.SqlClient" />

    易錯點:不能忘了寫providerName="System.Data.SqlClient"增加兩個entityFramework 相關配置段;

EF 簡單DataAnnotations 實體配置

  1. 數據庫中建表T_Perons,有Id(主鍵,自動增長)、Name、CreateDateTime字段。

  2. 創建Person類[Table("T_Persons")]因爲類名和表名不一樣,所以要使用Table標註

        [Table("T_Persons")]
        public class Person
        {
            public long ID { get; set; }
            public string Name { get; set; }
            public DateTime CreateTime { get; set; }
        }

    因爲EF約定主鍵字段名是Id,所以不用再特殊指定Id是主鍵,如果非要指定就指定[Key]。因爲字段名字和屬性名字一致,所以不用再特殊指定屬性和字段名的對應關係,如果需要特殊指定,則要用[Column("Name")]

    (*)必填字段標註[Required]、字段長度[MaxLength(5)]、可空字段用int?、如果字段在數據庫有默認值,則要在屬性上標註[DatabaseGenerated]注意實體類都要寫成public,否則後面可能會有麻煩。

  3. 創建DbContext類(模型類、實體類)

        public class MyDBContext: DbContext
        {
            //表示使用連接字符串中名字爲conn1 的去連接數據庫
            public MyDBContext() : base("name=strcon")
            {
    ​
            }
            //通過對Persons 集合的操作就可以完成對T_Persons的操作
            public DbSet<Person> Persons { get; set; }
        }

     

  4. 測試

         protected void Button1_Click(object sender, EventArgs e)
            {
                MyDBContext context = new MyDBContext();
                Person p=new Person();
                p.Name =TextBox1.Text;
                p.CreateTime = DateTime.Now;
                context.Persons.Add(p);
                context.SaveChanges();
            }

    注意:MyDbContext 對象是否需要using有爭議,不using也沒事。每次用的時候new MyDbContext就行,不用共享同一個實例,共享反而會有問題。SaveChanges()纔會把修改更新到數據庫中。

    EF的開發團隊都說要using DbContext,很多人不using,只是想利用LazyLoad 而已,但是那樣做是違反分層原則的。我的習慣還是using。

    異常的處理:如果數據有錯誤可能在SaveChanges()的時候出現異常,一般仔細查看異常信息或者一直深入一層層的鑽InnerException 就能發現錯誤信息。

    舉例:創建一個Person對象,不給Name、CreateDateTime賦值就保存。

EF 模型的兩種配置方式

EF 中的模型類的配置有DataAnnotations、FluentAPI 兩種。

上面這種在模型類上[Table("T_Persons")]、[Column("Name")]這種方式就叫DataAnnotations這種方式比較方便,但是耦合度太高,一般的類最好是POCO(Plain Old C# Object,沒有繼承什麼特殊的父類,沒有標註什麼特殊的Attribute,沒有定義什麼特殊的方法,就是一堆普通的屬性);不符合大項目開發的要求。微軟推薦使用FluentAPI 的使用方式,因此後面主要用FluentAPI 的使用方式。

FluentAPI 配置T_Persons 的方式

  1. 數據庫中建表T_Perons,有Id(主鍵,自動增長)、Name、CreateDateTime 字段。

  2. 創建 Person 類。模型類就是普通C#類

        public class Person
        {
            public long ID { get; set; }
            public string Name { get; set; }
            public DateTime CreateTime { get; set; }
        }

     

  3. 創建一個 PersonConfig 類,放到ModelConfig 文件夾下(PersonConfig、EntityConfig這樣的名字都不是必須的)

        public class PersonConfig : EntityTypeConfiguration<Person>
        {
            public PersonConfig()
            {
                this.ToTable("T_Person");
            }
        }

     

  4. 創建 DbContext 類

        public class MyDBContext:DbContext
        {
            public MyDBContext() : base("name=strcon")
            {
            }
            protected override void OnModelCreating(DbModelBuilder modelBuilder)
            {
                base.OnModelCreating(modelBuilder);
                modelBuilder.Configurations.AddFromAssembly(Assembly.GetExecutingAssembly());
            }
            public DbSet<Person> Persons { get; set; }
        }

    下面這句話:

    modelBuilder.Configurations.AddFromAssembly(Assembly.GetExecutingAssembly());

    代表從這句話所在的程序集加載所有的繼承自EntityTypeConfiguration 爲模型配置類。還有很多加載配置文件的做法(把配置寫到OnModelCreating中或者把加載的代碼寫死到OnModelCreating 中),但是這種做法是最符合大項目規範的做法。

    和以前唯一的不同就是:模型不需要標註Attribute;編寫一個XXXConfig類配置映射關係;DbContext 中override OnModelCreating;

  5. 測試

           protected void Button1_Click(object sender, EventArgs e)
            {
                MyDBContext context = new MyDBContext();
                Person p = new Person();
                p.Name = TextBox1.Text;
                p.CreateTime = DateTime.Now;
                context.Persons.Add(p);
                context.SaveChanges();
            }

     

EF 的基本增刪改查

獲取DbSet除了可以ctx.Persons之外,還可以ctx.Set()。

  1. 增加,一個點:如果Id是自動增長的,創建的對象顯然不用指定Id的值,並且在SaveChanges ()後會自動給對象的Id屬性賦值爲新增行的Id字段的值。

  2. 刪除。先查詢出來要刪除的數據,然後Remove。這種方式問題最少,雖然性能略低,但是刪除操作一般不頻繁,不用考慮性能。後續在“狀態管理”中會講其他實現方法。  

              MyDBContext context = new MyDBContext();
                if (e.CommandName=="BtnDelete")
                {
                    int id = Convert.ToInt32(e.CommandArgument);
                    var p = context.Persons.Where(per => per.ID == id).SingleOrDefault();
                    if (p!=null)
                    {
                        context.Persons.Remove(p);
                    }
                   int i= context.SaveChanges();
                    if (i>0)
                    {
                        Repeater1.DataSource = context.Persons.ToList();
                        Repeater1.DataBind();
                    }
                }

    怎麼批量刪除,比如刪除Id>3 的?查詢出來一個個Remove。性能坑爹。如果操作不頻繁或者數據量不大不用考慮性能,如果需要考慮性能就直接執行sql 語句

  3. 修改:先查詢出來要修改的數據,然後修改,然後SaveChanges()

    MyDbContext ctx = new MyDbContext();
    var ps = ctx.Persons.Where(p => p.Id > 3);
    foreach(var p in ps)
    {
        p.CreateDateTime = p.CreateDateTime.AddDays(3);
        p.Name = "haha";
    }
    ctx.SaveChanges();

    性能問題?同上。

  4. 查。因爲DbSet 實現了IQueryable 接口,而IQueryable 接口繼承了IEnumerable 接口,所以可以使用所有的linq、lambda 操作。給表增加一個Age 字段,然後舉例orderby、groupby、where 操作、分頁等。一樣一樣的。

  5. 查詢 order by 的一個細節

    EF調用Skip之前必須調用OrderBy:如下調用var items = ctx.Persons.Skip(3).Take(5); 會報錯“The method 'OrderBy' must be called before the method 'Skip'.)”,要改成:var items = ctx.Persons.OrderBy(p=>p.CreateDateTime).Skip(3).Take(5);

    這也是一個好習慣,因爲以前就發生過(寫原始sql):分頁查詢的時候沒有指定排序規則,以爲默認是按照Id 排序,其實有的時候不是,就造成數據混亂。寫原始SQL 的時候也要注意一定要指定排序規則。

EF 原理及SQL 監控

EF 會自動把Where()、OrderBy()、Select()等這些編譯成“表達式樹(Expression Tree)”,然後會把表達式樹翻譯成SQL 語句去執行。(編譯原理,AST)因此不是“把數據都取到內存中,然後使用集合的方法進行數據過濾”,因此性能不會低。但是如果這個操作不能被翻譯成SQL語句,則或者報錯,或者被放到內存中操作,性能就會非常低。

  1. 怎麼查看真正執行的SQL是什麼樣呢?

    DbContext有一個Database屬性,其中的Log屬性,是Action委託類型,也就是可以指向一個void A(string s)方法,其中的參數就是執行的SQL語句,每次EF執行SQL語句的時候都會執行Log。因此就可以知道執行了什麼SQL。

    EF的查詢是“延遲執行”的,只有遍歷結果集的時候才執行select 查詢,ToList()內部也是遍歷結果集形成List。

    查看Update操作,會發現只更新了修改的字段。

  2. 觀察一下前面學學習時候執行的SQL是什麼樣的。Skip().Take()被翻譯成了?Count()被翻譯成了?

    var result = ctx.Persons.Where(p => p.Name.StartsWith("inlett"));//看看翻譯成了什麼? 
    var result = ctx.Persons.Where(p => p.Name.Contains("com"));  
    var result = ctx.Persons.Where(p => p.Name.Length>5);  
    var result = ctx.Persons.Where(p => p.CreateDateTime>DateTime.Now); 
    long[] ids = { 2,5,6};//不要寫成int[] 
    var result = ctx.Persons.Where(p => ids.Contains(p.Id));

     

  3. EF中還可以多次指定where來實現動態的複合檢索:

    查看一下生成的SQL語句。

  4. EF是跨數據庫的,如果遷移到MYSQL上,就會翻譯成MYSQL的語法。要配置對應數據庫的Entity Framework Provider。

  5. 細節:

    每次開始執行的__MigrationHistory等這些SQL語句是什麼?是DBMigration用的,也就是由EF幫我們建數據庫,現在我們用不到,用下面的代碼禁用:

    Database.SetInitializer(null);

    XXXDbContext就是項目DbContext的類名。一般建議放到XXXDbContext構造函數中。注意這裏的Database 是System.Data.Entity下的類,不是DbContext的Database屬性。如果寫到DbContext中,最好用上全名,防止出錯。

執行原始SQL

不要“手裏有錘子,到處都是釘子”在一些特殊場合,需要執行原生SQL。

執行非查詢語句,調用DbContext的Database屬性的ExecuteSqlCommand方法,可以通過佔位符的方式傳遞參數:

ctx.Database.ExecuteSqlCommand("update T_Persons set Name={0},CreateDateTime=GetDate()","YLT.com");

佔位符的方式不是字符串拼接,經過觀察生成的SQL語句,發現仍然是參數化查詢,因此不會有SQL注入漏洞。

執行查詢:

var q1 = ctx.Database.SqlQuery<Item1>("select Name,Count(*) Count from T_Persons where Id>{0} and CreateDateTime<={1} group by Name",2, DateTime.Now); //返回值是DbRawSqlQuery<T>類型,也是實現IEnumerable 接口
foreach(var item in q1)
{
    Console.WriteLine(item.Name+":"+item.Count);
}
class Item1
{
    public string Name { get; set; }
    public int Count { get; set; }
}

類似於ExecuteScalar的操作比較麻煩:

int c = ctx.Database.SqlQuery<int>("select count(*) from T_Persons").SingleOrDefault();

不是所有lambda 寫法都能被支持

下面想把Id轉換爲字符串比較一下是否爲"3"(別管爲什麼):

var result = ctx.Persons.Where(p => Convert.ToString(p.Id)=="3");

運行會報錯(也許高版本支持了就不報錯了),這是一個語法、邏輯上合法的寫法,但是EF目前無法把他解析爲一個SQL語句。

出現“System.NotSupportedException”異常一般就說明你的寫法無法翻譯成SQL語句

想獲取創建日期早於當前時間一小時以上的數據:

var result = ctx.Persons.Where(p => (DateTime.Now - p.CreateDateTime).TotalHours>1);

同樣也可能會報錯。

怎麼解決?

嘗試其他替代方案(沒有依據,只能亂試):

var result = ctx.Persons.Where(p => p.Id==3);

EF中提供了一個SQLServer專用的類SqlFunctions,對於EF不支持的函數提供了支持,比如:

var result = ctx.Persons.Where(p =>SqlFunctions.DateDiff("hour",p.CreateDateTime,DateTime.Now)>1);

EF對象的狀態

簡介

爲什麼查詢出來的對象Remove()、再SaveChanges()就會把數據刪除。而自己new一個Person()對象,然後Remove()不行?爲什麼查詢出來的對象修改屬性值後、再SaveChanges()就會把數據庫中的數據修改。

因爲EF會跟蹤對象狀態的改變。

EF中中對象有五個狀態:Detached(遊離態,脫離態)、Unchanged(未改變)、Added(新增)、Deleted(刪除)、Modified(被修改)。

Add()、Remove()修改對象的狀態。所有狀態之間幾乎都可以通過:Entry(p).State=xxx的方式進行強制狀態轉換。

通過代碼來演示一下。這個狀態轉換圖沒必要記住,瞭解即可。

狀態改變都是依賴於Id的(Added除外)

應用(*)

當SavaChanged()方法執行期間,會查看當前對象的EntityState的值,決定是去新增(Added)、修改Modified)、刪除(Deleted)或者什麼也不做(UnChanged)。下面的做法不推薦,在舊版本中一些寫法不被支持,到新版EF中可能也會不支持。

  1. 不先查詢再修改再保存,而是直接更新部分字段的方法

    var p = new Person();
    p.Id = 2;
    ctx.Entry(p).State = System.Data.Entity.EntityState.Unchanged;
    p.Name = "adfad";
    ctx.SaveChanges();

    也可以:

    var p = new Person();
    p.Id = 5;
    p.Name = "yltedu";
    ctx.Persons.Attach(p);//等價於ctx.Entry(p).State = System.Data.Entity.EntityState.Unchanged;
    ctx.Entry(p).Property(a => a.Name).IsModified = true;
    ctx.SaveChanges();

     

  2. 不先查詢再Remove再保存,而是直接根據Id刪除的方法:

    var p = new Person();
    p.Id = 2;
    ctx.Entry(p).State = System.Data.Entity.EntityState.Deleted;
    ctx.SaveChanges();

    注意下面的做法並不會刪除所有Name="ylt.com" 的,因爲更新、刪除等都是根據Id進行的:

    var p = new Person();
    p.Name = "yltedu.com";
    ctx.Entry(p).State = System.Data.Entity.EntityState.Deleted;
    ctx.SaveChanges();
    上面其實是在:
    
    delete * from t_persons where Id=0

     

EF優化的一個技巧

如果查詢出來的對象只是供顯示使用,不會修改、刪除後保存,那麼可以使用AsNoTracking()來使得查詢出來的對象是Detached狀態,這樣對對象的修改也還是Detached狀態,EF不再跟蹤這個對象狀態的改變,能夠提升性能。

var p1 = ctx.Persons.Where(p => p.Name == "rupeng.com").FirstOrDefault();
Console.WriteLine(ctx.Entry(p1).State);

改成:

var p1 = ctx.Persons.AsNoTracking().Where(p => p.Name == "rupeng.com").FirstOrDefault();
Console.WriteLine(ctx.Entry(p1).State);

因爲AsNoTracking()是DbQuery類(DbSet的父類)的方法,所以要先在DbSet後調用AsNoTracking()。

Fluent API更多配置

基本EF配置只要配置實體類和表、字段的對應關係、表間關聯關係即可。如果利用EF的高級配置,可以達到更多效果:如果數據錯誤(比如字段不能爲空、字符串超長等),會在EF層就會報錯,而不會被提交給數據庫服務器再報錯;如果使用自動生成數據庫,也能幫助EF生成更完美的數據庫表。

這些配置方法無論是DataAnnotations、FluentAPI都支持,下面講FluentAPI的用法,DataAnnotations感興趣的自己查(http://blog.csdn.net/beglorious/article/details/39637475)。

儘量用約定,EF配置越少越好。Simple is best 參考資料:http://www.cnblogs.com/nianming/archive/2012/11/07/2757997.html

HasMaxLength設定字段的最大長度

public PersonConfig()
{
    this.ToTable("T_Persons");
    this.Property(p => p.Name).HasMaxLength(50);//長度爲50
}

依賴於數據庫的“字段長度、是否爲空”等的約束是在數據提交到數據庫服務器的時候纔會檢查;EF的配置,則是由EF來檢查的,如果檢查出錯,根本不會被提交給服務器。

如果插入一個Person對象,Name屬性的值非常長,保存的時候就會報DbEntityValidationException異常,這個異常的Message中看不到詳細的報錯消息,要看EntityValidationErrors屬性的值。

var p = new Person();
p.Name = "非常長的字符串";
ctx.Persons.Add(p);
try
{
    ctx.SaveChanges();
}
catch(DbEntityValidationException ex)
{
    StringBuilder sb = new StringBuilder();
    foreach(var ve in ex.EntityValidationErrors.SelectMany(eve=>eve.ValidationErrors))
    {
        sb.AppendLine(ve.PropertyName+":"+ve.ErrorMessage);
    }
    Console.WriteLine(sb);
}

(有用)字段是否可空

this.Property(p => p.Name).IsRequired() 屬性不能爲空; 
this.Property(p => p.Name).IsOptional() 屬性可以爲空;(沒用的雞肋!)

EF默認規則是“主鍵屬性不允許爲空,引用類型允許爲空,可空的值類型long?等允許爲空,值類型不允許爲空。”基於“儘量少配置”的原則:如果屬性是值類型並且允許爲null,就聲明成long?等,否則聲明成long等;如果屬性屬性值是引用類型,只有不允許爲空的時候設置IsRequired()。

其他一般不用設置的(瞭解即可)

  1. 主鍵:this.HasKey(p => p.pId);

  2. 某個字段不參與映射數據庫:this.Ignore(p => p.Name1);

  3. this.Property(p => p.Name).IsFixedLength(); 是否對應固定長度

  4. this.Property(p => p.Name).IsUnicode(false) 對應的數據庫類型是varchar類型,而不是nvarchar

  5. this.Property(p => p.Id).HasColumnName("Id1"); Id列對應數據庫中名字爲Id的字段

  6. this.Property(p=>p.Id).HasDatabaseGeneratedOption(System.ComponentModel.DataAnnotations.Schema.DatabaseGeneratedOption.Identity) 指定字段是自動增長類型。

流動起來

因爲ToTable()、Property()、IsRequired()等方法的還是配置對象本身,因此可以實現類似於StringBuilder的鏈式編程,這就是“Fluent”一詞的含義; 因此下面的寫法:

public PersonConfig ()
{
    this. ToTabl e ("T—Persons");
    this.HasKey(p => p. Id);
    this. Ignore(p => p. Name2);
    this.Property(p => p.Name) . HasMaxLength (50);
    this. Property (p => p. Name) . I sRequired ();
    this.Property(p => p.CreateDateTime) . HasCol umnName ("CreateDateTi me");
    this. Property (p => p. Name) . I sRequired () ;
}

可以簡化成:

public PersonConfig()
{
    this. ToTable ("T_Persons") . HasKey (p => p. Id). Ignore (p => p. Name2) ;
    this. Property (p => p. Name) . HasMaxLength (50). IsRequired O ;
    this. Property (p => p. CreateDateTime) . HasColumnName ("CreateDateTime") . IsRequiredO;
}

後面用的時候都Database.SetInitializer(null);

一對多關係映射

EF最有魅力的地方在於對於多表間關係的映射,可以簡化工作。 複習一下表間關係:

  1. 一對多(多對一):一個班級對應着多個學生,一個學生對着一個班級。一方是另外一方的唯一。在多端有一個指向一端的外鍵。舉例:班級表:T_Classes(Id,Name) 學生表

    T_Students(Id,Name,Age,ClassId)
  2. 多對多:一個老師對應多個學生,一個學生對於多個老師。任何一方都不是對方的唯一。 需要一箇中間關係表。具體: 學生表T_Students(Id,Name,Age,ClassId) , 老師表 T_Teachers(Id,Name,PhoneNum),關係表T_StudentsTeachers(Id,StudentId,TeacherId)

和關係映射相關的方法:

  1. 基本套路this.Has(p=>p.A).With***() 當前這個表和A 屬性的表的關係是Has 定義, With 定義的是A 對應的表和這個表的關係。Optional/Required/Many

  2. HasOptional() 有一個可選的(可以爲空的)

  3. HasRequired() 有一個必須的(不能爲空的)

  4. HasMany() 有很多的

  5. WithOptional() 可選的

  6. WithRequired() 必須的

  7. WithMany() 很多的

舉例:

在AAA 實體中配置this.HasRequired(p=>p.BBB).WithMany();是什麼意思? 在AAA 實體中配置this.HasRequired(p=>p.BBB).WithRequired ();是什麼意思?

配置一對多關係

  1. 先按照正常的單表配置把Student、Class 配置起來,T_Students 的ClassId 字段就對應Student類的ClassId 屬性。WithOptional()

    using (MyDbContext ctx = new MyDbContext ())
    {
        Class c l = new Class { Name= " 三年二班,, } ;
        ctx. Cl asses. Add (cl) ;
        ctx. SaveChanges () ;
        Student s l = new Student { Age = 11, Nam e = " 張三" , Cl assl d = cl. Id } ;
        Student s2 = new Student { Name = " 李四" , Classld = cl. Id } ;
        ctx.Students.Add(s1);
        ctx. Students. Add(s2);
        ctx. SaveChanges O ;
    }

     

  2. 給Student類增加一個Class類型、名字爲Class(不一定非叫這個,但是習慣是:外鍵名去掉Id)的屬性,要聲明成virtual(後面講原因)。

  3. 然後就可以實現各種對象間操作了:

    • Console.WriteLine(ctx.Students.First().Class.Name)

    • 然後數據插入也變得簡單了,不用再考慮“先保存Class,生成Id,再保存Student”了。這樣就是純正的“面向對象模型”,ClassId 屬性可以刪掉。

      Class c1 = new Class { Name = "五年三班" };
      ctx.Classes.Add(c1);
      Student s1 = new Student { Age = 11, Name = "皮皮蝦"};
      Student s2 = new Student { Name = "巴斯"};
      s1.Class = c1;
      s2.Class = c1;
      ctx.Students.Add(s1);
      ctx.Students.Add(s2);
      ctx.Classes.Add(c1);
      ctx.SaveChanges();

       

  4. 如果ClassId 字段可空怎麼辦?直接把ClassId 屬性設置爲long?

  5. 還可以在Class中配置一個public virtual ICollection Students { get; set; } = new List(); 屬性。最好給這個屬性初始化一個對象。注意是virtual。這樣就可以獲得所有指向了當前對象的Stuent 集合,也就是這個班級的所有學生。我個人不喜歡這個屬性,業界的大佬也是建議“儘量不要設計雙向關係”,因爲可以通過Class clz = ctx.Classes.First(); var students =ctx.Students.Where(s => s.ClassId == clz.Id);來查詢獲取到,思路更清晰。

    不過有了這樣的集合屬性之後一個方便的地方:

    Class c1 = new Class { Name = "五年三班" };
    ctx.Classes.Add(c1);
    Student s1 = new Student { Age = 11, Name = "皮皮蝦" };
    Student s2 = new Student { Name = "巴斯" };
    c1.Students.Add(s1);//注意要在Students屬性聲明的時候= new List<Student>();或者在之前賦值
    c1.Students.Add(s2);
    ctx.Classes.Add(c1);
    ctx.SaveChanges();

    EF會自動追蹤對象的關聯關係,給那些有關聯的對象也自動進行處理。

    在進行數據遍歷的時候可能會報錯“已有打開的與此 Command 相關聯的 DataReader,必須首先將它關閉。”

    foreach(var s in ctx.Students)
    {
        Console.WriteLine(s.Name);
        Console.WriteLine(s.Class.Name);
    }

     

一對多深入:

  1. 默認約定配置即可,如果非要配置,可以在StudentConfig 中如下配置:

    this.HasRequired(s=> s.Class).WithMany().HasForeignKey(s =>  s.ClassId); 

    表示“我需要(Require)一個Class,Class有很多(Many)的Student;ClassId是這樣一個外鍵”。

    如果ClassId 可空,那麼就要寫成:

    this.HasOptional (s =>  s.Class).WithMany().HasForeignKey(s => s.ClassId);

     

  2. 如果這樣Class clz = ctx.Classes.First();foreach (Student s in clz.Students)訪問,也就是從一端發起對多端的方法,那麼就會報錯“找不到Class_Id 字段”需要在ClassConfig中再反向配置一遍 HasMany(e =>e.Students).WithRequired().HasForeignKey(e=>e.ClassId); 因爲如果在Class 中引入Students 屬性,還要再在ClassConfig 再配置一遍反向關係,很麻煩。因此再次驗證“不要設計雙向關係”。

  3. 如果一張表中有兩個指向另外一個表的外鍵怎麼辦?比如學生有“正常班級Class”(不能空)和“小竈班級XZClass”(可以空)兩個班。在StudentConfig 中:

    this.HasRequired(s => s.Class).WithMany().HasForeignKey(s => s.ClassId); 
    this. HasOptional (s => s.XZClass).WithMany().HasForeignKey(s => s.XZClassId);

     

多對多關係配置

老師和學生:



class Student
{
    public long Id { set; get; }
    public string Name { get; set; }
    public virtual ICollection<Teacher> Teachers { get; set; }=new List<Teacher>();
}
class Teacher
{
    public long Id { set; get; }
    public string Name { get; set; }
    public virtual ICollection<Student> Students { get; set; }=new List< Student >();
}
class StudentConfig : EntityTypeConfiguration<Student>
{
    public StudentConfig()
    {
        ToTable("T_Students");
    }
}
class TeacherConfig : EntityTypeConfiguration<Teacher>
{
    public TeacherConfig()
    {
        ToTable("T_Teachers");
        this.HasMany(e => e.Students).WithMany(e => e.Teachers)//易錯,容易丟了WithMany 的參數
        .Map(m =>
        m.ToTable("T_TeacherStudentRelations").MapLeftKey("TeacherId").MapRightKey("StudentId"));
    }
}

關係配置到任何一方都可以

這樣不用中間表建實體(也可以爲中間表建立一個實體,其實思路更清晰),就可以完成多對多映射。當然如果中間關係表還想有其他字段,則要必須爲中間表建立實體類。 測試:

Teacher t1 = new Teacher();
t1.Name = "張老師";
t1.Students = new List<Student>();
Teacher t2 = new Teacher();
t2.Name = "王老師";
t2.Students = new List<Student>();
Student s1 = new Student();
s1.Name = "tom";
s1.Teachers = new List<Teacher>();
Student s2 = new Student();
s2.Name = "jerry";
s2.Teachers = new List<Teacher>();
t1.Students.Add(s1);

附錄:

  1. 關於WithMany()的參數

    • 在一對多關係中,如果只配置多端關係並且沒有給WithMany()指定參數的話,在進行反向關係操作的時候就會報錯。要麼在一端也配置一次,最好的方法就是還是隻配置多端,只不過給WithMany()指定參數:

      class StudentConfig:EntityTypeConfiguration<Student>
      {
          public StudentConfig()
          {
              ToTable("T_Students");
              this.HasRequired(e => e.Class).WithMany(e=>e.Students)
              .HasForeignKey(e=>e.ClassId);
          }
      }

      當然還是不建議用反向的集合屬性,如果Class沒有Students這個集合屬性的話,就不用(也不能)WithMany的參數了。

    • 關於多對多關係配置的WithMany()問題

      上次講配置多對多的關係沒有給WithMany設定參數,這樣反向操作的時候就會出錯,應該改成:this.HasMany(e => e.Students).WithMany(e=>e.Teachers)

      總結:一對多的中不建議配置一端的集合屬性,因此配置的時候不用給WithMany()參數,如果配置了集合屬性,則必須給WithMany 參數;多對多關係必須要給WithMany()參數。

      總結一對多、多對多的“最佳實踐”

  2. 一對多最佳方法(不配置一端的集合屬性):

    • 多端

      public class Student
      {
          public long Id { get; set; }
          public string Name { get; set; }
          public long ClassId { get; set; }
          public virtual Class Class { get; set; }
      }

       

    • 一端

      public class Class
      {
          public long Id { get; set; }
          public string Name { get; set; }
      }

       

    • 在多端的模型配置(StudentConfig)中:

      this.HasRequired(e => e.Class).WithMany() .HasForeignKey(e=>e.ClassId);

       

  3. 一對多的配置(在一端配置一個集合屬性,極端不推薦)

    • 多端

      public class Student
      {
          public long Id { get; set; }
          public string Name { get; set; }
          public long ClassId { get; set; }
          public virtual Class Class { get; set; }
      }

       

    • 一端

      public class Class
      {
          public long Id { get; set; }
          public string Name { get; set; }
          public virtual ICollection<Student> Students { get; set; } = new List<Student>();
      }

       

    • 多端的配置(StudentConfig)中

      this.HasRequired(e => e.Class).WithMany(e=>e.Students)//WithMany()的參數不能丟 .HasForeignKey(e=>e.ClassId);

       

  4. 多對多最佳配置

    • 兩端模型

      public class Student
      {
          public long Id { get; set; }
          public string Name { get; set; }
          public virtual ICollection<Teacher> Teachers { get; set; } = new List<Teacher>();
      }
      public class Teacher
      {
          public long Id { get; set; }
          public string Name { get; set; }
          public virtual ICollection<Student> Students { get; set; } = new List<Student>();
      }

       

    • 在其中一端配置(StudentConfig)

      this.HasMany(e => e.Teachers).WithMany(e=>e.Students).Map(m =>//不要忘了WithMany的參數 m.ToTable("T_StudentTeachers").MapLeftKey("StudentId").MapRightKey("TeacherId"));

       

    • 多對多中 移除關係:t.Students.Remove(t.Students.First()); 添加關係

    • (*)多對多中還可以爲中間表建立一個實體方式映射。當然如果中間關係表還想有其他字段,則要必須爲中間表建立實體類(中間表和兩個表之間就是兩個一對多的關係了)。

    • 數據庫創建策略(*): 如果數據庫創建好了再修改模型或者配置,運行就會報錯,那麼就要手動刪除數據庫或者:Database.SetInitializer(new DropCreateDatabaseIfModelChanges());如果報錯“數據庫正在使用”,可能是因爲開着Mangement Studio,先關掉就行了。知道就行了,只適合學習時候使用。

      CodeFirst Migration 參考(*): http://www.cnblogs.com/libingql/p/3330880.html 太複雜, 不符合Simple is Best 的原則,這是爲什麼有一些開發者不用EF,而使用Dapper 的原因。

      做項目的時候建議初期先把主要的類使用EF 自動生成表,然後幹掉Migration 表,然後就 Database.SetInitializer(null);以後對數據庫表的修改都手動完成,也就是手動改實體類、 手動改數據庫表。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章