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 來表達。

 

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