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

  • 相关阅读:
    MS SQL入门基础:使用SQL Server Profiler
    收藏夹路径设置
    MS SQL入门基础:数据转换服务基本概念
    MS SQL入门基础:复制的概述和术语
    MS SQL入门基础:创建备份设备
    巧用ASP生成PDF文件
    MS SQL入门基础:sql 警报
    MS SQL入门基础:基于HTTP的数据访问
    MS SQL入门基础:XML文档与数据库表
    MS SQL入门基础:复制监视器
  • 原文地址:https://www.cnblogs.com/CreateMyself/p/9818785.html
Copyright © 2020-2023  润新知