Entity Framework 6 Child Deletion and Foreign Keys - String Int GUID
2018-04-27 14:52
- The Problem
- The Solution - Short Version for the Impatient
- Foreign Keys That Are Manually Generated, And/Or a String, Int or GUID
- Lessons Learned
- References
The Problem
Here's a typical pattern for deleting a child entity we've all tried when using EF6.
public void DeleteItem(int orderId, int itemId)
{
var db = MyDbContext();
var order = db.Orders.Find(orderId);
order.Items.Remove(order.Items.Find(itemId));
db.SaveChanges();
}
Then we get one of these errors:
Cannot insert the value NULL into column 'OrderId', table 'MyDbContext.dbo.Items'; column does not allow nulls. UPDATE fails.
Or
The operation failed: The relationship could not be changed because one or more of the foreign-key properties is non-nullable. When a change is made to a relationship, the related foreign-key property is set to a null value. If the foreign-key does not support null values, a new relationship must be defined, the foreign-key property must be assigned another non-null value, or the unrelated object must be deleted.
This article explains why this happens and what to do about it. It also demonstrates configuring for five different key datatypes:
- String
- Manually-generated Int
- Identity Int
- Manually-generated GUID
- "Identity" GUID
It's nigh-on impossible to find any EF examples of foreign key relationships that don't use a integer identity column for the primary key. Also, I haven't seen any author demonstrate the approach I'm taking, even though it's perfectly legitimate from an Entity Framework perspective. Simply put:
We don't have to model the database exactly as it is.
The Solution - Short Version for the Impatient
Most SQL databases I've seen create a one-to-many relationship this way. All of these columns are not nullable.
Order
-----
OrderId PK
Item
----
ItemId PK
OrderId FK
This is technically called a non-identifying relationship. An item belongs to an order, but it's only required because OrderId is not null. OrderId isn't part of the item's primary key. The OrderId foreign key can change. The foreign key could be changed to nullable, and there could then be items that exist without an order.
Here's an identifying relationship. And item's uniqueness is bound to an order. The foreign key is also part of the primary key (it's a composite key). Thus, the foreign key can't change and the item must be part of an order.
Order
-----
OrderId PK
Item
----
ItemId PK
OrderId PK, FK
Entity Framework will automatically delete child entities that are part of an identifying relationship. If the relationshiop is non-identifying, EF will try to set the foreign key to null.
I'm sure there's a reason for this behavior, but I don't understand it. If EF knows the foreign key can't be null, why not go ahead and delete the child entity?
If your child table has a non-identifying relationship (which is common), you have two options:
- Change the table's foreign key to a composite key, as well as the entity's configuration in EF.
- Only change the entity's EF configuration.
I like the second one. Here's how this looks.
Again, the non-Identifying Item relationship in the table
Order
-----
OrderId PK
Item
----
ItemId PK
OrderId FK
Identifying Item relationship in Entify Framework
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
modelBuilder.Entity<Item>()
.HasKey(a => new { a.ItemId, a.OrderId });
}
With this in place, using Order.Items.Remove(item)
will result in Entity Framework deleting the item.
Foreign Keys That Are Manually Generated, And/Or a String, Int or GUID
Let's keep assuming a non-identifying relationship with non-nullable foreign keys. How does the EF configuration change if we're dealing with string or GUID primary keys? What if the keys aren't auto-generated?
I wrote a console application to explore this. It turns out each flavor needs a little tweak because of EF conventions.
Test Schema
Create a new database and run this script to create the tables.
--String Keys
create table Customers (
CustomerId varchar(10) not null primary key,
)
create table Addresses (
AddressId varchar(10) not null primary key,
CustomerId varchar(10) not null foreign key references Customers(CustomerId),
)
--Manually-generated Int keys
create table Orders (
OrderId int not null primary key,
)
create table Items (
ItemId int not null primary key,
OrderId int not null foreign key references Orders(OrderId),
)
--Identity Int keys
create table Projects (
ProjectId int not null identity primary key,
)
create table Tasks (
TaskId int not null identity primary key,
ProjectId int not null foreign key references Projects(ProjectId),
)
--Manually-generated GUID keys
create table Stores (
StoreId uniqueidentifier not null primary key,
)
create table Employees (
EmployeeId uniqueidentifier not null primary key,
StoreId uniqueidentifier not null foreign key references Stores(StoreId),
)
--"Identity" Default Sequential GUID keys
create table Gardens (
GardenId uniqueidentifier default newsequentialid() not null primary key,
)
create table Flowers (
FlowerId uniqueidentifier default newsequentialid() not null primary key,
GardenId uniqueidentifier not null foreign key references Gardens(GardenId),
)
Classes
Here are the classes that map to the above tables.
//String Keys
public class Customer
{
public string CustomerId { get; set; }
public virtual ICollection<Address> Addresses { get; set; } = new HashSet<Address>();
}
public class Address
{
public string AddressId { get; set; }
public string CustomerId { get; set; }
public Customer Customer { get; set; }
}
//Manually-generated Int keys
public class Order
{
public int OrderId { get; set; }
public virtual ICollection<Item> Items { get; set; } = new HashSet<Item>();
}
public class Item
{
public int ItemId { get; set; }
public int OrderId { get; set; }
public Order Order { get; set; }
}
//Identity Int keys
public class Project
{
public int ProjectId { get; set; }
public virtual ICollection<Task> Tasks { get; set; } = new HashSet<Task>();
}
public class Task
{
public int TaskId { get; set; }
public int ProjectId { get; set; }
public Project Project { get; set; }
}
//Manually-generated GUID keys
public class Store
{
public Guid StoreId { get; set; }
public ICollection<Employee> Employees { get; set; } = new HashSet<Employee>();
}
public class Employee
{
public Guid EmployeeId { get; set; }
public Guid StoreId { get; set; }
public Store Store { get; set; }
}
//Identity GUID keys
public class Garden
{
public Guid GardenId { get; set; }
public ICollection<Flower> Flowers { get; set; } = new HashSet<Flower>();
}
public class Flower
{
public Guid FlowerId { get; set; }
public Guid GardenId { get; set; }
public Garden Garden { get; set; }
}
Basic DbContext class
My DbContext is named EfTestDb. The basic setup lets us view SQL statements.
I've turned off lazy loading, because I'm finding that a typical enterprise practice. The configurations are the same if lazy loading is turned on.
public class EfTestDb : DbContext
{
public DbSet<Customer> Customers { get; set; }
public DbSet<Address> Addresses { get; set; }
public DbSet<Order> Orders { get; set; }
public DbSet<Item> Items { get; set; }
public DbSet<Project> Projects { get; set; }
public DbSet<Task> Tasks { get; set; }
public DbSet<Store> Stores { get; set; }
public DbSet<Employee> Employees { get; set; }
public DbSet<Garden> Gardens { get; set; }
public DbSet<Flower> Flowers { get; set; }
public EfTestDb()
{
Database.SetInitializer<EfTestDb>(null);
Configuration.LazyLoadingEnabled = false;
Configuration.ProxyCreationEnabled = false;
//Show SQL in Output Debug window.
this.Database.Log = s =>
{
System.Diagnostics.Debug.Write(s);
System.Diagnostics.Trace.Write(s);
Console.WriteLine(s);
};
}
}
String Key (Manually Generated)
It's still pretty common to have primary keys that are "intelligent" strings such as "blue-washer-20050225" that are generated by the application.
Here's the EF configuration.
I'm relying on EF conventions for key column naming, otherwise I'd need more configurations for .HasKey, .HasColumnName, etc.
// # String Keys
modelBuilder.Entity<Customer>()
.HasMany(a => a.Addresses);
modelBuilder.Entity<Address>()
.HasKey(a => new { a.AddressId, a.CustomerId })
.HasRequired(a => a.Customer);
This is the general pattern we'll see. The difference will be in how the keys and properties are defined.
Here's the testing code. I'm using DetachAllEntities to simulate saving a complex entity in one session, then deleting a child in another.
class Program
{
static void Main(string[] args)
{
StringKey();
ManualInt();
IdentityInt();
ManualGuid();
IdentityGuid();
Console.ReadLine();
}
static void DetachAllEntities(EfTestDb db)
{
foreach (var entry in db.ChangeTracker.Entries()) { entry.State = EntityState.Detached; }
}
static void StringKey()
{
using (var db = new EfTestDb())
{
//clean up
db.Addresses.RemoveRange(db.Addresses);
db.Customers.RemoveRange(db.Customers);
db.SaveChanges();
DetachAllEntities(db);
//add complex entity
var customer = new Customer()
{
CustomerId = "a",
Addresses = new HashSet<Address>()
{
new Address() { AddressId = "aa" },
new Address() { AddressId = "bb" }
}
};
db.Customers.Add(customer);
db.SaveChanges();
DetachAllEntities(db);
//delete child
customer = db.Customers.Include(a => a.Addresses).First();
var address = customer.Addresses.First();
customer.Addresses.Remove(address);
db.SaveChanges();
}
}
//... remaining test methods
}
Manually-Generated Int keys
// # Manually-generated Int keys
modelBuilder.Entity<Order>()
.HasMany(a => a.Items);
modelBuilder.Entity<Order>()
// I don't know why this is required, except that
// maybe EF assumes an int key is going to be an identity column
.Property(p => p.OrderId)
.HasDatabaseGeneratedOption(System.ComponentModel.DataAnnotations.Schema.DatabaseGeneratedOption.None);
modelBuilder.Entity<Item>()
.HasKey(a => new { a.ItemId, a.OrderId })
.HasRequired(a => a.Order);
static void ManualInt()
{
var db = new EfTestDb();
//clean up
db.Items.RemoveRange(db.Items);
db.Orders.RemoveRange(db.Orders);
db.SaveChanges();
DetachAllEntities(db);
//add complex entity
var order = new Order()
{
OrderId = 1,
Items = new List<Item>()
{
new Item() { ItemId = 11 },
new Item() { ItemId = 22 }
}
};
db.Orders.Add(order);
db.SaveChanges();
DetachAllEntities(db);
//delete child
order = db.Orders.Include(a => a.Items).First();
var Item = order.Items.First();
order.Items.Remove(Item);
db.SaveChanges();
}
Identity Int keys
modelBuilder.Entity<Task>()
.HasKey(a => new { a.TaskId, a.ProjectId });
modelBuilder.Entity<Task>()
.Property(p => p.TaskId)
// required because EF can't assume TaskId is an identity column since it's part of a composite key.
.HasDatabaseGeneratedOption(System.ComponentModel.DataAnnotations.Schema.DatabaseGeneratedOption.Identity);
static void IdentityInt()
{
var db = new EfTestDb();
//clean up
db.Tasks.RemoveRange(db.Tasks);
db.Projects.RemoveRange(db.Projects);
db.SaveChanges();
DetachAllEntities(db);
//add complex entity
var Project = new Project()
{
Tasks = new List<Task>()
{
new Task(),
new Task()
}
};
db.Projects.Add(Project);
db.SaveChanges();
DetachAllEntities(db);
//delete child
Project = db.Projects.Include(a => a.Tasks).First();
var Task = Project.Tasks.First();
Project.Tasks.Remove(Task);
db.SaveChanges();
}
Manually-Generated GUID keys
modelBuilder.Entity<Store>()
.HasMany(a => a.Employees);
modelBuilder.Entity<Employee>()
.HasKey(a => new { a.EmployeeId, a.StoreId })
.HasRequired(a => a.Store);
static void ManualGuid()
{
var db = new EfTestDb();
//clean up
db.Employees.RemoveRange(db.Employees);
db.Stores.RemoveRange(db.Stores);
db.SaveChanges();
DetachAllEntities(db);
//add complex entity
var Store = new Store()
{
StoreId = Guid.NewGuid(),
Employees = new List<Employee>()
{
new Employee() { EmployeeId = Guid.NewGuid() },
new Employee() { EmployeeId = Guid.NewGuid() }
}
};
db.Stores.Add(Store);
db.SaveChanges();
DetachAllEntities(db);
//delete child
Store = db.Stores.Include(a => a.Employees).First();
var Employee = Store.Employees.First();
Store.Employees.Remove(Employee);
db.SaveChanges();
}
"Identity" GUID (Default Sequential GUID keys)
modelBuilder.Entity<Garden>()
.Property(p => p.GardenId)
.HasDatabaseGeneratedOption(System.ComponentModel.DataAnnotations.Schema.DatabaseGeneratedOption.Identity);
modelBuilder.Entity<Garden>()
.HasMany(a => a.Flowers);
modelBuilder.Entity<Flower>()
.Property(p => p.FlowerId)
.HasDatabaseGeneratedOption(System.ComponentModel.DataAnnotations.Schema.DatabaseGeneratedOption.Identity);
modelBuilder.Entity<Flower>()
.HasKey(a => new { a.FlowerId, a.GardenId })
.HasRequired(a => a.Garden);
static void IdentityGuid()
{
var db = new EfTestDb();
//clean up
db.Flowers.RemoveRange(db.Flowers);
db.Gardens.RemoveRange(db.Gardens);
db.SaveChanges();
DetachAllEntities(db);
//add complex entity
var Garden = new Garden()
{
Flowers = new List<Flower>()
{
new Flower(),
new Flower()
}
};
db.Gardens.Add(Garden);
db.SaveChanges();
DetachAllEntities(db);
//delete child
Garden = db.Gardens.Include(a => a.Flowers).First();
var Flower = Garden.Flowers.First();
Garden.Flowers.Remove(Flower);
db.SaveChanges();
}
Lessons Learned
- Non-identifying, but required, parent-child relationships are common.
- The database doesn't have to be changed to allow Entity Framework to easily delete child records.
- The most common key types can be modeled.
References
- Entity Framework 6 GUID as primary key: Cannot insert the value NULL into column 'Id', table 'FileStore'; column does not allow nulls
- Auto generated SQL Server keys with the uniqueidentifier or IDENTITY
- What's the difference between identifying and non-identifying relationships?
- Is it possible to remove child from collection and resolve issues on SaveChanges?
- Delete item in database when removed from collection (EF 4.3)
- How to add a composite unique key using EF 6 Fluent Api?
- Property Mappings using Fluent API: