zoukankan      html  css  js  c++  java
  • EFCore:关于DDD中值对象(Owns)无法更新数值

      最近使用DDD+EFCore时,使用EFCore提供的OwnsOne或者OwnsMany关联值对象保存数据,没想到遇到一个很奇怪的问题:值对象中的值竟然无法被EFCore保存!也没有抛出任何异常!我瞬间惊呆了!

      准确说,这里说的应该碰到的两个问题

      1、值对象中所有的数值数据都无法保存更新

      2、值对象中的数据0无法保存更新

      这两个问题初看有点摸不着头脑,后来不断的尝试,通过简单的打印SQL,发现了一些端倪,但是保存不了问什么不抛出异常呢?这让人有些费解,有点头大,决定先做个笔记,以后找个时间再去看看源码找找答案。

      首先,我创建了一个.net core控制台项目,尝试的.net core版本是3.1.10,数据库使用的是mysql(不知道是否与数据库有关),然后使用NUGET安装了如下包:  

        Microsoft.EntityFrameworkCore
        Microsoft.EntityFrameworkCore.Tools
        Microsoft.Extensions.Logging.Console
        Pomelo.EntityFrameworkCore.MySql

      然后创建如下文件:  

      
        using System;
        using System.Collections.Generic;
        using System.Text;
        
        namespace ConsoleApp8
        {
            public class MyTable
            {
                public MyTable()
                {
                    MyOwns = new MyOwns();
                }
                public int Id { get; set; }
                public decimal DecimalValue1 { get; set; }
                public decimal DecimalValue2 { get; set; }
        
                public MyOwns MyOwns { get; set; }
            }
            public class MyOwns
            {
                public MyOwns() { }
                public MyOwns(decimal decimalValue1, decimal decimalValue2)
                {
                    DecimalValue1 = decimalValue1;
                    DecimalValue2 = decimalValue2;
                }
                public decimal DecimalValue1 { get; private set; }
                public decimal DecimalValue2 { get; private set; }
        
                public void Update(decimal decimalValue1, decimal decimalValue2)
                {
                    DecimalValue1 = decimalValue1;
                    DecimalValue2 = decimalValue2;
                }
            }
        
        }
    MyTable.cs
      
    using Microsoft.EntityFrameworkCore;
    using Microsoft.Extensions.Logging;
    using Microsoft.Extensions.Logging.Console;
    using System;
        using System.Collections.Generic;
        using System.Text;
        
        namespace ConsoleApp8
        {
            public class DemoDbContext : DbContext
            {
                public DemoDbContext(DbContextOptions options) : base(options)
                {
        
                }
        
                public DbSet<MyTable> MyTable { get; set; }
        
                #region Method
                /// <summary>
                /// 配置
                /// </summary>
                /// <param name="optionsBuilder"></param>
                protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
                {
                    var loggerFactory = LoggerFactory.Create(builder => builder.AddConsole());
                    optionsBuilder.UseLoggerFactory(loggerFactory);
                    optionsBuilder.EnableSensitiveDataLogging();
                    base.OnConfiguring(optionsBuilder);
                }
                /// <summary>
                /// 初始化
                /// </summary>
                /// <param name="modelBuilder"></param>
                protected override void OnModelCreating(ModelBuilder modelBuilder)
                {
                    base.OnModelCreating(modelBuilder);
        
                    var builder = modelBuilder.Entity<MyTable>();
                    builder.HasKey(p => p.Id);
                    builder.Property(p => p.DecimalValue1).HasColumnType($"decimal(18,6)");
                    builder.Property(p => p.DecimalValue2).HasDefaultValue(0m).HasColumnType($"decimal(18,6)");
                    builder.OwnsOne(f => f.MyOwns, o =>
                    {
                        o.Property(p => p.DecimalValue1).HasColumnType($"decimal(18,6)");
                        o.Property(p => p.DecimalValue2).HasDefaultValue(0m).HasColumnType($"decimal(18,6)");
                    });
                }
                #endregion
            }
        }
    DemoDbContext.cs
      
        using Microsoft.EntityFrameworkCore;
        using Microsoft.EntityFrameworkCore.Design;
        using System;
        using System.Collections.Generic;
        using System.Text;
        
        namespace ConsoleApp8
        {
            public class DemoMigrationsDbContextFactory : IDesignTimeDbContextFactory<DemoDbContext>
            {
                public DemoDbContext CreateDbContext(string[] args)
                {
                    var builder = new DbContextOptionsBuilder<DemoDbContext>()
                           .UseMySql("Server=192.168.209.128;Port=3306;Database=demodb;Uid=root;Pwd=123456");
        
                    return new DemoDbContext(builder.Options);
                }
            }
        }
    DemoMigrationsDbContextFactory.cs

      然后使用【程序包管理器控制台】(导航栏【工具】=》【NuGet包管理器】=》【程序包管理器控制台】)输入 Add-Migration init 生成迁移,输入 Update-Database 更新迁移至数据库,最后的结构类似这样子:

      

       问题一:值对象中所有的数值数据都无法保存更新

      这个问题最后发现挺巧合的,一方面又是因为EFCore生成的迁移中Owns类型尽然是nullable(可空)类型,一方面是自己对值对象的使用有问题。

      同样的,在上面的MyTable类和MyOwns类中,同样的有DecimalValue1和DecimalValue2两个数值,但是生成的迁移文件中两者就区别了:  

        migrationBuilder.CreateTable(
            name: "MyTable",
            columns: table => new
            {
                Id = table.Column<int>(nullable: false)
                    .Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn),
                DecimalValue1 = table.Column<decimal>(type: "decimal(18,6)", nullable: false),
                DecimalValue2 = table.Column<decimal>(type: "decimal(18,6)", nullable: false, defaultValue: 0m),
                MyOwns_DecimalValue1 = table.Column<decimal>(type: "decimal(18,6)", nullable: true),
                MyOwns_DecimalValue2 = table.Column<decimal>(type: "decimal(18,6)", nullable: true, defaultValue: 0m)
            },
            constraints: table =>
            {
                table.PrimaryKey("PK_MyTable", x => x.Id);
            });

      可以看到MyTable类中的属性被映射成 nullable:false ,而且使用 IsRequired(false) 设置时,生成迁移过程中将会抛出异常,但是MyOwns类中的属性竟然直接被映射成了 nullable:true !!!

      这样就问题来了,如果因为某些原因,导致数据库中这些字段未null,但是实体中的decimal等等属性是非空的,那不就。。。这种情况是可能存在的,比如我上线时是先更新脚本,在更新系统前如果保存数据,那这一列就有可能是null。

      如果仅仅因为这点,还不至于问题出现,但是如果在错误使用值对象(Owns)时就可能出现这种问题,直接上测试代码:

        class Program
        {
            static void Main(string[] args)
            {
                //清空表数据
                using (var db = new DemoMigrationsDbContextFactory().CreateDbContext(args))
                {
                    using (var connection = db.Database.GetDbConnection())
                    {
                        connection.Open();
                        var cmd = connection.CreateCommand();
                        cmd.CommandText = $@"delete from {nameof(MyTable)}";
                        cmd.ExecuteNonQuery();
                    }
                }
    
                //新增一条数据
                using (var db = new DemoMigrationsDbContextFactory().CreateDbContext(args))
                {
                    var myTable = new MyTable()
                    {
                        Id = 1,
                        DecimalValue1 = 1m,
                        DecimalValue2 = 2m,
                        MyOwns = new MyOwns(1m, 2m)
                    };
                    db.MyTable.Add(myTable);
                    db.SaveChanges();
                }
    
                //修改数值为空数据
                using (var db = new DemoMigrationsDbContextFactory().CreateDbContext(args))
                {
                    using (var connection = db.Database.GetDbConnection())
                    {
                        connection.Open();
                        var cmd = connection.CreateCommand();
                        cmd.CommandText = $@"update {nameof(MyTable)} set {nameof(MyTable.MyOwns)}_{nameof(MyOwns.DecimalValue1)}=null where Id=1";
                        cmd.ExecuteNonQuery();
                    }
                }
    
                //修改数据
                using (var db = new DemoMigrationsDbContextFactory().CreateDbContext(args))
                {
                    var myTable = db.MyTable.Find(1);
                    myTable.DecimalValue1 = 10m;
                    myTable.DecimalValue2 = 20m;
                    //myTable.MyOwns = new MyOwns(10m, 20m); //正确用法
                    myTable.MyOwns.Update(10m, 20m);    //错误用法,值对象应该赋值,不应该修改其里面的值!
                    db.SaveChanges();
                }
    
                Console.WriteLine("Ok.");
                Console.ReadLine();
            }
        }

      上面测试会打印出SQL,其中修改数据使用Find方法的查询SQL如下:  

        SELECT `m`.`Id`, `m`.`DecimalValue1`, `m`.`DecimalValue2`, `t`.`Id`, `t`.`MyOwns_DecimalValue1`, `t`.`MyOwns_DecimalValue2`
          FROM `MyTable` AS `m`
          LEFT JOIN (
              SELECT `m0`.`Id`, `m0`.`MyOwns_DecimalValue1`, `m0`.`MyOwns_DecimalValue2`
              FROM `MyTable` AS `m0`
              WHERE `m0`.`MyOwns_DecimalValue2` IS NOT NULL AND `m0`.`MyOwns_DecimalValue1` IS NOT NULL
          ) AS `t` ON `m`.`Id` = `t`.`Id`
          WHERE `m`.`Id` = @__p_0
          LIMIT 1

      可以看到,值对象中的数据是通过Left Join得到的,而且Left Join中的条件都是 IS NOT NULL ,这样值对象就相当于查出来一个null空对象,这样,值对象中的属性自然就不会被EFCore跟踪记录了。

      而如果此时,我们直接给值对象的属性赋值,那自然就不会被更新了,比如上面demo中,我使用的是值对象里面自定义的Update方法来更新数据,这种做法是错误的,确实,更新打印出来的SQL如下:  

        UPDATE `MyTable` SET `DecimalValue1` = @p0, `DecimalValue2` = @p1
          WHERE `Id` = @p2;
          SELECT ROW_COUNT();

      值对象应该赋值,不应该修改其里面的值,那怕只是修改一个属性也应该使用一个新的值对象来赋值,换句话说,我们应该把值对象当做int,string,DateTime等类型一样来看待!!!

      

      问题二:值对象中的数据0无法保存更新

      解决上面的问题一后,又遇到另一个问题,发现0无法被更新,而其它数据(如,1,2,3等)都可以被更新,测试代码如下:  

        class Program
        {
          static void Main(string[] args)
            {
                //清空表数据
                using (var db = new DemoMigrationsDbContextFactory().CreateDbContext(args))
                {
                    using (var connection = db.Database.GetDbConnection())
                    {
                        connection.Open();
                        var cmd = connection.CreateCommand();
                        cmd.CommandText = $@"delete from {nameof(MyTable)}";
                        cmd.ExecuteNonQuery();
                    }
                }
    
                //新增一条数据
                using (var db = new DemoMigrationsDbContextFactory().CreateDbContext(args))
                {
                    var myTable = new MyTable()
                    {
                        Id = 1,
                        DecimalValue1 = 1m,
                        DecimalValue2 = 2m,
                        MyOwns = new MyOwns(1m, 2m)
                    };
                    db.MyTable.Add(myTable);
                    db.SaveChanges();
                }
    
                //修改数据
                using (var db = new DemoMigrationsDbContextFactory().CreateDbContext(args))
                {
                    var myTable = db.MyTable.Find(1);
                    myTable.DecimalValue1 = 0m;
                    myTable.DecimalValue2 = 0m;
                    myTable.MyOwns = new MyOwns(0m, 0m);
                    db.SaveChanges();
                }
    
                Console.WriteLine("Ok.");
                Console.ReadLine();
            }
        }

      运行之后,修改数据部分的Find方法打印出的SQL如下:  

        SELECT `m`.`Id`, `m`.`DecimalValue1`, `m`.`DecimalValue2`, `t`.`Id`, `t`.`MyOwns_DecimalValue1`, `t`.`MyOwns_DecimalValue2`
          FROM `MyTable` AS `m`
          LEFT JOIN (
              SELECT `m0`.`Id`, `m0`.`MyOwns_DecimalValue1`, `m0`.`MyOwns_DecimalValue2`
              FROM `MyTable` AS `m0`
              WHERE `m0`.`MyOwns_DecimalValue2` IS NOT NULL AND `m0`.`MyOwns_DecimalValue1` IS NOT NULL
          ) AS `t` ON `m`.`Id` = `t`.`Id`
          WHERE `m`.`Id` = @__p_0
          LIMIT 1

      这一点和上面的例子是一样的,但是更新的SQL却是:  

        UPDATE `MyTable` SET `DecimalValue1` = @p0, `DecimalValue2` = @p1, `MyOwns_DecimalValue1` = @p2
          WHERE `Id` = @p3;
          SELECT `MyOwns_DecimalValue2`
          FROM `MyTable`
          WHERE ROW_COUNT() = 1 AND `Id` = @p3;

      可以看到,MyOwns_DecimalValue1和DecimalValue1、DecimalValue2都更新了,但是MyOwns_DecimalValue2没有被更新!!!

      这里,我们在用法上基本上没什么问题,于是我猜想是EFCore迁移映射导致的,查看DbContext的 OnModelCreating 方法:  

        /// <summary>
        /// 初始化
        /// </summary>
        /// <param name="modelBuilder"></param>
        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            base.OnModelCreating(modelBuilder);
    
            var builder = modelBuilder.Entity<MyTable>();
            builder.HasKey(p => p.Id);
            builder.Property(p => p.DecimalValue1).HasColumnType($"decimal(18,6)");
            builder.Property(p => p.DecimalValue2).HasDefaultValue(0m).HasColumnType($"decimal(18,6)");
            builder.OwnsOne(f => f.MyOwns, o =>
            {
                o.Property(p => p.DecimalValue1).HasColumnType($"decimal(18,6)");
                o.Property(p => p.DecimalValue2).HasDefaultValue(0m).HasColumnType($"decimal(18,6)");
            });
        }

      这里,MyOwns的DecimalValue1和DecimalValue2仅仅差别一个默认值!去掉DecimalValue2的默认值,运行测试,成功更新!!!但是奇怪的是,MyTable的DecimalValue1和DecimalValue2却不受默认值的影响。

      总结

      DDD(领域驱动设计)是应对复杂软件设计的利器,而EFCore为DDD中的实体,值类型等持久化提供了非常方便的解决方案,但是在使用时,我们要切记:

      1、值对象要当做和int,String,DateTime等类型一样使用,哪怕是修改值对象中一个属性,也需要从新创建一个值对象!

      2、EFCore提供的OwnsOne或者OwnsMany方法关联的值对象中的属性默认是可空的,而对实体则是会根据属性类型是否可空而定,所以使用时要根据自己的需求而定。

      3、EFCore提供的OwnsOne或者OwnsMany方法关联的值对象中的属性尽可能不要设置默认值,这里笔者只是用decimal类型碰到了,但是不排除还有其它类型也会有这样的问题

      4、目前这几点在.net 5.0简单测试过了,结果也是一样,那么估计是有意这么做的,所以大家使用时多留意吧

  • 相关阅读:
    关于ORALE将多行数据合并成为一行 报错未找到where关键字
    Input限制输入数字
    Dev Gridcontrol每行添加序号或者文本。
    Android studio SDK配置
    介数中心性快速计算
    Buuoj 被嗅探的流量
    Docker安装(win10)
    filter CTF
    MySQLdb._exceptions.OperationalError: (2026, 'SSL connection error: unknown error number')
    DNS解析原理(www.baidu.com)
  • 原文地址:https://www.cnblogs.com/shanfeng1000/p/14242788.html
Copyright © 2011-2022 走看看