-
Notifications
You must be signed in to change notification settings - Fork 3.2k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Change default graph behavior of Add/Attach/etc #4424
Comments
Nice write up. :-) |
Issue #4424 This makes the behavior for Add/Attach/Update be to track all reachable entities, as in EF6. Single object operations can be done with the EntityEntry API. In the future we made add a context-wide flag to modify this behavior. Also, make Remove still be single object, but do attachment of reachable entities such that cascade deletes will be processed correctly on SaveChanges.
:( Thought I should clarify that ... :) .... sorry you weren't able to work it out. I liked that idea but also know that people are going to have a hard time with changed behavior no matter how many times you tell them "it's not EF6". I'm glad we still have ENTRY to add just the root. It was always a hard problem not being able to tear apart graphs for this purpose so that's still a big win. Agree re great detailed writeup. Thanks. Even Frans said "Tho, reading about the defaults, what they've picked is the best choice for POCOs I think." But he also adds "Root cause is lack of self tracking." (Don't ever bring that back! :) ) |
So just to clarify, we'll still be able to set an entity state to Added via TrackGraph and EF will only set the state of that single entity to Added? |
@tonysneed if you want to set the state of a single entity then |
Thanks @rowanmiller, so context.Entry(myEntity).State = EntityState.Added will not set child entity states to Added, as it did in EF6. Just wanted to confirm, thanks! |
@tonysneed correct, EF Core is now ok with having tracked entities reference entities that are not tracked. |
Excellent. In case anyone is interested, Here is a test which shows the difference between EF 6 and EF Core when it comes to setting entity state to Added on a disconnected graph. [Fact]
public void Set_state_should_mark_single_entity()
{
// Arrange
var detail = new OrderDetail();
var order = new Order
{
OrderDetails = new List<OrderDetail> { detail }
};
// Act: Set parent state to Added
_context.Entry(order).State = EntityState.Added;
// Assert for EF 6: Child state set to Added
Assert.Equal(EntityState.Added, _context.Entry(detail).State);
// Assert for EF Core: Child state remains Detached
Assert.Equal(EntityState.Detached, _context.Entry(detail).State);
} Here is the full source code for the tests. |
yeah I better go do a RC2 branch and update all of my disconnected data/graph tests in my ef7 repo (https://github.com/julielerman/EF7WhoAreYou/tree/master/Familiar%20and%20Enhancements%20Console%20Full%20NET%20After/Tests.RC) |
@julielerman and rename your repo to EFCore 😉 BTW our nightly builds are pretty unstable at the moment due to moving over to .NET CLI... so you may want to hold off a couple of weeks. |
So right now I am adding a Project entity (a new one) that has a list of Folder entities (also new). Each folder points to a FolderType (existing). For it to work, I have to attach all the existing folder types to the context before saving, or it tries to insert new folder types. What is the correct approach when part of the graph (root and some bits) are new, and part references existing objects? I was hoping for one method that would look at the IDs <= 0 to infer whether that object was new or existing.. |
It doesn't do that automatically because that would only work if you have setup database generated keys, which is only a subset of the scenarios EF supports. If you know you did that in your entities you should be able to use |
I'm a little confused now. Until today (I upgraded to the latest build), I just used Add and everything worked like a charm ;) Can someone give an example for the following scenario? How to realize this?
|
I know you asked Diego, but if I may jump in as this is a topic near & dear to my heart... First of all, the problem would go away if you would just use FKs .
Then you could set the relationships like:
And context.Add(a) would work perfectly! :) Because it's a new context and unaware of the existing objects, Add (update delete and attach) will affect the entire graph with whatever state you imply with the method. As of EFCore, you can use the DbContext.Entry(). State method e.g.
to apply that state only to the entity that you specified and it will ignore anything else that is attached. That is new behavior for Entry and gives us a great opportunity to have a consistent pattern. This is good in your example when you know for 100% sure that you only want to affect "a" and that you 100% surely kow that you don't want b & c to be updated. But that is not your scenario, so Entry doesn't 'work either because you want to skip B but you DO want to add C as well. If your method does not have that confidence then you need to choose a pattern to employ. There is no magic. If you know that in your system a pre-existing key value ("Id" in your case) indicates that an entity should not get dragged into the update, then you can iterate through and check for Id =0 vs Id has a value >0. (or whatever your rule is). The new TrackGraph method gives you a chance to do this. TrackGraph iterates through every item in the graph and performs the specified function on it. I have an example of using TrackGraph in my RC2 sample on Github. (https://github.com/julielerman/EFCore-RC2-Demo). But I'm not just using a "if id=0, added, if not unchanged" rule. I'm using trackgraph in combination with an old pattern that I wrote about yet again in a recent MSDN Mag article: https://msdn.microsoft.com/magazine/mt694083 (Rowan & I wrote about it in our DbContext book, too and that was back with EF 4.1 :) ) Look at the weathercontroller FixSeasonInReactions method. It uses trackgraph to update state based on a known state flag applied to each of my classes. Hope this helps and sorry if you wanted to get an answer straight from Diego because that would represent the guidance directly from the team. |
@aflx I am having a little trouble understanding everything Julie said, but given the code you posted I think you just need two steps:
Attach tells EF that the entity already exists in the database, and sets the state of the entity to Unchanged. Add tells EF that the entity is new and should be inserted into the database, and so sets the state to Added. Since Attach and Add both operate on the graph of reachable objects you may not need to call them for every single object in your graph. For example, in the code you wrote you can Attach b and then Add a, and since c is reachable from a it will also get added. If you want to automatically decide whether the entity should be Added or Attached based on key value, then you can use TrackGraph as Diego suggested. But this may be overkill. |
@julielerman Thank you! (I removed Diego 10 seconds after publishing the comment...so don't worry ;) I already tried it with just setting the Id...but no chance...
I tried the easy example with TrackGraph (checking for Id), but it seems that I'm doing something wrong, because the lambda function is just called once for the parent (A)
@ajcvickers Thx. But I don't like to use attach for b. I think, this only makes sense if you have something like a global context. Otherwise you always have to keep in mind storing and attaching existing objects. For a larger solution with complex objects and relationships this will be hard. And maybe I have a total wrong concept of MVVM in mind. |
There are certainly some interesting changes here! I found another one today... I was adding a new Project object and it has a Client property, that may be an existing Client or a new One. For new ones, we had set the Id to -1. Client and Project have IDENTITY columns. I believe this used to cause it to be Inserted, but omitting the ID. But NOW, it tries to insert the -1! This of cause fails. To further complicate matters, 0 is a valid ID values. So now I need the client to specify a negative ID to mean add, then I need to CHANGE that Id to 0 before saving it! |
oh no! If what I suggested was so convoluted that @ajcvickers didn't grok it, that makes me nervous. Maybe we should chat offline about this Arthur because I want to make sure I'm not inventing anti-patterns. @aflx Add works the same as it did in EF6 and earlier. I wouldn't combine TrackGraph and dbset.add. Use one of the other. The result of what you are doing is not much different than Arthur's suggestion. You are having the changetracker essentially call Attach on the object that has a value in id. Then you are calling Add on the dbset. The trackgraph is itereating through all of the objects. That's its job. Your code is telling it to ignore A & C. Make it say (this is not real syntax) e.entry.state= if (id>0) Unchanged else Added. Get rid of dbset.add. |
This is where "this is not EF6" comes in. People are going to see familiar methods and presume they will act the same as they always did. |
@julielerman Ok, with TrackGraph and without Add => works ;) Thank you for this hint.
|
Funny, I came up with the same code. |
You shouldn't need a base entity class. Try |
IsKeySet? Oh what a great property!! I need to start iterating through the APIs to see what gems I've missed!! Which reminds me there was a graphnode property who's purpose I have to figure out.. |
But don't you still need to examine the value of the key? How does IsKeySet help with that? |
Wouldnt it be cool if when specifying a key, you could give a lambda specifying what constitutes a 'new' value, for instances SetKey( e => e.Id, () => e.Id <=0 ). Then the fwk could go back to doing the heavy lifting in a generic way... and with some good defaults, you could even not need to do this for traditional patterns (Guid.Empty, Int<0 etc). |
@johnkwaters did you check the code base? It checks to make sure that the key is not at the default value. That's pretty good coverage. And it's virtual so perhaps you can override it ... though I haven't tried it out yet.
|
Awesome. No, I didnt check the codebase... next time I will! Right now I am working on a DB that was ported over from Access to SQL server, and sadly they use 0 as a valid key value for all kinds of tables, so having it be negative is the lambda I would need, or the override. |
@julielerman @divega @ajcvickers And I must say it is such a treat to be able to discuss and possibly influence the evolution of EF7 as it is crafted! I am using it in several production projects, due to ship in the August time frame (!!), so having this dialog definately makes me feel good about my bet on .net core. |
If zero is a valid key you can change the type of the key to be nullable, e.g.
Not sure what the problem could be. You don't need to make the navigation properties virtual, in fact virtual doesn't make any difference with EF Core. Could you please create a new issue with a self-contained repro of this issue? |
But then wouldn't I need a nullable pk in the db? Sent from my iPhone
|
No. The column will still be non-nullable as null is not a valid key value in a persisted object. It only means that the key can have a null value temporarily while it is in memory and before it is added to a |
@ajcvickers when Attaching a graph (no previously tracked entities) and child does NOT have it's key value filled, it becomes Added. When there is a a value in key, it becomes Unchanged. I did not expect this. Is Attach now reading key values to make state decisions? Did I miss that explanation somewhere? edit: yeah okay I see that re-reading the original post in this thread.... |
It's great to follow this discussion, especially given how long these challenges have been kicking around. I believe the TrackGraph pattern described by @julielerman may be superceded by the IsKeySet approach, i.e. simply use store-generated keys and then do one of the following: either @julielerman 's comment of June 21, and the description of Attach in @rowanmiller 's original posting, seem to say that in case (a) you could use Attach instead of Add for the new entities. The new entities' store-generated keys will not have been set and therefore the Attach will actually set the state to Added. In other words you can just construct the complete graph, including store-generated Id's in the case where the entity is a copy of an object already in the data store, and just Attach the whole thing. Or you can refer to the existing objects by Id instead of by reference, in which case you can either Add or Attach the graph. Have I gotten that right? It would be great to have an update to @julielerman 's April MSDN article that takes into account the June discussion above. |
@sjb-sjb did you see this https://msdn.microsoft.com/magazine/mt767693 ? |
Cool, thanks! Sent from my iPhone
|
In EF6.x the default behavior of these methods was to start tracking every reachable entity in the state being requested for the root entity. This caused a lot of issues because:
In EF Core we tried to alleviate some of these issues by initially making Add/Attach/etc. just work on a single object, but this caused a lot of confusion since it seems the natural expectation (based on the feedback we saw) is that you could add an Order and the OrderLines would be automatically added too.
We then tried an "Include Dependents" approach, where if you add a parent, we also include the children as added. This works well for some cases... but it gets pretty confusing. This is compounded by the fact that it's not always clear from your model which is the principal/dependent of a relationship.
After discussion, we realized that with some small variations on the rules from EF6.x, we already have APIs in EF Core to enable these scenarios:
We have the
TrackGraph
API for exactly this purpose.This is now supported in EF Core, our stack is capable of handling entities in the graph that it is not tracking. We have the
DbContext.Entry(...).State
API that allows you to put a single entity into the context without any graph behavior (i.e. no related entities are brought in).We will adjust the graph behavior of these APIs as follows:
Add
: Adds every reachable entity that is not already trackedAttach
: Attaches every reachable entity, except where a reachable entity has a store generated key and no key value is assigned, these will be marked as added.Update
: Same asAttach
, but entities are marked as modifiedRemove
: Same asAttach
, and then mark the root as deleted. Since cascade delete now happens onSaveChanges
, this allows cascade rules to flow to entities later on.Some important results of this behavior:
Add
, they all get added.TrackGraph
may also be a better API for these scenarios too.The text was updated successfully, but these errors were encountered: