EF里Guid類型數據的自增長、時間戳和復雜類型的用法


通過前兩章Lodging和Destination類的演示,大家肯定基本了解Code First是怎么玩的了,本章繼續演示一些很實用的東西。
文章的開頭提示下:提供的demo為了后面演示效果,前面代碼有些是注釋了的,請按照文章講解的順序先后釋放注釋並運行查看效果。

I.EF里Guid類型數據的自增長

現在新添加一個Trip旅行類:

    /// <summary>
    /// 旅行類
    /// </summary>
    public class Trip
    {
        public Guid Identifier { get; set; }
        public DateTime StartDate { get; set; }   //開始時間
        public DateTime EndDate { get; set; }   //結束時間
        public decimal CostUSD { get; set; }     //花費
    }

當然,還需要在BreakAwayContext類中添加上讓上下文能識別Trip類:

public DbSet<CodeFirst.Model.Trip> Trips { get; set; }

跟以往的實體類不同的地方:Trip類的第一個屬性不是類名+id,也不是int類型的;
EF的默認約定就是第一個屬性如果是類名+id,並且是int類型的,那么直接設置第一個屬性為主鍵,同時設置自增長。顯然兩條都不符合,如果直接跑程序,那么會報一個ModelValidationException錯:
One or more validation errors were detected during model generation:
System.Data.Edm.EdmEntityType: : EntityType 'Trip' has no key defined. Define the key for this EntityType.
System.Data.Edm.EdmEntitySet: EntityType: EntitySet ?Trips? is based on type ?Trip? that has no keys defined.
很明顯是沒有主鍵的錯。可以使用上一節提過的Data Annation的方式設置主鍵:直接在Identifier屬性上加注[key]
我個人還是喜歡Fluent API的方式,按照之前說的,方便以后修改和維護,在DataAccess類庫下新建一個實現EntityTypeConfiguration接口的TripMap類,把所有的Fluent API配置都寫在此類的構造函數里:

        public TripMap()
        {
            this.HasKey(t => t.Identifier);   //主鍵
        }

同樣,這個也要添加到BreakAwayContext類的OnModelCreating方法里:

modelBuilder.Configurations.Add(new TripMap());

這時再跑下程序,主鍵就正常的生成了。但是由於是guid類型的,EF一樣不會自動設置自增長,不設自增長有什么壞處呢,往下看。
添加一個方法,插入一條數據到Trip表里:

        private static void InsertTrip()
        {
            var trip = new CodeFirst.Model.Trip
            {
                CostUSD = 800,
                StartDate = new DateTime(2011, 9, 1),
                EndDate = new DateTime(2011, 9, 14)
            };
            using (var context = new CodeFirst.DataAccess.BreakAwayContext())
            {
                context.Trips.Add(trip);
                context.SaveChanges();
            }
        }

在Main方法里調用下InsertTrip方法,再跑下程序,再去查看下數據庫,結果:

可見,沒有設置自增長並且程序中沒有向此字段注入guid類型的數據,數據庫會自動補充一堆0,下次再添加還是全部0,自然會報錯(主鍵不允許重復)
腦補:Guid就是全局統一標識符的意思,隨機的一串字母。幾乎不可能重復,所以適合用來當主鍵。可以向mssql的控制台打印Guid試試,每次執行都不一樣:

可以通過注解或者api的方式為guid類型的數據設置自增長,分別是:

[Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)]
this.Property(t => t.Identifier).HasDatabaseGeneratedOption(DatabaseGeneratedOption.Identity);  //Guid類型主鍵自增長

再跑下程序,guid列就有值了。
到這里就有疑問了,自增長不是像int類型一樣每次加1么,這里的guid無法加1啊,其實就是自動生成。我本以為大家對“自增長”理解太狹隘啊:+1才是自增長,其實類似guid自動生成的形式也屬於自增長。但是查閱了資料,也沒有什么結果。大家在文章下面的留言都挺有道理。這里姑且算自動生成的吧,也更好理解。留個坑,以后學習遇到了再來修改,當然如果有高手願意分享,期待你的回復。

有心的園友肯定注意到了:DatabaseGeneratedOption還有另外兩個枚舉值:None(空)、Computed(計算)
None還是挺有用的,這里演示下None的用法,新建一個Person類:

    /// <summary>
    /// 人類
    /// </summary>
    public class Person
    {
        public int SocialSecurityNumber { get; set; }   //社保號
        public string FirstName { get; set; }
        public string LastName { get; set; }
    }

沒有PersonId屬性,那么需要手動配置主鍵。前面說過很多次了,這里不再贅述。直接使用Fluent API配置(不會請下載源碼)
配置好關系后,在Program.cs里再寫一個方法,向Person表里插入一條數據:

        private static void InsertPerson()
        {
            var person = new CodeFirst.Model.Person
            {
                FirstName = "Rowan",
                LastName = "Miller",
                SocialSecurityNumber = 12345678
            };
            using (var context = new CodeFirst.DataAccess.BreakAwayContext())
            {
                context.People.Add(person);
                context.SaveChanges();
            }
        }

