马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。
您需要 登录 才可以下载或查看,没有账号?立即注册
×
EF Core 默认实现了许多值比力器,用于在实体状态追踪时查抄属性值是否被修改。故大多环境下,咱们不必要操纵心。但,凡是总有特别环境,有些数据固然值不相称,但所表现的意思是相称的。这种时间就不能依赖默认的比力器了。
老周举一个连外星人都知道的例子。假设有如许的实体类。- public class Company
- {
- public Guid CompID { get; set; }
- public required string Name { get; set; }
- public required string Phone { get; set; }
- }
复制代码 这个类表现某些公司信息,除主键外,两个属性分别表现公司名称和固话。银河系住民都相识,固话由区号和电话号码构成,且有两种写法:- (010)88988989
- 010-88988989
复制代码 也就是说,用户给 Phone 属性设置这两个值,指的是同一个固话。若以默认的字符串比力器,肯定会以为二者不相称的。以是,这就要咱们动手了。
要实现自界说的比力器,99% 的做法是从 ValueComparer 类派生。不必要重写任何成员,只提供三个方法(由对应的委托范例吸取)的实现,然后传给基类的构造函数即可。
必要的三个委托为:
1、Func:两个输入参数是 T 的值,返回值是 bool 范例。该委托用于判断两个值是否相称,相称就返回 true,不相称就返回 false。
2、Func:返回输入参数 T 的哈希值,整型。
3、Func:此委托用于创建“快照”,由 ChangeTracker 负责管理。返回的 T 实例就是创建的快照实例。对于简单范例,咱们不必要实现这个委托。它重要面向必要深度拷贝或存在嵌套数据的值,用于自界说属性值的拷贝。
对于这个固定电话,咱们要实现相称比力和哈希盘算,创建快照不必要实现,利用 EF Core 内置的就可以。
于是,界说 MyValueComparer 类。- public class MyValueComparer : ValueComparer<string>
- {
- /// <summary>
- /// 匹配规则
- /// </summary>
- private const string REGEX_PATTERN = "<strong>^\\(?(\\d{3,4})(?:\\)|-)?(\\d{6,})$</strong>";
- #region 辅助方法
- /// <summary>
- /// 分析固话号码
- /// </summary>
- /// <param name="no">输入值</param>
- /// <returns>返回区号和电话</returns>
- protected static (string code, string phone) ParsePhoneNo(string no)
- {
- // 分析号码
- var res = Regex.Match(no, REGEX_PATTERN);
- // 实际捕捉两个分组
- if(res.Success)
- {
- return (
- res.Groups[1].Value,
- res.Groups[2].Value
- );
- }
- return (string.Empty, string.Empty);
- }
- /// <summary>
- /// 两个固话号码是否相等
- /// </summary>
- protected static bool IsEqual(string? val1, string? val2)
- {
- if (val1 == null && val2 == null)
- return true;
- if (val1 == null && val2 != null)
- return false;
- if (val1 != null && val2 == null)
- return false;
- string code1, phone1; // 第一个号码
- string code2, phone2; // 第二个号码
- (code1, phone1) = ParsePhoneNo(val1);
- (code2, phone2) = ParsePhoneNo(val2);
- // 两个号码的区号与电话是否相同
- return (code1 == code2 && phone1 == phone2);
- }
- /// <summary>
- /// 计算哈希值
- /// </summary>
- protected static int GetHash(string val)
- {
- (string code, string ph) = ParsePhoneNo(val);
- return HashCode.Combine(code, ph);
- }
- #endregion
- public MyValueComparer()
- :base((p1, p2) => IsEqual(p1, p2), p => GetHash(p))
- {
- // .........
- }
- }
复制代码 上面代码中,老周利用正则表达式来提取固话中的区号的号码。先表明一下这个匹配规则。- ^\\(?(\\d{3,4})(?:\\)|-)?(\\d{6,})$
复制代码
- ^ 表现开头字符;
- \(? 表现开头的字符大概是左括号,但大概不出现,以是用 ? 匹配;
- (\d{3,4}) 这是一个分组,匹配时会把它存储起来,\d 是数字,{3,4} 表现数字的出现次数为最少三次最多四次。即区号由三到四个数字构成;
- (?:\)|-)? 表现可选的右括号大概“-”。(?: ...) 克制被辨认为分组,由于我们对右括号和“-”不感爱好,匹配效果也不要存储这些字符,以是用 ?: 告诉正则处理处罚引擎可以匹配它,但不要存到效果中,我们不必要;“|”表现分支(并列、或),即可以出现右括号或“-”。反面的 ? 表现这个分组可以出现可以不出现。着实这里用“+”也可以,右括号和“-”应该至少出现一次;
- (\d{6,})$ “$”表现字符串末了,号码部门同样也是匹配数字,{6, } 表现至少出现六次。也可以是 {6,8},不外这里老周就没把规则定那么严酷。
正则匹配乐成后,应从 Groups 集会合找效果,不要去 Captures 中找。Groups 聚集存储了两个分组中匹配的数字字符(区号和号码)。- if(res.Success)
- {
- return (
- res.Groups[1].Value,
- res.Groups[2].Value
- );
- }
复制代码 Groups 集会合,第一个元素存的是整个字符串,以是要从第二个元素获取分组的文本(索引1起)。
好了,如今咱们实现数据库上下文类,并在设置数据库模子时应用自界说的比力器。- public class MyContext : DbContext
- {
- public DbSet<Company> Companies { get; set; }
- protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
- {
- optionsBuilder.UseSqlServer(<em>"server=.\\Test;database=some_db;Trust Server Certificate=True;Integrated Security=True"</em>);
- }
- protected override void OnModelCreating(ModelBuilder modelBuilder)
- {
- modelBuilder.Entity<Company>(ent =>
- {
- ent.Property(x => x.CompID).HasColumnName("cmp_id");
- ent.Property(x => x.Name).HasColumnName("cmp_name").HasMaxLength(45);
- ent.Property(x => x.Phone).HasColumnName("cmp_phone").HasMaxLength(15)
- <strong>.Metadata.SetValueComparer(</strong><strong>new</strong><strong> MyValueComparer());</strong>
- ent.HasKey(x => x.CompID).HasName("PK_Company");
- });
- }
- }
复制代码 要注意的是,PropertyBuilder 的成员方法/扩展方法并没有让咱们设置比力器的(利用 HasConversion 方法除外,设置值转换器时可以设置比力器。但这种只得当你必要转换范例的环境)。不外没事,咱们可以通过元数据对象来设置。- .Metadata.SetValueComparer(new MyValueComparer())
复制代码 访问 PropertyBuilder.Metadata 得到的是属性元数据。要是访问 EntityTypeBuilder 的 Metadata 呢,那就是实体范例元数据。
下面,创建数据库上下文实例,先动态创建数据库,然后我们修改第一条记载的 Phone 属性为【与原电话号码类似但格式差异的电话】。- using (var c = new MyContext())
- {
- var created = c.Database.EnsureCreated();
- if(created)
- {
- c.Companies.AddRange([
- new Company
- {
- Name = "三鬼贸易有限公司",
- Phone = "020-55128130"
- },
- new Company
- {
- Name = "一口焖信息服务有限公司",
- Phone = "0765-20881919"
- }
- ]);
- c.SaveChanges();
- }
- }
- using(var c = new MyContext())
- {
- var obj = c.Companies.FirstOrDefault();
- if(obj != null)
- {
- obj.Phone = "<strong>(020)55128130</strong>";
- c.<em>ChangeTracker.DetectChanges</em>(); // 检测是否更改
- // 打印跟踪信息
- Console.WriteLine(c.ChangeTracker.ToDebugString(ChangeTrackerDebugStringOptions.IncludeProperties));
- }
- }
复制代码 在插入数据时,咱们设置的值是 020-55128130,之后我们修改为 (020)55128130,咱们以为这是同一个号码。由于这里老周没有调用 SaveChanges 方法,即不会生存到数据库。以是,必要调用一次 ChangeTracker.DetectChanges 方法,欺压 context 做一轮更改扫描。末了向控制台打印跟踪信息。
属性修改后,跟踪信息如下:- Company {CompID: 286554a7-e1f4-4ab1-e791-08deae71e616} <strong>Unchanged</strong>
- CompID: '286554a7-e1f4-4ab1-e791-08deae71e616' PK
- Name: '三鬼贸易有限公司'
- Phone: '<strong>(020)55128130</strong>' Originally '<strong>020-55128130</strong>'
复制代码 抠亮眼睛看清晰呢,属性值确实是变了的,但由于咱们自界说的比力器在作怪,以是,实体的状态依然被标记为 Unchanged。
-----------------------------------------------------------------------------------------------
接下来,咱们看看值转器和值比力器一起用的环境。- // 表示颜色的结构
- public struct RGBColor
- {
- public byte Red { get; set; }
- public byte Green { get; set; }
- public byte Blue { get; set; }
- // 构造函数
- public RGBColor(byte r, byte g, byte b)
- {
- Red = r;
- Green = g;
- Blue = b;
- }
- }
- // 纸张 - 实体类
- public class Paper
- {
- public int ID { get; set; }
- public int WidthCM { get; set; }
- public int HeightCM { get; set; }
- public <strong>RGBColor</strong> Color { get; set; }
- }
复制代码 Paper 实体类的 Color 属性是 RGBColor 布局范例,而存入数据库时,我们只必要一个 uint 值即可,因此,它必要一个值转换器。- public class RGBValueConverter : ValueConverter<RGBColor, uint>
- {
- #region 封装方法
- private static RGBColor IntToColor(uint color)
- {
- byte red = Convert.ToByte((color >> 16) & 0xff);
- byte green = Convert.ToByte((color >> 8) & 0xff);
- byte blue = Convert.ToByte(color & 0xff);
- return <strong>new</strong><strong> RGBColor(red, green, blue)</strong>;
- }
- private static uint ColorToInt(ref RGBColor color)
- {
- return Convert.ToUInt32(<strong>(color.Red << 16) | (color.Green << 8) |</strong><strong> color.Blue</strong>);
- }
- #endregion
- // 构造函数
- public RGBValueConverter()
- : base(rgb => ColorToInt(ref rgb), cv => IntToColor(cv))
- { }
- }
复制代码 由于 uint 是 32 位整数,咱们用它的低 24 位就可以表现 RGB 值。在查询数据时,数据库提供步调先以 uint 范例读出值,然后转为 RGBColor 布局实例,再赋给 Paper.Color 属性;反过来,存入数据时,将 RGBColor 的三个属性合成一个 uint 值,再用此值写入数据库。
由于 Paper 实体类的 Color 属性用的 RGBColor 范例,以是,比力器应面向 RGBColor 布局。- public class RGBValueComparer : ValueComparer<RGBColor>
- {
- #region 封装的方法
- // 相等比较
- private static bool ColorEqual(RGBColor c1, RGBColor c2)
- {
- return c1.Red == c2.Red && c1.Green == c2.Green && c1.Blue == c2.Blue;
- }
- // 获取哈希值
- private static int ColorHash(RGBColor c)
- {
- HashCode hc = new();
- hc.Add(c.Red);
- hc.Add(c.Green);
- hc.Add(c.Blue);
- return hc.ToHashCode();
- }
- // 创建快照
- private static RGBColor CreateSnapshot(RGBColor c)
- {
- return new RGBColor(c.Red, c.Green, c.Blue);
- }
- #endregion
- // 构造函数
- public RGBValueComparer()
- :base((c1, c2) => ColorEqual(c1, c2), c => ColorHash(c), c => CreateSnapshot(c))
- { }
- }
复制代码 在用于创建快照的 CreateSnapshot 方法中,上述代码创了个 RGBColor 实例,并将原 RGBColor 对象的三个属性值作为参数传给构造函数。这就完成了深度拷贝。固然,由于 RGBColor 是值范例,着实它的实例本身就会把成员复制的。这里老周只演示功能,假如换成是类(引用),那就可以拷贝成员了,而不是仅通报引用。
在 ColorEqual 方法中,上述代码直接用比力两个 RGBColor 的实例的属性值。只有三个属性值都相称才行。
下面,咱们创建数据库上下文类,并做好设置。- public class MyContext : DbContext
- {
- protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
- {
- // 配置连接字符串
- SqlConnectionStringBuilder cnnsbd = new();
- cnnsbd.DataSource = "(localdb)\\MSSQLLocalDB";
- cnnsbd.InitialCatalog = "test_5";
- cnnsbd.IntegratedSecurity = true;
- optionsBuilder.UseSqlServer(cnnsbd.ConnectionString);
- }
- protected override void OnModelCreating(ModelBuilder modelBuilder)
- {
- modelBuilder.Entity<Paper>(ent =>
- {
- ent.ToTable("tb_papers");
- ent.Property(a => a.ID).HasColumnName("p_id");
- ent.Property(a => a.WidthCM).HasColumnName("wid_cm");
- ent.Property(a => a.HeightCM).HasColumnName("hei_cm");
- ent.Property(a => a.Color)
- .HasColumnName("p_color")
- // 应用转换器
- .<strong>HasConversion(new RGBValueConverter(), new</strong><strong> RGBValueComparer())</strong>;
- ent.HasKey(s => s.ID).HasName("PK_Papers");
- });
- }
- // 数据集合
- public DbSet<Paper> Papers { get; set; }
- }
复制代码 在设置 Color 属性时,通过 HasConversion 方法可以指定值转换器和值比力器。
创建数据库上下文实例,插入一条测试数据。- using(var ctx = new MyContext())
- {
- // 创建数据库
- bool r = ctx.Database.EnsureCreated();
- if(r)
- {
- // 插入测试数据
- ctx.Papers.Add(new() { HeightCM = 219, WidthCM = 100, Color = new RGBColor(0x2f, 0x05, 0x66) });
- ctx.SaveChanges();
- }
- }
复制代码 把刚存入数据库的记载查询出来,然后咱们更换 Color 属性的值,再扫描一遍实体状态。- using(var context = new MyContext())
- {
- Paper? p = context.Papers.FirstOrDefault();
- // 修改颜色
- if(p is not null)
- {
- RGBColor c = p.Color;
- c.Blue = 0x4e;
- p.Color = c;
- // 强制扫描更改
- context.<strong>ChangeTracker.DetectChanges</strong>();
- // 打印跟踪信息
- Console.WriteLine(context.ChangeTracker.DebugView.LongView);
- }
- }
复制代码 和上一个示例一样,由于老周没有调用 SaveChanges 方法,以是不会生存到数据库,也不会扫描实体是否更改。必要手动调用一次 DetectChanges 方法欺压举行一次扫描,末了打印实体跟踪信息。
输出效果如下:- Paper {ID: 1} <strong>Modified</strong>
- ID: 1 PK
- Color: 'QixenLie.RGBColor' Modified Originally 'QixenLie.RGBColor'
- HeightCM: 219
- WidthCM: 100
复制代码 注意看,实体被标记为 Modified,表明通过值比力器已发现 Color 属性被更改。
好了,本日的水文就水到这里了。
|