EF Core – Owned Entity Types & Complex Types

前言

EF Core 8.0 推出了 Complex Types,这篇要来介绍一下。

由于它和 Owned Entity Types 傻傻分不清楚,加上我之前也没有写过 Owned Entity Types 的文章,所以这篇就一起介绍呗。

 

Owned Entity Types

Owned Entity Types 本质上任然属于一种 Entity Types,只是它有一些潜规则,所以变得和普通 Entity Type 有所区别。

Owned Entity Types 在 Domain-driven design (领域驱动设计) 里被视作为 Aggregate 的实现。很遗憾,我对 DDD 一窍不通,无法用 DDD 视角去解释它。

Compare with one-to-one relationship

我们拿 one-to-one relationship 来做对比,这样就可以看出 Owned Entity Types 的特色了。

首先,做两个 Entity -- Order 和 OrderCustomerInfo

public class Order
{
  public int Id { get; set; }
  public OrderCustomerInfo CustomerInfo { get; set; } = null!;
  public decimal Amount { get; set; }
}

public class OrderCustomerInfo
{
  public int Id { get; set; }
  public Order Order { get; set; } = null!;
  public string Name { get; set; } = "";
  public string Phone { get; set; } = "";
}

它们是一对一关系

public class ApplicationDbContext() : DbContext()
{
  public DbSet<Order> Orders => Set<Order>();
  public DbSet<OrderCustomerInfo> OrderCustomerInfos => Set<OrderCustomerInfo>();

  protected override void OnModelCreating(ModelBuilder modelBuilder)
  {
    modelBuilder.Entity<Order>(
      builder =>
      {
        builder.ToTable("Order");
        builder.Property(e => e.Amount).HasPrecision(19, 2);
        builder.HasOne(e => e.CustomerInfo).WithOne(e => e.Order).HasForeignKey<OrderCustomerInfo>(e => e.Id);
      });

    modelBuilder.Entity<OrderCustomerInfo>(
      builder =>
      {
        builder.ToTable("OrderCustomerInfo");
        builder.Property(e => e.Name).HasMaxLength(256);
        builder.Property(e => e.Phone).HasMaxLength(256);
      });
  }
}

接着 insert 和 query

    using var db = new ApplicationDbContext();

// insert order with customer info
db.Orders.Add(new()
{
  Amount = 100,
  CustomerInfo = new()
  {
    Name = "Derrick",
    Phone = "+60 16-773 7062",
  },
});
db.SaveChanges();

// query 
var order = db.Orders
  .Include(e => e.CustomerInfo)
  .First();

Console.WriteLine(order.CustomerInfo.Name); // "Derrick"

Owned Entity Types 的特性

对比 one-to-one,Owned Entity Types 有几个特性:

  1. Owned Entity Types 没有 DbSet

    OrderCustomerInfo 不是独立的 Entity,它属于 part of the Order Entity,所以它没有 DbSet。

    这也导致了它不能直接被创建,下面这句是不成立的

    db.OrderCustomerInfos.Add() // 'ApplicationDbContext' does not contain a definition for 'OrderCustomerInfos'

    OrderCustomerInfo 需要依附在 Order 上才能一起被创建,像这样

    db.Orders.Add(new()
    {
      Amount = 100,
      CustomerInfo = new()
      {
        Name = "Derrick",
        Phone = "+60 16-773 7062",
      },
    });
  2. 自动 Include

    没有 DbSet 自然也无法直接 query

    db.OrderCustomerInfos.ToList() // 'ApplicationDbContext' does not contain a definition for 'OrderCustomerInfos'

    要获取 OrderCustomerInfo 只能透过 Order。另外 Owned Entity Types 有一个特色 -- 它会自动被 Include 出来。

    即便我们没有写 Include,Owned Entity Types 依然会被 eager loading 出来。

  3. same Table

    默认情况下,Owned Entity Types (OrderCustomerInfo) 会和它依附的 Entity Types (Order) 存放在同一个数据库 Table,还有 column name 会加上 prefix,这就像使用了 Table Splitting 的那样。

  4. Id become shadow property

    Owned Entity Types 任然是 Entity Types,它依然有 primary key 的概念,只是它改成了 Shadow Property,在 class 会看不见 Id。

Config Owned Entity Types

替换成这样

modelBuilder.Entity<Order>(
  builder =>
  {
    builder.ToTable("Order");
    builder.Property(e => e.Amount).HasPrecision(19, 2);
    builder.OwnsOne(e => e.CustomerInfo, builder =>
    {
      builder.Property(e => e.Name).HasMaxLength(256);
      builder.Property(e => e.Phone).HasMaxLength(256);
    });
  });

效果

// query 
var order = db.Orders.First();

Console.WriteLine(order.CustomerInfo.Name); // "Derrick"