運行后如圖,id為1,並不是方法里寫的的12345678。因為int類型的設置為主鍵后,會自動設置標識增量1、標識種子1。那么就從1開始自增長了,它會忽略這行插入的任何數據。

這里就可以通過配置None來解決這個問題,很明顯了設置為None就是不自增長了。

this.Property(p => p.SocialSecurityNumber).HasDatabaseGeneratedOption(DatabaseGeneratedOption.None);

再跑下程序,id列就是12345678了。
注:每次重新配置實體類和映射關系重新生成數據庫的過程都需要【斷開數據庫連接】,否則提示“數據庫正在使用,無法刪除重新生成”

II.EF里時間戳的用法

我們在Trip類和Person類里同時添加一個byte[]字節數組的屬性(時間戳必須是byte[]類型的):

public byte[] RowVersion { get; set; }

直接在屬性上標注:[Timestamp],或者使用Fluent API配置:

this.Property(p => p.RowVersion).IsRowVersion();  //時間戳

重新跑下程序:

可以看出RowVersion列的類型是timestamp類型的,時間戳可以防並發。並發分為樂觀和悲觀並發
悲觀並發:一個用戶訪問一條數據時,則把這個數據變為只讀屬性 。把該數據變為獨占,只有該用戶釋放了這條數據,其他用戶才能修改。
樂觀並發:用戶讀取數據時不鎖定數據。當一個用戶更新數據時,系統將進行檢查,查看該用戶讀取數據后其他用戶是否又更改了該數據。如果其他用戶更新了數據,將產生一個錯誤。
摘自這里。EF里的並發都是樂觀並發(optimistic concurrency)。

重新執行下之前的InsertTrip方法,同時打開sql profiler跟蹤下發到數據庫的sql:
注:如果不改變實體直接執行,那么不會重新生成數據庫,再執行下InsertTrip方法,那么Trip表加上之前的數據就有兩條了。如果想不管實體發生不發生變化都重新生成數據庫,那么直接在main方法里初始化數據庫之后加上:

            using (var context = new CodeFirst.DataAccess.BreakAwayContext())
            {
                context.Database.Initialize(true);
            }

有時間戳的表,雖然是插入語句 但是仍然執行了查詢,每次都返回了RowVersion:

exec sp_executesql N'declare @generated_keys table([Identifier] uniqueidentifier)
insert [dbo].[Trips]([StartDate], [EndDate], [CostUSD])
output inserted.[Identifier] into @generated_keys
values (@0, @1, @2)
select t.[Identifier], t.[RowVersion]
from @generated_keys as g join [dbo].[Trips] as t on g.[Identifier] = t.[Identifier]
where @@ROWCOUNT > 0',N'@0 datetime2(7),@1 datetime2(7),@2 decimal(18,2)',@0='2011-09-01 00:00:00',@1='2011-09-14 00:00:00',@2=800.00

比較亂,重點看這一句:select t.[Identifier], t.[RowVersion] from...
繼續演示,添加一個更新的方法,把價格從800修改成750:

        private static void UpdateTrip()
        {
            using (var context = new CodeFirst.DataAccess.BreakAwayContext())
            {
                var trip = context.Trips.FirstOrDefault();
                trip.CostUSD = 750;
                context.SaveChanges();
            }
        }

注:不要重復執行該方法,數據庫的CostUSD價格是750的話,再執行此方法把價格改成750沒效果,EF監測到修改之前之后沒區別的話不會發sql到數據庫,可自行測試下。
注:必須同時執行InsertTrip方法和UpdateTrip方法,否則重新生成數據庫后找不到要修改的數據。sql profiler監測到update的sql:

exec sp_executesql N'update [dbo].[Trips]
set [CostUSD] = @0
where (([Identifier] = @1) and ([RowVersion] = @2))
select [RowVersion]
from [dbo].[Trips]
where @@ROWCOUNT > 0 and [Identifier] = @1',N'@0 decimal(18,2),@1 uniqueidentifier,@2 binary(8)',@0=750.00,@1='1D424880-CCC2-4F34-8D6E-3838E5FC72EF',@2=0x00000000000007D1

重點看這一句:where (([Identifier] = @1) and ([RowVersion] = @2))
更新的where同時倆條件:一個是主鍵值,一個就是時間戳。如果有人修改了數據,那么時間戳RowVersion的值就不一樣了,就不符合where條件了,自然無法更新,同時程序會拋錯。同樣,如果更新成功,那么同時也會更新時間戳的值。下圖展示了修改數據前后時間戳的值:

ToHexString是一個二進制數據轉成16進制字符串的方法。注意看修改前和修改后的時間戳不一樣了。但是為何sql profiler監控的時間戳的值是0x00000000000007D1,而程序里拿到的是00000000000007D1?這就是計算機16進制的問題了。一般都會在開頭加上0x表示16進制的數。其他不夠位數的都補0。看這里 為了說明這個,再舉一例:打開windows 7自帶的計算器,選擇查看 - 程序員 - 選擇左側16進制 - 輸入上面的7D1:

