dotnet / efcore

EF Core is a modern object-database mapper for .NET. It supports LINQ queries, change tracking, updates, and schema migrations.
https://docs.microsoft.com/ef/
MIT License
13.73k stars 3.18k forks source link

Allow passing entity type instances to ExecuteUpdate #32719

Open roji opened 9 months ago

roji commented 9 months ago

This is about allowing an entity type to be used on the left-hand side (LHS) of an ExecuteUpdate setter, replacing the entire contents of the entity in the database with the right-hand side (RHS). #32058 did this for complex types.

Note that this could mean different things. Consider the following:

// 1. Assign reference nagivation from entity scalar subquery
_ = ctx.Blogs.ExecuteUpdate(setters => setters.SetProperty(b => b.BlogDetails, b => ctx.BlogDetails.Single(...)));

// 2. Assign reference nagivation from entity parameter
var blog = new Blog() { ... };
_ = ctx.Blogs.ExecuteUpdate(setters => setters.SetProperty(b => b.BlogDetails, blog));

// 3. Assign reference nagivation from inline entity
var blog = new Blog() { ... };
_ = ctx.Blogs.ExecuteUpdate(setters => setters.SetProperty(b => b.BlogDetails, new Blog { ... }));

For the 1st case, this would make all Blogs point to a single BlogDetails row in the database, based on the result of the subquery. This is reference assignment semantics.

For the 2nd and 3rd case, it's not immediately clear what should happen. We could overwrite each Blog's BlogDetail columns (value assignment semantics); but if BlogDetails is optional and some Blog doesn't have one, we'd need to insert a new one - but SQL UPDATE doesn't allow this (so ExecuteUpdate would need to do INSERTs as well). An alternative approach would be to conserve reference assignment semantics (these are entity types after all), and only extract the key from the parameterized blog, assigning the foreign key property only (or again, overwrite the BlogDetails properties as well, which requires updating two tables, assuming Blogs has the foreign key).

For top-level entities things are slightly different:

// 4. Assign top-level entity from parameterized entity instance
_ = ctx.Blogs.ExecuteUpdate(setters => setters.SetProperty(b => b, blog));

// 5. Contrived: assign top-level entity from entity scalar subquery
_ = ctx.Blogs.ExecuteUpdate(setters => setters.SetProperty(b => b, b => ctx.BlogDetails.Single(...));

Here, there's no possibility of reference assignment (there's no reference) - (4) can only overwrite the columns in existing rows. (5) seems contrived but would presumably do the same.

So it seems like the behavior could depend both on the RHS (parameter or database entity instance) and on the LHS (top-level entity or navigation).

Finally, there's also the question of owned entity types; the only thing that makes sense here is to overwrite (value assignment), since the same owned entity type can't be owned by multiple owners. Given that we have complex types and owned types may be deprioritized going forward, we probably shouldn't do this if it's too much work.

bachratyg commented 8 months ago

+1 for owned entity types. Today if I try to remove an inline owned entity:

ctx.Customers.ExecuteUpdate(setters => setters.SetProperty(b => b.ShippingAddress, (Address?)null));
// The following lambda argument to 'SetProperty' does not represent a valid property to be set

Also attempted this the quirky way. Same result, different error message:

ctx.Customers.Select(b => b.ShippingAddress).ExecuteDelete();
// The operation 'ExecuteDelete' is being applied on the table 'Customers'
// which contains data for multiple entity types.
// Applying this delete operation will also delete data for other entity type(s), hence it is not supported.

What works is explicitly setting every single property to null with all the bangs.

This is not (yet) applicable to complex types due to #31376, but even if it were (and I would definitely switch to complex in many cases) there are still some where owned is better e.g. it's handy to do db.Entry(owned).State = EntityState.Deleted without knowing the parent.

roji commented 8 months ago

@bachratyg we generally plan to invest more in complex types as a way of modeling contained data (JSON columns, table splitting), and less in owned entities. Supporting optional complex types is definitely high on our list and we're likely to work on that for 9.0; at that point I agree that adding ExecuteDelete support for it would make a lot of sense.