不需要 Include,它会自动 Include。

数据库 Order Table

OrderCustomerInfo 的属性被映射到 Order Table,而且 column name 加了 prefix "CustomerInfo" 这名字来自 Order.CustomerInfo 属性。

我们调 Entity Model 出来证实一下,Owned Entity Types 也是一种 Entity Types 而且它其实是有 Key 的。

using var db = new ApplicationDbContext();
var customerEntityType = db.Model.FindEntityType(typeof(OrderCustomerInfo))!; // Owned Entity Types 也是一种 Entity Types
var isOwned = customerEntityType.IsOwned();               // true,OrderCustomerInfo 是 Owned Entity Types
var property = customerEntityType.GetProperty("OrderId")!;
Console.WriteLine(property.IsShadowProperty());   // true,"OrderId" 是 Shadow Property
Console.WriteLine(property.IsKey());              // true,"OrderId" 是 Primary Key
Console.WriteLine(property.IsForeignKey());       // true,"OrderId" 是 Foreign Key

var orderEntityType = db.Model.FindEntityType(typeof(Order))!;
Console.WriteLine(orderEntityType.FindNavigation("CustomerInfo")!.ForeignKey); // "OrderId", CustomerInfo 属性不是普通的 Property 而是 Navigation

Two Table

默认情况下 Owned Entity Types (OrderCustomerInfo) 会和它的 Owner (Order) 共用一个 Table。

如果我们不希望这样,则可以使用 Table Splitting 将它们分开成两个 Table。

column name prefix 会自动被拿掉。

效果

Sharing Owned Entity Types

Owned Entity Types 最好不要共用,不顺风水。

public class Address
{
  public string Street1 { get; set; } = "";
  public string Street2 { get; set; } = "";
  public string PostalCode { get; set; } = "";
  public string Country { get; set; } = "";
}

public class Order
{
  public int Id { get; set; }
  public Address BillingAddress { get; set; } = null!;
  public Address ShippingAddress { get; set; } = null!;
  public decimal Amount { get; set; }
}

public class Customer
{
  public int Id { get; set; }
  public string Name { get; set; } = "";
  public Address Address { get; set; } = null!;
}

Order 和 Customer 都使用了 Address。

试想,如果是一对一关系,上面这样做成立吗?

答案是不成立,因为一对一关系是靠 id 作为 foreign key 维持关系的,上面这样就乱了套了。

我们测试看强制设置 Owned Entity Types 出来的效果是怎样的。

public class ApplicationDbContext() : DbContext()
{
  public DbSet<Order> Orders => Set<Order>();
  public DbSet<Customer> Customers => Set<Customer>();

  protected override void OnModelCreating(ModelBuilder modelBuilder)
  {
    static void BuildAction<TEntity>(OwnedNavigationBuilder<TEntity, Address> builder) where TEntity : class
    {
      builder.Property(e => e.Street1).HasMaxLength(256);
      builder.Property(e => e.Street2).HasMaxLength(256);
      builder.Property(e => e.PostalCode).HasMaxLength(256);
      builder.Property(e => e.Country).HasMaxLength(256);
    }

    modelBuilder.Entity<Order>(
      builder =>
      {
        builder.ToTable("Order");
        builder.Property(e => e.Amount).HasPrecision(19, 2);
        builder.OwnsOne(e => e.BillingAddress, BuildAction);
        builder.OwnsOne(e => e.ShippingAddress, BuildAction);
      });

    modelBuilder.Entity<Customer>(
      builder =>
      {
        builder.ToTable("Customer");
        builder.Property(e => e.Name).HasMaxLength(256);
        builder.OwnsOne(e => e.Address, BuildAction);
      });
  }
}

BuildAction 是共用的。

效果

如果做 Table Splitting 的话,就会多出 3 个 Table -- CustomerAddress,OrderBillingAddress,OrderShippingAddress。

从这个结构可以看出,它底层依然是一对一的概念,只是在上层搞了许多映射。

我们进资料看看

using var db = new ApplicationDbContext();
var address = new Address
{
  Street1 = "test",
  Street2 = "test",
  PostalCode = "81300",
  Country = "Malaysia"
};
db.Orders.Add(new()
{
  Amount = 100,
  ShippingAddress = address,
  BillingAddress = address
});
db.SaveChanges();

注意,shippingAddress 和 billingAddress 使用了同一个 address 对象。

运行结果是报错

因为官网声明了,Owned Entity Types 实例是不可以共享的,比如一个对一个。

我觉得 EF Core 如果硬硬要做映射是可以做到的,只是他们认为这种使用方式已经脱离了 Owned Entity Types 的本意,所以才不支持它。

但是,EF Core 8.0 推出的 Complex Types 支持这种使用方式,Complex Types 和 Owned Entity Types 有几分相似下一 part 会详细讲。

Collections of owned types

Owned Entity Types 不仅仅可以映射一对一关系,一对多关系也可以映射。

但是一对多就不可能共用同一个数据库 Table 了,一定是 2 个 Table。

其它特性,比如没有 DbSet,自动 Include,不能 share instance 这些则都一样。

来个简单的示范

Entity

public class Address
{
  public string Street1 { get; set; } = "";
  public string Street2 { get; set; } = "";
  public string PostalCode { get; set; } = "";
  public string Country { get; set; } = "";
}

public class Customer
{
  public int Id { get; set; }
  public string Name { get; set; } = "";
  public List<Address> Addresses { get; set; } = [];
}

ModelBuilder

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
  modelBuilder.Entity<Customer>(
    builder =>
    {
      builder.ToTable("Customer");
      builder.Property(e => e.Name).HasMaxLength(256);
      builder.OwnsMany(e => e.Addresses, builder =>
      {
        builder.Property(e => e.Street1).HasMaxLength(256);
        builder.Property(e => e.Street2).HasMaxLength(256);
        builder.Property(e => e.PostalCode).HasMaxLength(256);
        builder.Property(e => e.Country).HasMaxLength(256);
      });
    });
}

数据库

和普通一对多的表结构是一样的。

using var db = new ApplicationDbContext();
var addressEntityType = db.Model.FindEntityType(typeof(Address))!;
addressEntityType.GetProperty("Id").IsKey();                    // true
addressEntityType.GetProperty("Id").IsShadowProperty();         // true
addressEntityType.GetProperty("CustomerId").IsForeignKey();     // true
addressEntityType.GetProperty("CustomerId").IsShadowProperty(); // true

CustomerId 是 Foreign Key,Id 是 Primary Key,它们都是 Shadow Property。

如果想修改 Primary Key 和 Foreign Key 可以这样配置

builder.OwnsMany(e => e.Addresses, builder =>
{
  builder.WithOwner().HasForeignKey("CustomerId2");       // rename foreign key CustomerId to CustomerId2
  builder.Property<int>("Id").HasColumnName("AddressId"); // rename primary key Id to AddressId
});

To JSON

Owned Entity Types 还有一个强项,它可以以 JSON 格式保存到数据库里,不管是 OwnsOne 还是 OwnsMany 都行。

builder.OwnsMany(e => e.Addresses, builder =>
{
  builder.ToJson();
  builder.Property(e => e.Street1).HasMaxLength(256);
  builder.Property(e => e.Street2).HasMaxLength(256);
  builder.Property(e => e.PostalCode).HasMaxLength(256);
  builder.Property(e => e.Country).HasMaxLength(256);
});

加一句 ToJson() 就可以了。

测试

using var db = new ApplicationDbContext();
db.Customers.Add(new()
{
  Name = "Derrick",
  Addresses = [
    new() { Street1 = "test1", Street2 = "test1", PostalCode = "81300", Country = "Malaysia" },
    new() { Street1 = "test2", Street2 = "test2", PostalCode = "123456", Country = "Singapore" }
  ]
});
db.SaveChanges();

效果

厉害吧😎 

Nested Owned Entity Types

Owned Entity Types 支持嵌套,配置的方式和上面一样,在 OwnsOne / Many 里面继续调用 OwnsOne / Many 就可以了。

这里我就不演示了。

另外,ToJson 的话,一定要在最上层调用。

目前不支持一半一半的状况,要 JSON 就得从上层到下层通通 JSON。

Limitation

目前不支持 inheritance hierarchies 继承结构,TPH、TPT、TPC 通通不支持,希望未来会支持。

 

Complex Types

参考:Docs – Value objects using Complex Types

EF Core 8.0 推出了 Complex Types。Complex Types 和 Owned Entity Types 外观有点像,但内在思想是不同的。

Complex Types 在 Domain-driven design (领域驱动设计) 里被视作为 Value Object 的实现。很遗憾,我对 DDD 一窍不通,无法用 DDD 视角去解释它。

Current limitations

目前 Complex Types 还不算完整,它有很多该有得功能都还没有实现,要使用它要小心哦。

  1. 不支持一对多关系。

  2. 不支持 nullable

    public OrderCustomerInfo? CustomerInfo { get; set; }

    像上面这样 OrderCustomerInfo 将无法设置成 Complex Types,因为它是 nullable。

  3. 不支持 ToJson。

这几个都挺需要的,既然都没有😅

和 Owned Entity Types 的共同点和区别

共同点:

  1. 没有 DbSet,不可以直接 Add 和 Query

  2. 自动 Include

区别:

  1. Complex Types 不支持一对一映射到 two table。

  2. Complex Types 不继承 Entity Types,因为它完全没有 Primary Key 概念。

  3. Complex Types 的实例是可以共用的。

  4. Complex Types 更倾向使用 record 而非 class。

