Skip to content

Commit

Permalink
Document entity splitting
Browse files Browse the repository at this point in the history
Fixes #3848
  • Loading branch information
AndriySvyryd authored Nov 8, 2022
1 parent afdf606 commit de1a618
Show file tree
Hide file tree
Showing 4 changed files with 140 additions and 12 deletions.
138 changes: 129 additions & 9 deletions entity-framework/core/modeling/table-splitting.md
Original file line number Diff line number Diff line change
@@ -1,15 +1,22 @@
---
title: Table Splitting - EF Core
description: How to configure table splitting using Entity Framework Core
title: Advanced table mapping - EF Core
description: How to configure table splitting and entity splitting using Entity Framework Core.
author: AndriySvyryd
ms.date: 10/10/2022
uid: core/modeling/table-splitting
---
# Table Splitting

# Advanced table mapping

EF Core offers a lot of flexibility when it comes to mapping entity types to tables in a database. This becomes even more useful when you need to use a database that wasn't created by EF.

The below techniques are described in terms of tables, but the same result can be achieved when mapping to views as well.

## Table splitting

EF Core allows to map two or more entities to a single row. This is called _table splitting_ or _table sharing_.

## Configuration
### Configuration

To use table splitting the entity types need to be mapped to the same table, have the primary keys mapped to the same columns and at least one relationship configured between the primary key of one entity type and another in the same table.

Expand All @@ -28,32 +35,145 @@ In addition to the required configuration we call `Property(o => o.Status).HasCo
> [!TIP]
> See the [full sample project](https://github.com/dotnet/EntityFramework.Docs/tree/main/samples/core/Modeling/TableSplitting) for more context.
## Usage
### Usage

Saving and querying entities using table splitting is done in the same way as other entities:

[!code-csharp[Usage](../../../samples/core/Modeling/TableSplitting/Program.cs?name=Usage)]

## Optional dependent entity
### Optional dependent entity

If all of the columns used by a dependent entity are `NULL` in the database, then no instance for it will be created when queried. This allows modeling an optional dependent entity, where the relationship property on the principal would be null. Note that this would also happen if all of the dependent's properties are optional and set to `null`, which might not be expected.

However, the additional check can impact query performance. In addition, if the dependent entity type has dependents of its own, then determining whether an instance should be created becomes non-trivial. To avoid these issues the dependent entity type can be marked as required, see [Required one-to-one dependents](xref:core/modeling/relationships#one-to-one) for more information.

## Concurrency tokens
### Concurrency tokens

If any of the entity types sharing a table has a concurrency token then it must be included in all other entity types as well. This is necessary in order to avoid a stale concurrency token value when only one of the entities mapped to the same table is updated.

To avoid exposing the concurrency token to the consuming code, it's possible the create one as a [shadow property](xref:core/modeling/shadow-properties):

[!code-csharp[TableSplittingConfiguration](../../../samples/core/Modeling/TableSplitting/TableSplittingContext.cs?name=ConcurrencyToken&highlight=2)]

## Inheritance
### Inheritance

It's recommended to read [the dedicated page on inheritance](xref:core/modeling/inheritance) before continuing with this section.

The dependent types using table splitting can have an inheritance hierarchy, but there are some limitations:

- The dependent entity type __cannot__ use TPC mapping as the derived types wouldn't be able to map to the same table.
- The dependent entity type __can__ use TPT mapping, but only the root entity type can use table splitting.
- If the principal entity type uses TPC, then only the entity types that don't have any descendants can use table splitting. Otherwise, the dependent columns would need to be duplicated on the tables corresponding to the derived types, complicating all interactions.
- If the principal entity type uses TPC, then only the entity types that don't have any descendants can use table splitting. Otherwise the dependent columns would need to be duplicated on the tables corresponding to the derived types, complicating all interactions.

## Entity splitting

EF Core allows to map an entity to rows in two or more tables. This is called _entity splitting_.

### Configuration

For example, consider a database with three tables that hold customer data:

- A `Customers` table for customer information
- A `PhoneNumbers` table for the customer's phone number
- An `Addresses` table for the customer's address

Here are definitions for these tables in SQL Server:

```sql
CREATE TABLE [Customers] (
[Id] int NOT NULL IDENTITY,
[Name] nvarchar(max) NOT NULL,
CONSTRAINT [PK_Customers] PRIMARY KEY ([Id])
);

CREATE TABLE [PhoneNumbers] (
[CustomerId] int NOT NULL,
[PhoneNumber] nvarchar(max) NULL,
CONSTRAINT [PK_PhoneNumbers] PRIMARY KEY ([CustomerId]),
CONSTRAINT [FK_PhoneNumbers_Customers_CustomerId] FOREIGN KEY ([CustomerId]) REFERENCES [Customers] ([Id]) ON DELETE CASCADE
);

