最近使用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简单测试过了,结果也是一样,那么估计是有意这么做的,所以大家使用时多留意吧

EFCore:关于DDD中值对象(Owns)无法更新数值的更多相关文章

  1. 关于项目中值对象Identifier的设计-领域驱动

    到现在为止做了不项目,发现每个实体都会有个相应的值对象. 先简单说一下值对象和实体之间的区别: (以下内容来着<领域驱动设计>一书) 当一个小孩画画的时候,他注意的是画笔的颜色和笔尖的粗细 ...

  2. Code First05--CodeFirst中值对象

    今天主要介绍EF Code First中一个高级部分:Value Object,中文翻译过来叫做值对象. 所谓的值对象就是一些没有生命周期,也没有业务逻辑上唯一标识符的类.哪些类是Entity,哪些类 ...

  3. C#中实现对象间的更新操作

    最近工作的时候遇到一个问题,根据Web端接收到的对象obj1,更新对应的对象值ogj2.先判断obj1中属性值是否为null, 若不等于null,则更新obj2中对应属性值:若等于null,则保持ob ...

  4. DDD中的EFCore

    EFCore在DDD中的使用 在DDD中,我们对聚合根的操作都会通过仓储去获取聚合实例. 因为聚合根中可能会含有实体属性,值对象属性,并且,在DDD中,我们所设计的领域模型都是充血模型.所以,在对聚合 ...

  5. DDD实战与进阶 - 值对象

    目录 DDD实战与进阶 - 值对象 概述 何为值对象 怎么运用值对象 来看一个例子 值对象的持久化 总结 DDD实战与进阶 - 值对象 概述 作为领域驱动设计战术模式中最为核心的一个部分-值对象.一直 ...

  6. DDD 领域驱动设计-Value Object(值对象)如何使用 EF 进行正确映射

    写在前面 首先,这篇博文是用博客园新发布的 MarkDown编辑器 编写的,这也是我第一次使用,语法也不是很熟悉,但我觉得应该会很爽,博文后面再记录下用过的感受,这边就不多说. 阅读目录: 上一篇回顾 ...

  7. [0] DDD领域驱动设计(二) 之 值对象

    DDD中实体对象与值对象的解释比较抽象.主要根据持续性与 ID 识别来区分. ID并非某一对象的直观自然属性,而是在分析建模之 后,赋给模型中的实体类,来达到跟踪,区别,存储目的的一个特值. 结合项目 ...

  8. [Abp vNext 源码分析] - 5. DDD 的领域层支持(仓储、实体、值对象)

    一.简要介绍 ABP vNext 框架本身就是围绕着 DDD 理念进行设计的,所以在 DDD 里面我们能够见到的实体.仓储.值对象.领域服务,ABP vNext 框架都为我们进行了实现,这些基础设施都 ...

  9. DDD中实体与值对象是干什么的

    实体值对象的含义 我们前面已经讲过领域的概念, 今天来讲讲实体, 实体是我们进行设计领域模型时的基础单元, 与之有关的是值对象, 接下来先梳理一下实体以及值对象的含义,然后讲讲他们俩的关系, 希望通过 ...

随机推荐

  1. 【Linux】【Basis】文件系统

    FHS:Filesystem Hierarchy Standard Web site: https://wiki.linuxfoundation.org/lsb/fhs http://www.path ...

  2. 3.3 GO字符串处理

    strings方法 index 判断子字符串或字符在父字符串中出现的位置(索引)Index 返回字符串 str 在字符串 s 中的索引( str 的第一个字符的索引),-1 表示字符串 s 不包含字符 ...

  3. springboot整合jetty

    1.jetty介绍 通常我们进行Java Web项目开发,必须要选择一种服务器来部署并运行Java应用程序,Tomcat和Jetty作为目前全球范围内最著名的两款开源servlet容器,该怎么选呢. ...

  4. notepad++ 连接远程服务器

    前言:为了便于编辑 linux 上的文件,因此通过 notepad++ 连接服务器后打开,编辑完,保存即可 1. 打开 notepad++,安装插件 2. 搜索 NppFtp,找到后 点击 安装/in ...

  5. “==” 和 equals()的区别

    ※ "==" 和 equals()的区别 ※ == :比较. 基本数据类型比较的是值:. 引用类型比较的是地址值. ※ equals(Object o):1)不能比较基本数据类型, ...

  6. PMP过程组与知识领域

    过程组知识领域 启动 规划 执行 监控 结尾 整合管理 制定项目章程 制定项目计划 指导与管理项目工作 监控项目工作 结束项目过程或阶段 项目管理知识 实施整体变更控制 范围管理 规划范围管理 确认范 ...

  7. NOAA数据下载方法

    NOAA OneStop https://data.noaa.gov/onestop/about NOAA 数据搜索平台,在一个地方同时搜索NOAA的 Geophysical, oceans, coa ...

  8. Google Earth Engine 批量点击RUN任务,批量取消正在上传的任务

    本文内容参考自: https://blog.csdn.net/qq_21567935/article/details/89061114 https://blog.csdn.net/qq_2156793 ...

  9. CF116B Little Pigs and Wolves 题解

    Content 有一张 \(n\times m\) 的地图,其中,\(\texttt{P}\) 代表小猪,\(\texttt{W}\) 代表狼.如果狼的上下左右有一头以上的小猪,那么它会吃掉其中相邻的 ...

  10. LuoguP7679 [COCI2008-2009#5] JABUKA 题解

    Content Mirko 拥有 \(R\) 个红苹果和 \(G\) 个绿苹果,他想把他分给若干个朋友,使得所有朋友分得的红苹果个数和绿苹果个数都一样.现给定 \(R,G\),请你帮助 Mirko 找 ...