This question already has answers here:
EF: Create/Remove relation from Many-to-Many relations when `AutoDetectChangesEnabled` and `ProxyCreationEnabled` are disabled on DbContext
(3 answers)
Closed 3 years ago.
I have a many-to-many relation in Entity Framework, in the mapping, EF allow us to specify the keys and table name. In my example below, a user can have many device logs and a device log can belong to many users.
HasMany(e => e.DeviceLogs)
.WithMany(e => e.Users)
.Map(m => m.ToTable("UserDeviceLog")
.MapLeftKey("UserId")
.MapRightKey("LogId")
);
Now, when i try to remove a device log from a user:
using (var context = new MyDbContext())
{
var user = context.Users.Find(userId);
var deviceLog = context.DeviceLogs.Find(logId);
user.DeviceLogs.Remove(deviceLog);
context.SaveChanges();
}
I know that I'm going to delete a relation from table "UserDeviceLog", not the device log itself.
This above code snippet works, but the problem is Entity Framework will load all device logs and then remove one record from table UserDeviceLog.
My application is blocked while execute the above logic. From the screenshot you can see, there is about 23 seconds between the two SQL statements. For the specified user, it has about 20K records(the first SQL statement). I'm guessing it was taken by transfering these 20K records from SQL Server to my application which cause my application very slow.
While this is unnecessary, how can i avoid it? I don't want to write a plain SQL delete statement, I prefer a solution in EF way.
Thank you!
This will occur because by accessing user.DeviceLogs you will lazy-load that collection.
If UserDeviceLogs is not accessible in the DbContext as a DbSet (which it typically would not as a many-to-many joining table/entity)...
What should get you there would be:
using (var context = new MyDbContext())
{
var userDeviceLog = context.Users.Where(x => x.UserId == userId)
.SelectMany(x => x.DeviceLogs.Where(l => l.LogId ==logId))
.SingleOrDefault();
if(userDeviceLog == null)
return;
context.Entry(userDeviceLog).State = EntityState.Deleted;
context.SaveChanges();
}
If a DeviceLog refers to any reference that also must be deleted you will need to handle that deletion as well. But this should unlink a user from a device log without loading all links.
Related
Got a strange one here. I am using EF 6 over SQL Server 2012 and C#.
If I delete a record, using DeleteObject, I get:
//order.orderitem count = 11
db.OrderItem.DeleteObject(orderitem);
db.SaveChanges();
var order = db.order.First(r => r.Id == order.id);
//Order.OrderItem count = 10, CORRECT
If I delete an Order Item, using ExecuteStoreCmd inline DML, I get:
//order.orderitem count = 11
db.ExecuteStoreCommand("DELETE FROM ORDERITEM WHERE ID ={0}", orderitem.Id);
var order = db.Order.First(r => r.Id == order.id);
//order.orderitem count = 11, INCORRECT, should be 10
So the ExecuteStoreCommand version reports 11, however the OrderItem is definitely deleted from the DB, so it should report 10. Also I would have thought First() does an Eager search thus repopulating the "order.orderitem" collection.
Any ideas why this is happening? Thanks.
EDIT: I am using ObjectContext
EDIT2: This is the closest working solution I have using "detach". Interestingly the "detach" actually takes about 2 secs ! Not sure what it is doing, but it works.
db.ExecuteStoreCommand("DELETE FROM ORDERITEM WHERE ID ={0}", orderitem.Id);
db.detach(orderitem);
It would be quicker to requery and repopulate the dataset. How can I force a requery? I thought the following would do it:
var order = db.order.First(r => r.Id == order.id);
EDIT3: This seems to work to force a refresh post delete, but still take about 2 secs:
db.Refresh(RefreshMode.StoreWins,Order.OrderItem);
I am still not really understanding why one cannot just requery as a Order.First(r=>r.id==id) type query oftens take much less than 2 secs.
This would likely be because the Order and it's order items are already known to the context when you perform the ExecuteStoredCommand. EF doesn't know that the command relates to any cached copy of Order, so the command will be sent to the database, but not update any loaded entity state. WHere-as the first one would look for any loaded OrderItem, and when told to remove it from the DbSet, it would look for any loaded entities that reference that order item.
If you don't want to ensure the entity(ies) are loaded prior to deleting, then you will need to check if any are loaded and refresh or detach their associated references.
If orderitem represents an entity should just be able to use:
db.OrderItems.Remove(orderitem);
If the order is loaded, the order item should be removed automatically. If the order isn't loaded, no loss, it will be loaded from the database when requested later on and load the set of order items from the DB.
However, if you want to use the SQL execute approach, detaching any local instance should remove it from the local cache.
db.ExecuteStoreCommand("DELETE FROM ORDERITEM WHERE ID ={0}", orderitem.Id);
var existingOrderItem = db.OrderItems.Local.SingleOrDefault(x => x.Id == orderItem.Id);
if(existingOrderItem != null)
db.Entity(existingOrderItem).State = EntityState.Detached;
I don't believe you will need to check for the orderItem's Order to refresh anything beyond this, but I'm not 100% sure on that. Generally though when it comes to modifying data state I opt to load the applicable top-level entity and remove it's child.
So if I had a command to remove an order item from an order:
public void RemoveOrderItem(int orderId, int orderItemId)
{
using (var context = new MyDbContext())
{
// TODO: Validate that the current user session has access to this order ID
var order = context.Orders.Include(x => x.OrderItems).Single(x => x.OrderId == orderId);
var orderItem = order.OrderItems.SingleOrDefault(x => x.OrderItemId == orderItemId);
if (orderItem != null)
order.OrderItems.Remove(orderItem);
context.SaveChanges();
}
}
The key points to this approach.
While it does mean loading the data state again for the operation, this load is by ID so it's fast.
We can/should validate that the data requested is applicable for the user. Any command for an order they should not access should be logged and the session ended.
We know we will be dealing with the current data state, not basing decisions on values/data from the point in time that data was first read.
Folks,
I am mapping EF objects to simple Pocos using automapper in my service layer. I have certain entities that have many relationships in EF,but I want to restrict how much of this gets pulled back from the db. One of these entities would map to a database table but would have many relationships to other tables that would appear as entity collections in the generated model (EDMX).
So I have created a "shallow" poco for the entity which only has poco properties for the first level of properties in the entity, i.e. some integer Ids instead of associated collections/entity collections. I map in the following manner....
var simplepocoforentity= Mapper.Map(_readOnlyDb.Single<EfEntity>(x => x.Id== Id),
new SimplPocoForEntity());
My question is.. because I am only mapping the entity to a simple poco here, can I be confident that EF will not try and query other non referenced data from the underlying db tables relationships when I do the mapping?
I know that I can investigate this via SQL Profiling, but I would appreciate any input befor going down that route.
Thanks
K.
It depends on the internal implementation of AutoMapper. I would assume that AutoMapper does not try to access the navigation properties of the entity (in which case lazy loading and an additional database query would kick in). I don't see a reason why AutoMapper should do this if you only map scalar properties. But who knows...
If you want to be on the safe side you could disable lazy loading temporarily:
try
{
_readOnlyDb.ContextOptions.LazyLoadingEnabled = false;
var simplepocoforentity = Mapper.Map(_readOnlyDb.Entities
.Single(x => x.Id == Id), new SimplPocoForEntity());
// ...
}
finally
{
_readOnlyDb.ContextOptions.LazyLoadingEnabled = true;
}
As a side note: Be aware that you load the full entity into the context with this approach. After that the mapping happens. Potentially you load more columns from the database than you really need (perhaps not in your special case here). In general I would do the mapping with a projection which ensures that the database only queries the columns which are needed:
var simplepocoforentity = _readOnlyDb.Entities
.Where(e => e.Id == Id)
.Select(e => new SimplPocoForEntity
{
PocoProperty1 = e.EntityProperty1,
PocoProperty2 = e.EntityProperty2,
PocoProperty3 = e.EntityProperty3
// etc.
})
.Single();
With this approach no lazy loading would happen at all because you don't load the entity but directly the PocoForEntity from the database.
I am trying to delete an object from a many to many relationship but it refused to get worked.
The structure is as follows:
I have a Products table which every product has excProducts
I am trying to delete an excProduct with the following code
var objectContx = new objectContx();
var prod = objectContx.Products.Where(p => p.ProductID == prodID).FirstOrDefault();
var excProd = objectContx.ExcProducts.Where(exc => exc.ExcProductID == excProdid).FirstOrDefault();
prod.ExcProducts.Attach(excProd);
prod.ExcProducts.Remove(excProd);
objectContx.SaveChanges();
The excProd deleted from database but when I am getting the Product from objectContext it has the deleted excProd in it's list.
How could I delete it from the objectContext as well?
If your many to many relationship has an entity (e.g. if it has payload) then the instance of that entity must be deleted. This can be done manually or using cascaded delete (see option in property window while standing on relationship in EDMX designer).
I've been looking at Applying filters when explicitly loading related entities and could not get it to work for a many-to-many relationship.
I created a simple model:
Brief description:
A Student can take many Courses and a Course can have many Students.
A Student can make many Presentation, but a Presentation can be made by only one Student.
So what we have is a many-to-many relationship between Students and Courses, as well as a one-to-many relationship between Student and Presentations.
I've also added one Student, one Course and one Presentation related to each other.
Here is the code I am running:
class Program
{
static void Main()
{
using (var context = new SportsModelContainer())
{
context.Configuration.LazyLoadingEnabled = false;
context.Configuration.ProxyCreationEnabled = false;
Student student = context.Students.Find(1);
context.
Entry(student).
Collection(s => s.Presentations).
Query().
Where(p => p.Id == 1).
Load();
context.
Entry(student).
Collection(s => s.Courses).
Query().
Where(c => c.Id == 1).
Load();
// Trying to run Load without calling Query() first
context.Entry(student).Collection(s => s.Courses).Load();
}
}
}
After loading the presentations I see that the count for Presentations changed from 0 to 1: . However, after doing the same with Courses nothing changes:
So I try to load the courses without calling Query and it works as expected:
(I removed the Where clause to further highlight the point - the last two loading attempts only differ by the "Query()" call)
Now, the only difference I see is that one relationship is one-to-many while the other one is many-to-many. Is this an EF bug, or am I missing something?
And btw, I checked the SQL calls for the last two Course-loading attempts, and they are 100% identical, so it seems that it's EF that fails to populate the collection.
I could reproduce exactly the behaviour you describe. What I got working is this:
context.Entry(student)
.Collection(s => s.Courses)
.Query()
.Include(c => c.Students)
.Where(c => c.Id == 1)
.Load();
I don't know why we should be forced also to load the other side of the many-to-many relationship (Include(...)) when we only want to load one collection. For me it feels indeed like a bug unless I missed some hidden reason for this requirement which is documented somewhere or not.
Edit
Another result: Your original query (without Include) ...
context.Entry(student)
.Collection(s => s.Courses)
.Query()
.Where(c => c.Id == 1)
.Load();
... actually loads the courses into the DbContext as ...
var localCollection = context.Courses.Local;
... shows. The course with Id 1 is indeed in this collection which means: loaded into the context. But it's not in the child collection of the student object.
Edit 2
Perhaps it is not a bug.
First of all: We are using here two different versions of Load:
DbCollectionEntry<TEntity, TElement>.Load()
Intellisense says:
Loads the collection of entities from
the database. Note that entities that
already exist in the context are not
overwritten with values from the
database.
For the other version (extension method of IQueryable) ...
DbExtensions.Load(this IQueryable source);
... Intellisense says:
Enumerates the query such that for
server queries such as those of
System.Data.Entity.DbSet,
System.Data.Objects.ObjectSet,
System.Data.Objects.ObjectQuery,
and others the results of the query
will be loaded into the associated
System.Data.Entity.DbContext,
System.Data.Objects.ObjectContext or
other cache on the client. This is
equivalent to calling ToList and then
throwing away the list without the
overhead of actually creating the
list.
So, in this version it is not guaranteed that the child collection is populated, only that the objects are loaded into the context.
The question remains: Why gets the Presentations collection populated but not the Courses collection. And I think the answer is: Because of Relationship Span.
Relationship Span is a feature in EF which fixes automatically relationships between objects which are in the context or which are just loaded into the context. But this doesn't happen for all types of relationships. It happens only if the multiplicity is 0 or 1 on one end.
In our example it means: When we load the Presentations into the context (by our filtered explicit query), EF also loads the foreign key of the Presentation entites to the Student entity - "transparently", which means, no matter if the FK is exposed as property in the model of not. This loaded FK allows EF to recognize that the loaded Presentations belong to the Student entity which is already in the context.
But this is not the case for the Courses collection. A course does not have a foreign key to the Student entity. There is the many-to-many join-table in between. So, when we load the Courses EF does not recognize that those courses belong to the Student which is in the context, and therefore doesn't fix the navigation collection in the Student entity.
EF does this automatic fixup only for references (not collections) for performance reasons:
To fix relationship, EF transparently
rewrites the query to bring
relationship info for all relations
which has multiplicity of 0..1 or1 on
the other end; in other words
navigation properties that are entity
reference. If an entity has
relationship with multiplicity of
greater then 1, EF will not bring back
the relationship info because it could
be performance hit and as compared to
bringing a single foreign along with
rest of the record. Bringing
relationship info means retrieving all
the foreign keys the records has.
Quote from page 128 of Zeeshan Hirani's in depth guide to EF.
It is based on EF 4 and ObjectContext but I think this is still valid in EF 4.1 as DbContext is mainly a wrapper around ObjectContext.
Unfortunately rather complex stuff to keep in mind when using Load.
And another Edit
So, what can we do when we want to explicitely load one filtered side of a many-to-many relationship? Perhaps only this:
student.Courses = context.Entry(student)
.Collection(s => s.Courses)
.Query()
.Where(c => c.Id == 1)
.ToList();
I'm trying to understand the Entity Framework, and I have a table "Users" and a table "Pages". These are related in a many-to-many relationship with a junction table "UserPages". First of all I'd like to know if I'm designing this relationship correctly using many-to-many: One user can visit multiple pages, and each page can be visited by multiple users..., so am I right in using many2many?
Secondly, and more importantly, as I have understood m2m relationships, the User and Page tables should not repeat information. I.e. there should be only one record for each user and each page. But then in the entity framework, how am I able to add new visits to the same page for the same user? That is, I was thinking I could simply use the Count() method on the IEnumerable returned by a LINQ query to get the number of times a user has visited a certain page.
But I see no way of doing that. In Linq to Sql I could access the junction table and add records there to reflect added visits to a certain page by a certain user, as many times as necessary. But in the EF I can't access the junction table. I can only go from User to a Pages collection and vice versa.
I'm sure I'm misunderstanding relationships or something, but I just can't figure out how to model this. I could always have a Count column in the Page table, but as far as I have understood you're not supposed to design database tables like that, those values should be collected by queries...
Please help me understand what I'm doing wrong...
You are doing it right.
In the Entity Data Model (EDM) Many-To-Many relationships can be represented with or without a join table, depending on whether it contains some additional fields. See the article below for more details.
In your case, the User entity will directly reference a collection of Page entities and vice versa, since your model doesn't include a mapping for the User_Page join table.
In order to add visits to a certain page on a user you could for example do something like:
using (var context = new YourEntityModelObjectContext())
{
var page = context.Pages.FirstOrDefault(p => p.Url == "http://someurl");
var user = context.Users.FirstOrDefault(u => u.Username == "someuser");
user.Pages.Add(page);
context.SaveChanges();
}
Or you could do it from the other side of the relation:
using (var context = new YourEntityModelObjectContext())
{
var page = context.Pages.FirstOrDefault(p => p.Url == "http://someurl");
var user = context.Users.FirstOrDefault(u => u.Username == "someuser");
page.Users.Add(user);
context.SaveChanges();
}
In both cases a new record will be added to the User_Page join table.
If you need to retrieve the number of pages visited by a particular user you could simply do:
using (var context = new YourEntityModelObjectContext())
{
var user = context.Users.FirstOrDefault(u => u.Username == "someuser");
var visitCount = user.Pages.Count;
}
Related resources:
Many to Many Relationships in the Entity Model