CREATE TABLE [Addresses] (
[CustomerId] int NOT NULL,
[Street] nvarchar(max) NOT NULL,
[City] nvarchar(max) NOT NULL,
[PostCode] nvarchar(max) NULL,
[Country] nvarchar(max) NOT NULL,
CONSTRAINT [PK_Addresses] PRIMARY KEY ([CustomerId]),
CONSTRAINT [FK_Addresses_Customers_CustomerId] FOREIGN KEY ([CustomerId]) REFERENCES [Customers] ([Id]) ON DELETE CASCADE
);
```

Each of these tables would typically be mapped to their own entity type, with relationships between the types. However, if all three tables are always used together, then it can be more convenient to map them all to a single entity type. For example:

<!--
public class Customer
{
public Customer(string name, string street, string city, string? postCode, string country)
{
Name = name;
Street = street;
City = city;
PostCode = postCode;
Country = country;
}
public int Id { get; set; }
public string Name { get; set; }
public string? PhoneNumber { get; set; }
public string Street { get; set; }
public string City { get; set; }
public string? PostCode { get; set; }
public string Country { get; set; }
}
-->
[!code-csharp[CombinedCustomer](../../../samples/core/Miscellaneous/NewInEFCore7/ModelBuildingSample.cs?name=CombinedCustomer)]

This is achieved in EF7 by calling `SplitToTable` for each split in the entity type. For example, the following code splits the `Customer` entity type to the `Customers`, `PhoneNumbers`, and `Addresses` tables shown above:

<!--
modelBuilder.Entity<Customer>(
entityBuilder =>
{
entityBuilder
.ToTable("Customers")
.SplitToTable(
"PhoneNumbers",
tableBuilder =>
{
tableBuilder.Property(customer => customer.Id).HasColumnName("CustomerId");
tableBuilder.Property(customer => customer.PhoneNumber);
})
.SplitToTable(
"Addresses",
tableBuilder =>
{
tableBuilder.Property(customer => customer.Id).HasColumnName("CustomerId");
tableBuilder.Property(customer => customer.Street);
tableBuilder.Property(customer => customer.City);
tableBuilder.Property(customer => customer.PostCode);
tableBuilder.Property(customer => customer.Country);
});
});
-->
[!code-csharp[EntitySplitting](../../../samples/core/Miscellaneous/NewInEFCore7/ModelBuildingSample.cs?name=EntitySplitting)]

Notice also that, if necessary, different column names can be specified for each of the tables.

### Configuring the linking foreign key

The FK linking the mapped tables is targeting the same properties on which it is declared. Normally it wouldn't be created in the database, as it would be redundant. But there's an exception for when the entity type is mapped to more than one table. To change its facets you can use the normal [relationship Fluent API](xref:core/modeling/relationships#foreign-key):

<!--
modelBuilder.Entity<Customer>()
.HasOne<Customer>()
.WithOne()
.HasForeignKey<Customer>(a => a.Id)
.OnDelete(DeleteBehavior.Restrict);
-->
[!code-csharp[LinkingForeignKey](../../../samples/core/Miscellaneous/NewInEFCore7/ModelBuildingSample.cs?name=LinkingForeignKey)]

### Limitations

- Entity splitting can't be used for entity types in hierarchies.
- For any row in the main table there must be a row in each of the split tables (the fragments are not optional).
2 changes: 1 addition & 1 deletion entity-framework/core/what-is-new/ef-core-7.0/whatsnew.md
Original file line number Diff line number Diff line change
Expand Up @@ -3979,7 +3979,7 @@ This is achieved in EF7 by calling `SplitToTable` for each split in the entity t
});
});
-->
[!code-csharp[TableSplitting](../../../../samples/core/Miscellaneous/NewInEFCore7/ModelBuildingSample.cs?name=TableSplitting)]
[!code-csharp[EntitySplitting](../../../../samples/core/Miscellaneous/NewInEFCore7/ModelBuildingSample.cs?name=EntitySplitting)]

Notice also that, if necessary, different primary key column names can be specified for each of the tables.

Expand Down
2 changes: 1 addition & 1 deletion entity-framework/toc.yml
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,7 @@
href: core/modeling/data-seeding.md
- name: Entity type constructors
href: core/modeling/constructors.md
- name: Table splitting
- name: Advanced table mapping
href: core/modeling/table-splitting.md
- name: Owned entity types
href: core/modeling/owned-entities.md
Expand Down
10 changes: 9 additions & 1 deletion samples/core/Miscellaneous/NewInEFCore7/ModelBuildingSample.cs
Original file line number Diff line number Diff line change
Expand Up @@ -493,7 +493,7 @@ protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
#region TableSplitting
#region EntitySplitting
modelBuilder.Entity<Customer>(
entityBuilder =>
{
Expand All @@ -518,6 +518,14 @@ protected override void OnModelCreating(ModelBuilder modelBuilder)
});
});
#endregion

#region LinkingForeignKey
modelBuilder.Entity<Customer>()
.HasOne<Customer>()
.WithOne()
.HasForeignKey<Customer>(a => a.Id)
.OnDelete(DeleteBehavior.Restrict);
#endregion

#region OwnedTemporalTable
modelBuilder
Expand Down

0 comments on commit de1a618

Please sign in to comment.