然后點擊左側的十進制,看十六進制的7D1轉換成了十進制的2001了:

轉到程序里試試,先定義一個2001的int類型變量,然后調試看看它的16進制是多少(右鍵 - 以16進制查看)。的確如我們所料:0x開頭,不夠位數的都補0了:

簡單的進制轉換問題。有時間戳的數據修改后,時間戳列的值都會改變,但是注意看更新的sql,為何並沒有看到更新時間戳列的sql?看這里
這就是ef的樂觀並發。為了更好的驗證,新加一個新的方法:

        private static void UpdateTrip2()
        {
            var firstContext = new CodeFirst.DataAccess.BreakAwayContext();
            var trip1 = firstContext.Trips.FirstOrDefault();  //第一個用戶取出第一條記錄
            trip1.CostUSD = 750;  //修改但是還沒來得及保存
            using (var secondContext = new CodeFirst.DataAccess.BreakAwayContext())
            {
                var trip2 = secondContext.Trips.FirstOrDefault();  //第二個用戶進來同樣取出第一條記錄
                trip2.CostUSD = 900;
                secondContext.SaveChanges();  //修改並保存(保存的操作不僅修改了CostUSD為900,同時修改了RowVersion)
            }
            try
            {
                firstContext.SaveChanges();  //此時第一個用戶想保存,但是RowVersion已經改變了
                Console.WriteLine("保存成功!");
            }
            catch (DbUpdateConcurrencyException ex)
            {
                Console.WriteLine(ex.Entries.First().Entity.GetType().Name + " 保存失敗");
            }
            finally
            {
                firstContext.Dispose();
            }
        }

上面的演示了通過設置列為timestamp類型達到控制並發的效果,但是很多數據庫甚至沒有timestamp類型,所以控制並發再介紹一個:ConcurrencyCheck
社保號SocialSecurityNumber一般是唯一的,它是int類型,並不是timestamp類型。我們使用控制並發,直接標注ConcurrencyCheck或者使用Fluent API配置(具體見源碼)再添加一個更新的方法:

        private static void UpdatePerson()
        {
            using (var context = new CodeFirst.DataAccess.BreakAwayContext())
            {
                var person = context.People.FirstOrDefault();
                person.FirstName = "Rowena";
                context.SaveChanges();
            }
        }

和之前的InsertPerson方法一起執行,否則找不到對象,sql profiler監控到的sql語句:

exec sp_executesql N'update [dbo].[People]
set [FirstName] = @0
where ([SocialSecurityNumber] = @1)
',N'@0 nvarchar(max) ,@1 int',@0=N'Rowena',@1=12345678

修改的時候有雙條件,這樣也達到了控制並發的效果,和timestamp差不多。

III.EF中的復雜類型
如果給Person類添加更詳細的信息,類似:StreetAddress、City、State、ZipCode等屬性,直接添加感覺Person類越來越臃腫。可以抽象出一個Address類,然后把Address類作為Person類的屬性,這樣更符合面向對象的思維,也方便管理和重用。如果直接把Address類作為Person的屬性,那么就如前面第一章的例子,自動映射成主外鍵關系了。可以通過標注復雜類型(ComplexType)來實現。先添加Address類:

    /// <summary>
    /// 地址類(復雜類型)
    /// </summary>
    public class Address
    {
        //public int AddressId { get; set; } //復雜類型不能有主鍵id
public string StreetAddress { get; set; }
public string City { get; set; } public string State { get; set; } //public string ZipCode { get; set; } //郵編 }

然后可以直接在Address類上標注[ComplexType],這是Data Annotations的方式。自然也可以用Fluent API的形式配置,同樣提供兩種方法,一個直接寫、一個單獨寫成類,然后添加進去,方便管理:

modelBuilder.ComplexType<CodeFirst.Model.Address>(); //這個直接寫在OnModelCreating方法里
public class AddressConfiguration :ComplexTypeConfiguration<Address>
{
  public AddressConfiguration()
  {
    Property(a => a.StreetAddress).HasMaxLength(150);
  }
}
modelBuilder.Configurations.Add(new AddressConfiguration());

最后在Person類里添加Address類的屬性。把Main方法里所有的方法都注釋了,重新生成下數據庫,可以看到Address的屬性都在People表里了。
使用復雜類型需要注意:

  1. 沒有主鍵列;
  2. 實例不能重復(Person類里只能有一個Address實例);
  3. 只能是單實例,不能是一個集合(Person類里是Address單實例,不能是List<Address>)。

本文到此結束,謝謝閱讀。本章源碼

EF Code First 系列文章導航
  1. EF Code First 初體驗
  2. EF里的默認映射以及如何使用Data Annotations和Fluent API配置數據庫的映射  本節源碼
  3. EF里Guid類型數據的自增長、時間戳和復雜類型的用法  本節源碼
  4. EF里一對一、一對多、多對多關系的配置和級聯刪除  本節源碼
  5. EF里的繼承映射關系TPH、TPT和TPC的講解以及一些具體的例子  本節源碼


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM