EntityFramework Core 2.1重新梳理系列属性映射(一)

前言

满血复活啦,大概有三个月的时间没更新博客了,关于EF Core最新进展这三个月也没怎么去看,不知现阶段有何变化没,本文将以EF Core 2.1稳定版本作为重新梳理系列,希望对看本文的你有所帮助,欢迎一起探讨。(请不要嫌弃啰嗦哈,我习惯于将来龙去脉给大家梳理清楚,各种我能想到的场景给大家讲解明白)。

属性映射探讨

当我们利用Code First映射属性时,此时本身没有什么太大问题,但是当我们初始化表或者获取数据时等等,通过日志会发现打印出一些需要我们注意的地方,推荐我们使用最佳方式,对于属性探讨我们将着眼于进一步探讨日志中所打印的信息。我们依然利用两个类Blog和Post来探讨,大家也好对照着看。

    public class Blog
    {
        public int Id { get; set; }
        public string Name { get; set; }
        public byte Status { get; set; }
        public bool Deleted { get; set; }
        public DateTime CreatedTime { get; set; }

        public ICollection<Post> Posts { get; set; }
    }

    public class Post
    {
        public int Id { get; set; }
        public int BlogId { get; set; }
        public string Title { get; set; }
        public string Content { get; set; }
        public Blog Blog { get; set; }
    }

首先我们在映射时,不给定属性默认值以及映射列类型等,直接看看迁移时生成的列类型是怎样,然后我们再来进一步深入。对于关系映射还是建议手动配置一下,虽然EF Core也会通过约定来自动进行配置,但是手动配置便于理解,如下:

        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            modelBuilder.Entity<Blog>(b => 
            {
                b.ToTable("Blogs");

                b.HasMany(m => m.Posts)
                    .WithOne(o => o.Blog)
                    .HasForeignKey(k => k.BlogId);
            });

            modelBuilder.Entity<Post>(b =>
            {
                b.ToTable("Posts");
            });
        }

 

以上迁移是EF Core默认根据约定生成列类型以及约束和级联删除的情况。属性Id作为主键且自增长,对于字符串默认创建为NVARCHAR且长度为max,同时可空。日期类型默认为DATETIME2。这些都是最基础的东西,在我写的书中也有详细介绍,就不再啰嗦了。我们从以下几点开始探讨。

主键映射并添加数据探讨

数据库主键列无外乎就是INT、BIGINT、GUID、VARCHAR(36)这几种常见类型,接下来我们一一探讨。对于INT或者BIGINT整数类型大多数情况下,我们的主键都是数据库自动生成即自增长,所以此时我们进行如下操作万无一失。

            var blog = new Blog()
            {
                CreatedTime = Convert.ToDateTime("2018-10-20"),
                Deleted = false,
                Status = 1,
                Name = "EFCore"
            };
            _context.Blogs.Add(blog);
            var result = _context.SaveChanges();

上述我们也说过我们并未设置主键是否自增长,如果不进行手动配置,这个根据默认约定而配置。当然对于主键若是客户端自动生成,我们只需进行如下映射即可。

                b.HasKey(k => k.Id);
                b.Property(p => p.Id).ValueGeneratedNever();

我个人比较习惯对于主键也手动通过HasKey进行配置。在添加数据时根据约定主键自动增长,如上述。当然我们也可以手动配置在添加还是更新时自增长,如下:

                b.Property(p => p.Id).ValueGeneratedOnAdd();
                b.Property(P => P.Id).ValueGeneratedOnAddOrUpdate();
                b.Property(p => p.Id).ValueGeneratedOnUpdate();

是不是就这么完了呢?其实远没有这么简单,我们只看上述第一个即 ValueGeneratedOnAdd ,我们去看其方法解释。

        // 摘要:
        //     Configures a property to have a value generated only when saving a new entity,
        //     unless a non-null, non-temporary value has been set, in which case the set value
        //     will be saved instead. The value may be generated by a client-side value generator
        //     or may be generated by the database as part of saving the entity.

此方法解释如果主键为非空或者没有临时值未被设置的话,数据库将自动生成主键。同时请注意,它也表明主键值在保存时可通过客户端或者数据库自动生成。我们刚才演示了在数据库自动生成,既然解释在客户端也可自动生成,那我们再来添加一条数据试试呢。

            var blog = new Blog()
            {
                Id = 2,
                CreatedTime = Convert.ToDateTime("2018-10-20"),
                Deleted = false,
                Status = 1,
                Name = "EFCore"
            };
            _context.Blogs.Add(blog);
            var result = _context.SaveChanges();

 

有关EF 6.x和EF Core插入数据不同,请参看此链接:https://www.cnblogs.com/CreateMyself/p/9017296.html。该方法明确说好的在客户端也可自动生成的啊,难道解释有误?在EF 6.x中默认打开了IDENTITY_INSERT,而在EF Core中将IDENTITY_INSERT给关闭了,所以我们要想始终使用自增长值即使客户端给定了值且不抛出异常,那么需要在数据库中将IDENTITY_INSERT给打开才行。在EF Core会遇到将主键设置成临时值的情况,但是如果我们又不想显式打开IDENTITY_INSERT,同时需要始终使用自增长值,那么该如何做呢?在EF 6.x中可以通过 HasDatabaseGeneratedOption(DatabaseGeneratedOption.Identity) 进行设置,在EF Core则不能进行全局设置。这个问题的出现还是dudu老大所提出的,最终我所给出的答案则是利用追踪图来解决(https://q.cnblogs.com/q/110205/),追踪图中可获取到对象所有状态和主键是否已经设置,所以最终解决方案如下:

            _context.ChangeTracker.TrackGraph(blog, node =>
            {
                if (node.Entry.IsKeySet)
                {
                    blog.Id = 0;
                    _context.Blogs.Add(blog);
                }
            });

到了这里,还有一个方法我们一直未曾讲解到,不知你是否在项目中应用过,在EF Core还有一个 UseSqlServerIdentityColumn 方法,其名为使用SQL Server标识列,该方法只能针对Key进行设置,最终则会调用 ValueGeneratedOnAdd 方法,如此而已。

 

对于主键我们只是讲解了INT类型,对于BIGINT和INT一样都是整数,那我们看看带小数点的,比如主键为decimal类型。下面我们来改变一下上述Blog表主键Id的数据类型。修改为decimal,同时Post类中的外键BlogId也修改为decimal,且手动配置添加 ValueGeneratedOnAdd 方法,如下:

  public decimal Id { get; set; }

此时我们再来重新迁移一下。

通过上述错误我们知道主键列的数据类型,同时呢也知道 ValueGeneratedOnAdd 方法手动配置只是针对于整数类型,对于小数类型,则不能应用其方法,我们去掉该方法再看看。

因为其带小数,所以此时EF Core会自动发现主键且非自增长,这和我们在数据库正常设置主键和是否自增长一致。下面我们再来看看主键为GUID的情况,将主键修改为GUID如下:

  public Guid Id { get; set; }

如果主键是GUID,那么对于数据库列类型就是 uniqueidentifier 。此时也就涉及到两种情况,一是客户端自动生成,二是数据库自动生成。默认情况下会在数据库自动生成GUID。如果我们手动设置了GUID,那么将以手动设置的GUID为准,如下:

            var blog = new Blog()
            {
                Id = Guid.Parse("13D375A1-8AE7-4B84-B220-6BAB72FA2454"),
                CreatedTime = Convert.ToDateTime("2018-10-20"),
                Deleted = false,
                Status = 1,
                Name = "EFCore"
            };

            _context.Blogs.Add(blog);
            var result = _context.SaveChanges();

若是我们同时配置了添加时自动生成和数据库自动生成,此时在添加时却是给定了其值,此时依然是添加的为准,如下:

   b.Property(p => p.Id).HasDefaultValueSql("NEWID()");
   b.Property(p => p.Id).ValueGeneratedOnAdd();

讲完主键为GUID类型,我们再来讲讲主键列类型为VARCHAR(36),我们要使其在添加时自动生成,进行如下设置即可,EF Core会自动发现并生成36位的字符串,无需配置 ValueGeneratedOnAdd 方法。

   b.Property(p => p.Id).HasColumnType("VARCHAR(36)");

虽然我们一直说对于字符串类型映射,默认映射为可空,但是主键不同,主键本来就不可空,所以上述我们设置主键为VARCHAR(36),无需多此一举设置 IsRequired ,但是需要注意的是此时对于外键BlogId,依据关系是否必须,如果必须一定要设置 IsRequired ,否则为可空类型。若进行如下设置数据库自动生成,同时客户端手动指定了主键,则以手动指定为准。

 b.Property(p => p.Id).HasDefaultValueSql("NEWID()");

以上讲了这么多,我们来对主键映射做一个完整的总结。

(1)对于Int、Int64等整数,默认情况下自增长即数据库自动生成,添加数据时如果主键为空或者为0,数据库将自动生成,否则抛出异常。而对于decimal小数,主键Id由客户端指定生成。

(2) 对于Guid类型,默认情况下数据库自动生成,无需显式调用ValueGeneratedOnAdd方法或者HasDefaultValueSql("NEWID()"),若显式指定Guid,将会覆盖数据库自动生成。

(3)对于VARCHAR(36)类型,默认情况下自动生成,无需显式调用ValueGeneratedOnAdd方法或者HasDefaultValueSql("NEWID()"),若显式指定36位字符串,将会覆盖数据库自动生成。

初始化默认值探讨(什么时候用HasDefaultValue和HasDefaultValueSql)

默认值最常见类型属于bool、byte、datetime等等。同时提供默认我们可通过HasDefaultValue和HasDefaultValueSql两个方法来进行,那么是不是二者使用没有什么异同呢?下面我们一一来探讨,在Blog类中有Deleted属性,我们映射其默认值为false,如下:

 b.Property(p => p.Deleted).HasDefaultValue(false);

 //或者

 b.Property(p => p.Deleted).HasDefaultValueSql("0");

