I'm new to EF 4.1 and try to move some code from to EF 4.1 (code first). Here is some background. I'm managing products for several companies, so I have a table "Product" with a column called "Company". I need to update (insert if not exist, otherwise update) this table from an Excel file containing products for a given company(let's say C1). Here is what I'm doing (using proprietary db access code and LINQ) :
Retrieve all products for company C1 as a list of products
For each product appearing in Excel:
Search the loaded list of products if the product from Excel exists already
If product does not exist then :
create new product instance
add product to database
add product to the list of loaded products
Else
update product in database
That's pretty straightforward but converting it to EF 4.1 does not seem that easy:
I can easily retrieve and filter all products from the context. I can also easily search for the Excel product in the loaded list. If not present, I can create the product and add it to the context. But how to mimic my "caching" system where I add the newly added product to my list of product loaded in memory (Excel file can contains several times the same product) ? Also when change the entity state and when to do savechanges ?
Christian
Here is a rough outline of how you can do this.
EF caches the loaded entities. You can use Local property to access it. This collection will be modified by EF when you add new entities.
context.Products.Where(p => p.Company == "C1").ToList();//force EF to load products
while(/*loop through excel file)
{
var product = context.Products.Local.Where(p=> p.Id == excelProductId);
if (product == null)
{
//insert
}
else
{
/update
}
}
context.SaveChanges();
Related
I have 2 classes, like the below.
They can have very large collections - a Website may have 2,000+ WebsitePages and vice-versa.
class WebsitePage
{
public int ID {get;set;}
public string Title {get;set;}
public List<Website> Websites {get;set;}
}
class Website
{
public int ID {get;set;}
public string Title {get;set;}
public List<WebsitePage> WebsitePages {get;set;}
}
I am having trouble removing a WebsitePage from a Website. Particularly when removing a WebsitePage from mutliple Websites.
For example, I might have code like this:
var pageToRemove = db.WebsitePages.FirstOrDefault();
var websites = db.Websites.Include(i => i.WebsitePages).ToList();
foreach(var website in websites)
{
website.WebsitePages.Remove(pageToRemove)
}
If each website Include() 2k pages, you can imagine it takes ages to load that second line.
But if I don't Include() the WebsitePages when fetching the Websites, there is no child collection loaded for me to delete from.
I have tried to just Include() the pages that I need to delete, but of course when saving that gives me an empty collection.
Is there a recommended or better way to approach this?
I am working with an existing MVC site and I would rather not have to create an entity class for the join table unless absolutely necessary.
No, you can't... normally.
A many-to-many relationship (with a hidden junction table) can only be affected by adding/removing items in the nested collections. And for this the collections must be loaded.
But there are some options.
Option 1.
Delete data from the junction table by raw SQL. Basically this looks like
context.Database.ExecuteSqlCommand(
"DELETE FROM WebsiteWebsitePage WHERE WebsiteID = x AND WebsitePageID = y"));
(not using parameters).
Option 2.
Include the junction into the class model, i.e. map the junction table to a class WebsiteWebsitePage. Both Website and WebsitePage will now have
public ICollection<WebsiteWebsitePage> WebsiteWebsitePages { get; set; }
and WebsiteWebsitePage will have reference properties to both Website and WebsitePage. Now you can manipulate the junctions directly through the class model.
I consider this the best option, because everything happens the standard way of working with entities with validations and tracking and all. Also, chances are that sooner or later you will need an explicit junction class because you're going to want to add more data to it.
Option 3.
The box of tricks.
I tried to do this by removing a stub entity from the collection. In your case: create a WebsitePage object with a valid primary key value and remove it from Website.WebsitePages without loading the collection. But EF doesn't notice the change because it isn't tracking Website.WebsitePages, and the item is not in the collection to begin with.
But this made me realize I had to make EF track a Website.WebsitePages collection with 1 item in it and then remove that item. I got this working by first building the Website item and then attaching it to a new context. I'll show the code I used (a standard Product - Category model) to prevent typos.
Product prd;
// Step 1: build an object with 1 item in its collection
Category cat = new Category { Id = 3 }; // Stub entity
using(var db = new ProdCatContext())
{
db.Configuration.LazyLoadingEnabled = false;
prd = db.Products.First();
prd.Categories.Add(cat);
}
// Step 2: attach to a new context and remove the category.
using(var db = new ProdCatContext())
{
db.Configuration.LazyLoadingEnabled = false;
db.Products.Attach(prd);
prd.Categories.Remove(cat);
db.SaveChanges(); // Deletes the junction record.
}
Lazy loading is disabled, otherwise the Categories would still be loaded when prd.Categories is addressed.
My interpretation of what happens here is: In the second step, EF not only starts tracking the product when you attach it, but also its associations, because it 'knows' you can't load these associations yourself in a many to many relationship. It doesn't do this, however, when you add the category in the first step.
I have Customer, CustomerGroups, Groups tables. Customer has 1:n with CustomerGroups and Groups has 1:n with CustomerGroups. In the create customer screen, user can choose the groups information of existing customer. So when I create a new customer, I am retrieving groups from existing customer and adding them
///
Customer cust = new Customer();
foreach(var item in getothercustomergroups())
cust.groups.add(item);
commit
This code is generating update statement and updating the customergroups table with newly added customerid, instead of inserting new records with new customerid. Due to this, all groups with prev. customer are gone. Could anyone please explain this behavior.
From your description, I would guess getcustomergroups has gotten customergroups from an existing customer via NHibernate. If so, NHibernate has associated each item in the foreach loop with the existing customer, so when you add item to a new customer, the new customer assumes ownership of that item.
You can avoid this by cloning the item. See How do I copy an object with NHibernate
I have 2 tables in my database, one for projects members and one for projects
After authenticating the user I have his ID. In my project members table I have the project ID and the user ID.
I can obtain a list of project members for this user using
var pm = db.ProjectMembers.Where(c=> c.UserID=u.UserID) ;
Now my question is how can I get a list of ProjectID from the above list. And then how can I get a list of projects from Project table using the list of ProjectID.
First part of your question:
var projectIDs = db.ProjectMembers.Where(c=> c.UserID=u.UserID).select(pm=>pm.ProjectID) ;
2nd part:
if you are using Entity Framework, you should see a property called (project) for every entity of your ProjectMembers list, you can simply ignore my first part of this answer and go directly with this one
var projectsList = db.ProjectMembers.Where(c=> c.UserID=u.UserID).select(pm=>pm.Project) ;
the property may have a different name, give it a try and let me know what happened.
note: that if the retrieved projects were null, then your Entity framework is working wth Eager loading (Google it).
so in order to let the Entity framework generate the proper SQL syntax to retrieve the projects data, add an Include() to your query as the following:
var projectsList = db.ProjectMembers.Where(c=> c.UserID=u.UserID).Include("Projects").select(pm=>pm.Project) ;
In my model I have two classes Categories and Products. There is a relation many- to many between them.
I set states of all categories on modified manually and when I watched in the debugger before saveChanges() I saw that all of these categories were marked as modified. But after request mapping between categories and product weren't updated in my database. Code of update function.
public void UpdateProduct(Product product)
{
using (EFDbContext context = new EFDbContext())
{
context.Products.Attach(product);
if (product.Categories != null)
{
foreach (var item in product.Categories)
{
context.Entry(item).State = EntityState.Modified;
}
}
context.Entry(product).State = EntityState.Modified;
context.SaveChanges();
}
}
Setting entity to modified says that you have changed its properties (not navigation properties) and you want to save them. If you changed relations (navigation properties) by for example creating new relation between existing product and category or removing relation between existing product and category setting state to modified will not help you. This is actually very hard to solve (it is same in all current EF versions) because that relation has its own state which must be set and state of relation cannot be Modified = you must know if you added or removed relation. Especially removing is hard because you probably don't have information about relations you have removed from Categories navigation property when you are going to attach entity to the context. Moreover DbContext doesn't offer access to state of the relation so you must convert it to ObjectContext and use ObjectStateManager.
The easiest way to solve this issue is to load product with categories from database prior to saving and manually synchronize your detached object graph (the one you are trying to save at the moment) with loaded attached graph. Once you synchronize all changes in attached graph you will save it back to database. Attached graph will know which relations to categories were added or removed.
i have a view that shows the user a form and the user should upload a file and choose all the categories associated with it.
the controller that is responsible in submitting the data should
retrieve the file info and
insert data in the file category
retrieve the related category ids and
insert them as well in the table
that is abstracted by the EF just
insert the file and the category ids.
this is my problem the controller just gets some info about the category not all of it. basically it only needs the ids for the insertion
i can't use
[HttpPost]
public ActionResult SaveFile(File file, List<Category> Checkbox, HttpPostedFileBase FileUpload)
{
//some stuff
//for example got the first category and named it to category1
file.Categories.Add(category1)
}
i asked someone and he told me you have to select the category you want to insert
is this really necessary ? i only need a category id and a file id to make the insert why would i fire another request to the database that i don't really need
i am using
EF 4
MVC 3
It is better to select category first because it will save you a lot of possible problems but it is not necessary. You can use dummy category object:
var category = new Category { Id = receivedId };
file.Categories.Add(category);
You will only create new category and you will set its PK. Now you need to handle file insertion where you must explicitly instruct ObjectContext to insert only file (because your categories exists in database):
context.Files.Attach(file); // now whole object graph is attached but marked as Unchanged
context.ObjectStateManager.ChangeObjectState(file, EntityState.Added); // mark only file entity as inserted
context.SaveChanges();
You can also take opposite direction:
context.Files.AddObject(file); // all objects in object graph are marked for insertion
foreach (var category in file.Categories)
{
// you don't want to insert categories again
context.ObjectStateManager.ChangeObjectState(category, EntityState.Unchanged);
}
context.SaveChanges();
This scenario works if you know that all categories exist in your database. If you want to insert new categories together with saving file you will need to query categories first or add some information about which category is new and which is existing.