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

前言

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

属性映射探讨

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

    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。这些都是最基础的东西,在我写的书中也有详细介绍,就再也不啰嗦了。咱们从如下几点开始探讨。less

主键映射并添加数据探讨

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

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

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

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

我我的比较习惯对于主键也手动经过HasKey进行配置。在添加数据时根据约定主键自动增加,如上述。固然咱们也能够手动配置在添加仍是更新时自增加,以下:spa

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

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

        // 摘要:
        //     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插入数据不一样,请参看此连接:http://www.javashuo.com/article/p-uerkgmpc-gc.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/),追踪图中可获取到对象全部状态和主键是否已经设置,因此最终解决方案以下:code

            _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,后续也会发布到腾讯课堂。咱们下节再会,感谢您的耐心阅读。

相关文章
相关标签/搜索