第三节:DDD基础(相关概念、DbContextFactory、随机数、FluentApi配置、实体属性、充血模型写法、值对象及比较)
一. 相关概念
1. 单体结构
优点:
便于维护
缺点:
耦合;技术栈统一,软件包版本锁定;一崩全崩;升级周期长;无法局部扩容
2. 微服务结构
优点:
耦合性低,易于开发和维护;可以用不同技术栈;可以单独扩容;互相隔离,影响小;部署周期短。
缺点:
对运维能力要求高;运行效率会降低;技术要求高,需要处理事务最终一致性等问题。
3.DDD
DDD(Domain-driven design,领域驱动设计)是一个很好的应用于微服务架构的方法论.
在项目的全生命周期内,所有岗位的人员都基于对业务的相同的理解来开展工作。所有人员站在用户的角度、业务的角度去思考问题,而不是站在技术的角度去思考问题.
4.领域和领域模型
(1).领域(Domain)
核心域:解决项目的核心问题,和组织业务紧密相关。
支撑域:解决项目的非核心问题,则具有组织特性,但不具有通用性。
通用域:解决通用问题,没有组织特性
(2).领域模型(Domain Model)
对于领域内的对象进行建模,从而抽象出来模型。我们的项目应该开始于创建领域模型,而不是考虑如何设计数据库和编写代码。使用领域模型, 我们可以一直用业务语言去描述和构建系统,而不是使用技术人员的语言。
5.通用语言和界限上下文
(1) 通用语言:一个拥有确切含义的、没有二义性的语言.
(2) 界限上下文:通用语言离不开特定的语义环境,只有确定了通用语言所在的边界,才能没有歧义的描述一个业务对象。
6.标识符、实体和值对象
(1).标识符:用来唯一定位一个对象,在数据库中我们一般用表的主键来实现“标识符”。主键和标识符的思考角度不同。
(2).实体:拥有唯一的标识符,标识符的值不会改变,而对象的其他状态则会经历各种变化。标识符用来跟踪对象状态变化,一个实体的对象无论怎样变化,我们都能通过标识符定位这个对象。
(实体一般的表现形式就是EF Core中的实体类)
(3).值对象:没有标识符的对象,也有多个属性,依附于某个实体对象而存在。比如“商家”的地理位置、衣服的RGB颜色
(值对象一般的表现形式:api接口的参数接收类、结果处理类等)
PS:定义为值对象和普通属性的区别:体现整体关系
7. 聚合和聚合根
(1).聚合的目的
高内聚,低耦合,有关系的实体紧密协作,关系很弱的实体被隔离。
(2).聚合根(Aggregate Root)
把关系紧密的实体放到一个聚合中,每个聚合中有一个实体作为聚合根。所有对于聚合内对象的访问都通过聚合根来进行,外部对象只能持有对聚合根的引用。
PS:聚合根不仅仅是实体,还是所在聚合的管理者。
(3).聚合的意义
聚合体现的是现实世界中整体和部分的关系,比如订单与订单明细。整体封装了对部分的操作,部分与整体有相同的生命周期。
部分不会单独与外部系统单独交互,与外部系统的交互都由整体来负责。
(4).聚合的判断标准
聚合的判断标准:实体是否是整体和部分的关系,是否存在着相同的生命周期
8.领域服务和应用服务
(1).领域服务
对于聚合内的业务逻辑,我们编写领域服务(Domain Service),聚合中的实体中没有业务逻辑代码,只有对象的创建、对象的初始化、状态管理等个体相关的代码。
(2). 应用服务
对于跨聚合协作以及聚合与外部系统协作的逻辑,我们编写应用服务(Application Service),应用服务协调多个领域服务、外部系统来完成一个用例。
9. 仓储和工作单元
(1).仓储(Repository)
仓储负责按照要求从数据库中读取数据以及把领域服务修改的数据保存回数据库。
PS:聚合内的数据操作是关系非常紧密的,我们要保证事务的强一致性,而聚合间的协作是关系不紧密的,因此我们只要保证事务的最终一致性即可。
(2).工作单元(Unit Of Work)
聚合内的若干相关联的操作组成一个“工作单元”,这些工作单元要么全部成功,要么全部失败。
10.领域事件和集成事件
(1).领域事件(Domain Events)
微服务内部的,进程内的通信。(在同一个微服务内的聚合之间的事件传递。使用进程内的通信机制完成)
(2).集成事件(Integration Events)
微服务之间的,进程外的通信。(跨微服务的事件传递。使用事件总线 EventBus、Cap实现)
11.充血模型和贫血模型
(1).贫血模型
一个类中只有属性或者成员变量,没有方法。
(2).充血模型
一个类中既有属性、成员变量,也有方法。
二. 前置补充1
1. codefirst新的映射代码
需要单独创建一个类,实现IDesignTimeDbContextFactory接口中的CreateDbContext方法,在这里面配置连接字符串。
UserDbContext
public class UserDbContext : DbContext
{
public DbSet<UserInfo> User { get; private set; }
public DbSet<UserLoginHistory> UserLoginHistory { get; private set; }
public DbSet<UserLoginFail> UserLoginFail { get; private set; }
public UserDbContext(DbContextOptions<UserDbContext> options) : base(options)
{
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
//从当前程序集中加载实现了IDesignTimeDbContextFactory接口的配置类
modelBuilder.ApplyConfigurationsFromAssembly(this.GetType().Assembly);
//等价
//modelBuilder.ApplyConfigurationsFromAssembly(Assembly.GetExecutingAssembly());
}
/// <summary>
/// 这种写法主要针对没有单独的配置类的写法
/// </summary>
/// <param name="optionsBuilder"></param>
//protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
//{
// optionsBuilder.UseSqlServer(@"Server=localhost;Database=DDD1;User ID=sa;Password=123456;");
//}
}
DbContextFactory
/// <summary>
/// DB迁移的时候使用
/// </summary>
public class DbContextFactory : IDesignTimeDbContextFactory<UserDbContext>
{
public UserDbContext CreateDbContext(string[] args)
{
var optionsBuilder = new DbContextOptionsBuilder<UserDbContext>();
optionsBuilder.UseSqlServer("Server=localhost;Database=DDD1;User ID=sa;Password=123456;");
return new UserDbContext(optionsBuilder.Options);
}
}
特别注意:
上述写法是针对每个实体有单独的配置类的写法,如下:
2. 随机数新写法
//不存在高并发下的重复问题
Random.Shared.Next(1000, 9999).ToString();
3. 映射关系的FluentApi配置
一对一:HasOne(…).WithOne (…);
一对多:HasOne(…).WithMany(…);
多对多:HasMany (…).WithMany(…);
三. EFCore对实体属性操作秘密
1. EF Core是通过实体对象的属性的get、set来进行属性的读写吗?
答案:
基于性能和对特殊功能支持的考虑,EF Core在读写属性的时候,如果可能,它会直接跳过get、set,而直接操作真正存储属性值的成员变量。
2. 结论总结
(1).EF Core在读写实体对象的属性时,会查找属性对应的成员变量,如果能找到,EF Core会直接读写这个成员变量的值,而不是通过set和get代码块来读写。
(2).EF Core会尝试按照命名规则去直接读写属性对应的成员变量,只有无法根据命名规则找到对应成员变量的时候,EF Core才会通过属性的get、set代码块来读写属性值。
(3).可以在FluentAPI中通过UsePropertyAccessMode()方法来修改默认的这个行为【不建议】。
3. 实操
(1). 准备两个实体,Dog1和Dog2,其中Dog1中的Name属性使用的成员变量为name,即同名小写; Dog2中的Name属性使用的成员变量为:testName, 没有任何规律。
Dog1代码
public class Dog1
{
public long Id { get; set; }
private string name;
public string Name
{
get
{
Console.WriteLine("get被调用");
return name;
}
set
{
Console.WriteLine("set被调用");
name = value;
}
}
}
Dog2代码
public class Dog2
{
public long Id { get; set; }
private string testName;
public string Name
{
get
{
Console.WriteLine("get被调用");
return testName;
}
set
{
Console.WriteLine("set被调用");
testName = value;
}
}
}
(2). 测试Dog1和Dog2的新增(查询同理)
Dog1:仅输出1次set被调用
代码
MyDbContext dbContext = new();
{
Dog1 dog = new()
{
Name = "lmr"
};
dbContext.Dog1.Add(dog);
int count = dbContext.SaveChanges();
Console.WriteLine($"执行成功,条数:{count}");
}
运行结果
Dog2:输出1次set被调用 + 5次get被调用
代码
{
Dog2 dog = new()
{
Name = "lmr"
};
dbContext.Dog2.Add(dog);
int count = dbContext.SaveChanges();
Console.WriteLine($"执行成功,条数:{count}");
}
运行结果
四. EFCore充血模型各种需求
(一). 充血模型的实现要求
1. 属性是只读的或者是只能被类内部的代码修改 【特征1】
【解决方案:把属性的set定义为private或者init,然后通过构造方法为这些属性赋予初始值
eg:public Guid Id { get; init; } //init表示只读,仅允许构造函数对其进行修改
public PhoneNumber PhoneNumber { get; private set; } //private set 表示只允许该类中的方法或构造函数对其进行修改
】
2. 需要定义有参数的构造方法 【特征2】
【解决方案:
方案1:先定义一个private无参的构造方法,专门供EFCore使用;然后就可以随意构建有参的构造方法了。【推荐】
方案2:实体类中不定义无参构造方法,只定义有意义的有参构造方法,但是要求构造方法中的参数的名字和属性的名字一致【这是EFCore原理所要求的】
】
3. 有的成员变量没有对应属性,但是这些成员变量需要映射为数据表中的列,也就是我们需要把私有成员变量映射到数据表中的列。 【特征3】
【解决方案:builder.Property("成员变量名")
eg:private string passWordHash; //密码的散列值Md5 (不属于属性的成员变量)
builder.Property("passWordHash").HasMaxLength(100).IsUnicode(false);
】
4. 有的属性是只读的,也就是它的值是从数据库中读取出来的,但是我们不能修改属性值。 【特征4】
【解决方案:EF Core中提供了“支持字段”(backing field)来支持这种写法:在配置实体类的代码中,使用HasField(“成员变量名”)来配置属性。
】
5. 有的属性不需要映射到数据列,仅在运行时被使用。 【特征5】
【解决方案:使用Ignore()来配置忽略这个属性。 】
(二) . 实操
1. 通过Nuget安装程序集【Microsoft.EntityFrameworkCore.SqlServer】【Microsoft.EntityFrameworkCore.Tools】
2. 新建User充血模型类,实现上述5个特征
代码:
/// <summary> /// User实体类(充血模型) /// </summary> public record User { public int Id { get; init; } //【特征1】 public string Name { get; private set; } //【特征1】 public int Age { get; private set; } //【特征1】 public DateTime AddTime { get; private set; }//【特征1】 private string passwordHash; //【特征3】 private readonly string remark; public string Remark //【特征4--只读,不能插入】 { get { return remark; } } public string Tag { get; set; } //【特征5】 private User() //【特征2---给EFCore用来加载数据用的】 { } public User(string name,int age)//【特征2---给程序员写代码用的】 { this.Name = name; this.Age = age; this.AddTime=DateTime.Now; } public void ChangeName(string newValue) { this.Name=newValue; } public void ChangePwd(string newValue) { if (newValue.Length < 3) { throw new ArgumentException("密码太短"); } this.passwordHash=HashHelper.ComputeMd5Hash(newValue); } }
3. 新建UserConfig配置类,配置 【特征3】【特征4】【特征5】
代码:
public class UserConfig : IEntityTypeConfiguration<User>
{
public void Configure(EntityTypeBuilder<User> builder)
{
builder.Property("passwordHash");//【特征3】
builder.Property(u => u.Remark).HasField("remark");//【特征4】
builder.Ignore(u => u.Tag);//【特征5】
}
}
4. 新建MyDbContext类,继承DbContext类
代码:
public class MyDbContext : DbContext
{
public DbSet<User> User { get; set; }
public MyDbContext(DbContextOptions<MyDbContext> options):base(options)
{
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
//从当前程序集中加载实现了IDesignTimeDbContextFactory接口的配置类
modelBuilder.ApplyConfigurationsFromAssembly(this.GetType().Assembly);
}
}
5. 新建 DbContextFactory类,实现接口IDesignTimeDbContextFactory<MyDbContext>,用DB迁移
代码:
/// <summary>
/// DB迁移的时候使用
/// </summary>
public class DbContextFactory : IDesignTimeDbContextFactory<MyDbContext>
{
public MyDbContext CreateDbContext(string[] args)
{
var optionsBuilder = new DbContextOptionsBuilder<MyDbContext>();
optionsBuilder.UseSqlServer("Server=localhost;Database=DDD1;User ID=sa;Password=123456;");
return new MyDbContext(optionsBuilder.Options);
}
}
6. 运行指令【add-migration xxxx】 【update-database】
7. 测试
//方式1:通过注入的方式获取EFCore上下文
//ServiceCollection services = new();
//services.AddDbContext<MyDbContext>(opt => opt.UseSqlServer("Server=localhost;Database=DDD1;User ID=sa;Password=123456;"));
//var dbContext=services.BuildServiceProvider().GetRequiredService<MyDbContext>();
//方式2:直接创建
var optionsBuilder = new DbContextOptionsBuilder<MyDbContext>();
optionsBuilder.UseSqlServer("Server=localhost;Database=DDD1;User ID=sa;Password=123456;");
MyDbContext dbContext = new MyDbContext(optionsBuilder.Options);
//操作DB
User user = new User("ypf", 20);
user.ChangePwd("123456");
//user.Remark = "hhhh"; //只读字段,不能插入
dbContext.User.Add(user);
int count=dbContext.SaveChanges();
Console.WriteLine($"执行成功,条数:{count}");
Console.ReadLine();
五. EFCore实现值对象和值对象的比较
(一). 说明
1. 从属实体类型
(1).比如用户姓名,可能包括中文姓名 和 英文姓名,就可以采用从属实体配置。
public record NamesEntity(string ChineseName, string EnglishName);
(2).FluentApi的配置:
builder.OwnsOne(c=>c.Name, nb => {
nb.Property(e=>e.ChineseName).HasMaxLength(20).IsUnicode(false);
nb.Property(e=>e.EnglishName).HasMaxLength(20).IsUnicode(true);
});
2. 枚举类型
(1).实体的属性可以定义为枚举类型,枚举类型的属性在数据库中默认是以整数类型来保存的,eg:0,1,2,对于直接操作数据库的人员来讲, 0,1,2这样的值,没有"CNY"(人民币)、"USD"(美元)、"NZD"(新西兰元)等这样的字符串类型值可读性更强。
enum MoneyEnum { CNY, USD, NZD}
(2). FluentApi配置
EFCore中可以在Fluent API中用HasConversion<string>()把枚举类型的值配置成保存为字符串。
builder.Property(c => c.Money).HasMaxLength(20).IsUnicode(false).HasConversion<string>();
3. 从属实体类型的比较
详见:ExpressionHelper 方法
代码调用:var cities = ctx.Cities.Where(ExpressionHelper.MakeEqual((Region c) => c.Name, new MultilingualString("北京", "BeiJing")));
二. 实操
1. 通过Nuget安装程序集【Microsoft.EntityFrameworkCore.SqlServer】【Microsoft.EntityFrameworkCore.Tools】
2. 新建CrewInfo实体类,内含有:
(1).MoneyEnum,枚举属性
/// <summary>
/// 钱的枚举
/// </summary>
public enum MoneyEnum { CNY, USD, NZD }
(2).NamesEntity,从属实体类型
/// <summary>
/// 姓名实体
/// </summary>
/// <param name="chineseName">中文姓名</param>
/// <param name="englishName">英文姓名</param>
public record NamesEntity(string ChineseName, string EnglishName);
CrewInfo实体类
/// <summary>
/// 船员信息
/// </summary>
public class CrewInfo
{
public int Id { get; init; }
/// <summary>
/// 从属实体类型
/// </summary>
public NamesEntity Names { get; init; }
/// <summary>
/// 枚举类型
/// </summary>
public MoneyEnum Money { get; set; }
public int age { get; private set; }
private CrewInfo() //【特征2】
{
}
public CrewInfo(NamesEntity names,MoneyEnum money,int age)
{
this.Names = names;
this.Money = money;
this.age = age;
}
}
3. 新建配置类CrewInfoConfig,配置枚举属性在DB中以字符串的形式显示;配置从属实体属性
代码
public class CrewInfoConfig : IEntityTypeConfiguration<CrewInfo>
{
public void Configure(EntityTypeBuilder<CrewInfo> builder)
{
builder.Property(c => c.Money).HasMaxLength(20).IsUnicode(false).HasConversion<string>(); //配置枚举转换为字符串存储
//配置从属实体类型
builder.OwnsOne(c => c.Names, nb =>
{
nb.Property(e => e.EnglishName).HasMaxLength(20).IsUnicode(false);
nb.Property(e => e.ChineseName).HasMaxLength(20).IsUnicode(true);
});
}
}
4. 引进帮助类ExpressionHelper,用来比较从属实体类型的是否相同
代码
public class ExpressionHelper { public static Expression<Func<TItem, bool>> MakeEqual<TItem, TProp>(Expression<Func<TItem, TProp>> propAccessor, TProp? other) where TItem : class where TProp : class { var e1 = propAccessor.Parameters.Single(); BinaryExpression? conditionalExpr = null; foreach (var prop in typeof(TProp).GetProperties()) { BinaryExpression equalExpr; object? otherValue = null; if (other != null) { otherValue = prop.GetValue(other); } Type propType = prop.PropertyType; var leftExpr = MakeMemberAccess(propAccessor.Body, prop); Expression rightExpr = Convert(Constant(otherValue), propType); if (propType.IsPrimitive) { equalExpr = Equal(leftExpr, rightExpr); } else { equalExpr = MakeBinary(ExpressionType.Equal, leftExpr, rightExpr, false, prop.PropertyType.GetMethod("op_Equality")); } if (conditionalExpr == null) { conditionalExpr = equalExpr; } else { conditionalExpr = AndAlso(conditionalExpr, equalExpr); } } if (conditionalExpr == null) { throw new ArgumentException("There should be at least one property."); } return Lambda<Func<TItem, bool>>(conditionalExpr, e1); } }
5. 新建MyDbContext类,继承DbContext类
代码
public class MyDbContext : DbContext { public DbSet<CrewInfo> CrewInfo { get; set; } public MyDbContext(DbContextOptions<MyDbContext> options) : base(options) { } protected override void OnModelCreating(ModelBuilder modelBuilder) { base.OnModelCreating(modelBuilder); //从当前程序集中加载实现了IDesignTimeDbContextFactory接口的配置类 modelBuilder.ApplyConfigurationsFromAssembly(this.GetType().Assembly); } }
6. 新建 DbContextFactory类,实现接口IDesignTimeDbContextFactory<MyDbContext>,用DB迁移
代码
/// <summary> /// DB迁移的时候使用 /// </summary> public class DbContextFactory : IDesignTimeDbContextFactory<MyDbContext> { public MyDbContext CreateDbContext(string[] args) { var optionsBuilder = new DbContextOptionsBuilder<MyDbContext>(); optionsBuilder.UseSqlServer("Server=localhost;Database=DDD1;User ID=sa;Password=123456;"); return new MyDbContext(optionsBuilder.Options); } }
7. 运行指令【add-migration xxxx】 【update-database】
8. 测试
var optionsBuilder = new DbContextOptionsBuilder<MyDbContext>();
optionsBuilder.UseSqlServer("Server=localhost;Database=DDD1;User ID=sa;Password=123456;");
MyDbContext dbContext = new(optionsBuilder.Options);
//1. 添加数据
{
CrewInfo crewInfo = new CrewInfo(new NamesEntity("李马茹", "lmr"), MoneyEnum.CNY, 20);
dbContext.Add(crewInfo);
int count = dbContext.SaveChanges();
Console.WriteLine($"执行成功,条数:{count}");
}
//2. 值对象的比较(从属实体类型比较)
{
var data = dbContext.CrewInfo.Where(ExpressionHelper.MakeEqual((CrewInfo c) => c.Names, new NamesEntity("李马茹", "lmr"))).FirstOrDefault();
if (data == null)
{
Console.WriteLine("没有查到相同数据");
}
else
{
Console.WriteLine($"{data.Id},{data.Names},{data.Money}");
}
}
!
- 作 者 : Yaopengfei(姚鹏飞)
- 博客地址 : http://www.cnblogs.com/yaopengfei/
- 声 明1 : 如有错误,欢迎讨论,请勿谩骂^_^。
- 声 明2 : 原创博客请在转载时保留原文链接或在文章开头加上本人博客地址,否则保留追究法律责任的权利。