当我们迁移时会发现如下日志:

布尔类型默认值本来就是false,所以上述完全不用显式去配置,如此配置多此一举而且在日志中还会打印一条warning消息建议使用可空类型布尔值。对于布尔类型,不用映射时给定默认值,如果该类型为可空,将其属性设为可空即可。那么要是我们设置默认值不为false而是true呢?下面我们再来看看。

  b.Property(p => p.Deleted).HasDefaultValue(true);

此时迁移时依然会打印上述警示信息,在应用程序日志中也会显示这些警示信息,这个警示我认为有点让人疑惑,应该改善提示信息,但是我们真的不想根据其建议设置为可空类型,没什么太大意义。此时我们应该怎么办呢?我们可以进行如下映射配置从而警示信息也不会再有:

  b.Property(p => p.Deleted).HasDefaultValue(true).ValueGeneratedNever();

上述通过ValueGeneratedNever方法配置意在表明:

当迁移时对数据库中已存在的行使用其配置的默认值,而对新增的数据行完全不使用其默认值,换句话说,告知EF Core即使配置了默认值,在EF Core运行时也不应该使用其默认值。

下面我们再来看看byte映射为列tinyint情况。对于上述Status属性,我们进行如下映射。

  b.Property(k => k.Status).HasDefaultValue(0);

 

接下来我们修改为HasDefaultValueSql,如下:

   b.Property(k => k.Status).HasDefaultValueSql("0");

 

我们看到在使用HasDefaultValue和HasDefaultValueSql的区别所在,对于byte映射时利用HasDefaultValue会存在转换的情况(注意:在EF Core 2.0中利用HasDefaultValue不存在转换问题和HasDefaultValueSql配置一致),所以此时只能利用HasDefaultValueSql来映射默认值。对于日期列类型大部分情况都是DateTime,此时我们也只能通过HasDefaultValueSql来指定默认值,如下:

 b.Property(p => p.CreatedTime).HasColumnType("DATETIME").HasDefaultValueSql("GETDATE()");

上述我们对Status和DateTime指定了默认值,我们在添加数据时,依然指定Status默认值为0,CreatedTime也指定时间看看。

            var blog = new Blog()
            {
                Status = 0,
                CreatedTime = Convert.ToDateTime("2018-10-30"),
                Name = "EFCore",
            };

            _context.Blogs.Add(blog);
            var result = _context.SaveChanges();

 

在此我们对属性指定默认值做一个完整的总结:

(1)如果指定默认值和CLR Type默认值一致,此时数据库列默认值即CLR type默认值,同时我们无需通过HasDefaultValue和HasDefaultValueSql方法显式配置默认值,无疑是多此一举且会在日志打印warning信息。否则需要通过HasDefaultValue和HasDefaultValueSql显式指定默认值。

(2)对于HasDefaultValue和HasDefaultValueSql方法显式配置默认值时需注意值类型是否和数据库列类型一致,如果一致用HasDefaultValue方法,否则请用HasDefaultValueSql方法。如若不然会存在默认值显式转换的情况。

(3)指定默认值为CLR Type默认值后,在添加数据时,若显式指定了CLR Type默认值,那么此时将会在数据库自动生成(由上添加数据生成的SQL没有Status列可知)。

(4)若日期指定默认值为数据库自动生成,但添加时显式指定日期,此时将覆盖数据库自动生成的默认日期。

字符串映射探讨

对于字符串默认映射类型为NVARCHAR且长度为MAX,同时为可空,是否可空通过IsRequired来修正。如果映射为VARCHAR类且长度为50,我们可通过HasColumnType方法来指定类型,如下:

b.Property(p => p.Name).HasColumnType("VARCHAR(50)");

在EF Core 2.1之前,我们通过HasColumnType方法指定类型,而通过HasMaxLength指定长度迁移会抛异常,那么现在是否可以呢?,答案是:迁移不会抛异常,但结果长度不正确,如下:

  b.Property(p => p.Name).HasColumnType("VARCHAR").HasMaxLength(50);

 

在EF 6.x中对于char、nchar等需要通过HasMaxLength和IsFixedLength方法类修正实现,在EF Core中都统一通过HasColumnType方法实现即可。

总结 

本文比较详细的介绍了主键映射和属性映射以及映射默认值问题,下一节我们详细讲解关系映射。三 个月没写博客主要是私下去录制了ASP.NET Core MVC基础进阶视频,涉及到一些细节和基本原理,感兴趣的童鞋可以了解下,若您有任何疑问可私信于我,链接地址:https://study.163.com/course/courseMain.htm?courseId=1005573012&share=2&shareId=400000000517056,后续也会发布到腾讯课堂。我们下节再会,感谢您的耐心阅读。

posted @ 2018-10-22 08:28  Jeffcky  阅读(2729)  评论(6编辑  收藏  举报