Config Complex Types

和 config Owned Entity Types 大同小异。

Entity

public class Order
{
  public int Id { get; set; }
  public OrderCustomerInfo CustomerInfo { get; set; } = null!;
  public decimal Amount { get; set; }
}

public class OrderCustomerInfo
{
  public string Name { get; set; } = "";
  public string Phone { get; set; } = "";
}

ModelBuilder

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
  modelBuilder.Entity<Order>(
    builder =>
    {
      builder.ToTable("Order");
      builder.Property(e => e.Amount).HasPrecision(19, 2);
      builder.ComplexProperty(e => e.CustomerInfo, builder =>
      {
        builder.Property(e => e.Name).HasMaxLength(256);
        builder.Property(e => e.Phone).HasMaxLength(256);
      });
    });
}

效果

Entity Model

using var db = new ApplicationDbContext();
var customerInfoEntityType = db.Model.FindEntityType(typeof(OrderCustomerInfo)); // null
var orderEntityType = db.Model.FindEntityType(typeof(Order))!;
var customerInfoProperty = orderEntityType.FindProperty("CustomerInfo");         // null
var customerInfoComplexProperty = orderEntityType.FindComplexProperty("CustomerInfo");

OrderCustomerInfo 不是 Entity Types,所以 FindEntityType 是找不到的,这点和 Owned Entity Types 不同。

另外,CustomerInfo 也不是普通的 Property 也不是 Navigation,而是 ComplexProperty,要使用 FindComplexProperty 才能获取到,这点也和 Owned Entity Types 不同。

DbContext.Entry

Entity Model 不一样,那访问 Entry CurrentValue,IsModified 这些自然也不一样了。

Entity 

public class Order
{
  public int Id { get; set; }
  public OrderCustomerInfo CustomerInfo { get; set; } = null!;
  public Address ShippingAddress { get; set; } = null!;
  public decimal Amount { get; set; }
}

假设 OrderCustomerInfo 是 Complex Types,ShippingAddress 是 Owned Entity Types

想访问 ShippingAddress  的 CurrentValue 通过 DbContext.Entry 就可以了。

var order = db.Orders.First();
var shippingAddressEntry = db.Entry(order.ShippingAddress);
var countryValue = shippingAddressEntry.Property(e => e.Country).CurrentValue;

想访问 CustomerInfo 的 CurrentValue 通过 DbContext.Entry 会报错。

var customerInfoEntry = db.Entry(order.CustomerInfo); // error

因为 Complex Types 不是 Entity Types,它没有 Key 的概念。

正确的方式是先进入 OrderEntry 然后使用 ComplexProperty 找到 CustomerInfo 在进入它的 Property 获取 CurrentValue

var orderEntry = db.Entry(order);
var nameValue = orderEntry.ComplexProperty(e => e.CustomerInfo).Property(e => e.Name).CurrentValue

Immutable

Value Object 虽然是对象,但它通常是值类型 (Immutable)。

所以一般都是使用 struct / record 而不是 class。(虽然 EF Core 支持使用 class 而且即使不是 Immutable 它也可以 tracking 到 changes)

Entity

public class Order
{
  public int Id { get; set; }
  public OrderCustomerInfo CustomerInfo { get; set; } = null!;
  public decimal Amount { get; set; }
}

public record OrderCustomerInfo(string Name, string Phone);

Add Order

using var db = new ApplicationDbContext();
db.Orders.Add(new()
{
  Amount = 100,
  CustomerInfo = new("Derrick", "+60 16-773 7062")
});
db.SaveChanges();

update CustomerInfo

var order = db.Orders.First();
order.CustomerInfo = order.CustomerInfo with { Name = "New Name" };
db.SaveChanges();

个人觉得使用 record 会比使用 class 更合适。不熟悉 record 的朋友可以看这篇 C# – Record, Class, Struct

 

总结

我们可以把 Owned Entity Types 视为变种的 one-to-one 和 one-to-many。

它最大的好处是自动 Include,自动 Table Splitting,而且可以 ToJson,底层思想继承自 one-to-one 和 one-to-many Entity Types 也算很好理解。

目前只有一个缺陷 -- 不支持继承结构。

Complex Types 不是 one-to-one 和 one-to-many 的概念,它没有 Key,它不是 Entity Types。

它适合用在 Object Value,就是说这些属性确确实实是 Property 只是它们有关系所以被 group 在一起。

比如说,地址本来是一个 string "11, Jalan Merak 22, Taman Mutiara Rini, 81300 Johor Malaysia",我想把它做的有结构,

于是拆分成 Street1, Street2, PostalCode, State, Country,这种情况就适合使用 Complex Types 来表达。

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章