I have a many to many relationship in my code first.
public class Post
{
public int Id { get; set; }
public ICollection<Tag> Tags { get; set; }
}
public class Tag
{
public int Id { get; set; }
public ICollection<Post> Posts { get; set; }
}
modelBuilder.Entity<Post>().HasMany(c => c.Tags).WithMany(a => a.Posts);
If i have a PostId and a TagId , How i can insert relationship with single query in entity framework (Without load Post or Tag and add relationship to that)
This is one of the drawbacks of the implicit junction table.
Still it's possible to do what you are asking by creating two "stub" entities, attach them to the context (this telling EF that they are existing), and adding one of them to the collection of the other:
using (var db = new YourDbContext())
{
var post = db.Posts.Attach(new Post { Id = postId });
var tag = db.Tags.Attach(new Tag { Id = tagId });
post.Tags = new List<Tag> { tag };
db.SaveChanges();
}
Due to the hack-ish nature of above technique, make sure to use it only with short lived contexts specifically allocated for the operation.
If I understood your question correctly, you want to ignore the insertion of navigation property. You can change state of the collection property as 'UnChanged' to avoid insertion of the property.
It will looks like;
_context.Posts.Add(post);
_context.Entry(post.Tags).State = EntityState.Unchanged;
_context.SaveChanges();
Related
I am working with an asp MVC5 app with entity framework 6 and want to create an object that has a navigation property like so:
public class widget
{
public int Id { get; set; }
public string Name { get; set; }
public virtual Category Category { get; set; }
}
where the property Category is another entity in the database model. When I implement the CRUD functionality, I create a view model like so:
public class EditWidgetViewModel
{
public List<SelectListItem> Categories { get; set; }
public int CategoryId { get; set; }
}
and the contents of the selectList goes to a dropdown in the html form. Next, the CategoryId gets posted back to the server when the user submits the form. From there, I am currently doing something like the following to save changes:
var dbWidget = new Widget
{
Name = model.Name,
Category = db.Categories.Find(CategoryId)
}
db.Widgets.Add(dbWidget);
db.SaveChanges();
So my question is the following: can I assign the category navigation property without doing another DB lookup with db.Widgets.Find(WidgetId) - I already know the id number that should go in the Category_Category_Id column in the Widgets table of the database without doing a lookup. Also, it seems like if you have something with five or so navigation properties that this would be a significant performance problem with a round trip for each of those.
You can create a new entity with a constructor, assign it a proper id
then use dbContext's Entry().State like this:
Category category = new Category { Id = CategoryId };
db.Entry(category).State = EntityState.Unchanged;
var dbWidget = new Widget
{
Name = model.Name,
Category = category
}
db.Widgets.Add(dbWidget);
db.SaveChanges();
I'm facing an issue when inserting records with Many to Many navigation property,
I have these models
//ModelBase
public class ModelBase
{
[Key]
public long Id { get; set; }
}
//Book
public class Book : ModelBase
{
public string Title { get; set; }
public virtual ICollection<Author> Authors { get; set; }
public virtual ICollection<PublishedBook> PublishedBooks { get; set; }
}
//Author
public class Author : ModelBase
{
public string Name { get; set; }
public virtual ICollection<Book> Books { get; set; }
}
And DTOs called BookDto and AuthorDto for transfer data between layers
From the controller I fill data to DTOs and call Create method (which is in separate layer) to Save data to database,
BookDto bookDto = new BookDto()
{
Id = model.Id,
Title = model.Title,
Authors = model.AuthorIds.Select(c => new AuthorDto { Id = c }).ToList()
}
using (ServiceFactory facory = new ServiceFactory())
{
factory.Book.Create(bookDto);
}
In the Create method I map DTOs with POCOs using ValueInjector
public void Create(BookDto bookDTO)
{
Book book = new Book();
book.InjectFrom<DeepCloneInjection>(bookDTO);
bookRepository.Add(book);
unitOfWork.Commit();
}
And it calls Add method in Genaric Repository Base
public virtual void Add(T entity)
{
dbset.Add(entity);
}
This inserts data to Books table and BookAuthors tables appropriately BUT inserts new record into the Authors table even if I pass Authors which has existing AuthorIds for Book.Authors from the controller.
Any Idea how to fix this?
You are missing to attach the existing authors to the context. You could do it like so:
public void Create(BookDto bookDTO)
{
Book book = new Book();
book.InjectFrom<DeepCloneInjection>(bookDTO);
foreach (var author in book.Authors)
authorRepository.Attach(author);
bookRepository.Add(book);
unitOfWork.Commit();
}
where the generic Attach methods is just implemented calling dbset.Attach(entity);. I am assuming that all repositories (authorRepository and bookRepository) share the same context instance. Otherwise the above won't work.
The fact that the AuthorId already exists in the database doesn't matter. EF doesn't check if the Id exists by a query first. If you call Add on an object graph of detached entities it tries to insert the whole graph by generating INSERT statements for the parent and for all children. By attaching the children you effectively turn off the INSERT statements for those.
I have a database created by EF code-first, where there are two major classes: 'categories', and: 'products'.
Each category has a navigation property of a list of products (those that are associated with that category).
I've created a web api returning an 'IQueryable' - all the products associated with a specific category.
This is the api controller - which works just fine when called via a url:
[HttpGet]
public IQueryable<Product> ProductsFilteredByCategory(int categoryId)
{
return _contextProvider.Context.Categories.Include("Products").First(c => c.CategoryId == categoryId).Products.AsQueryable();
}
However, when I make a call from breeze via the following function:
var getProducts = function (productsObservable, parentCategoryId) {
var query = EntityQuery.from("ProductsFilteredByCategory")
.withParameters({ categoryId: parentCategoryId });
return manager.executeQuery(query)
.then(querySucceeded)
.fail(queryFailed);
}
I get the following error: 'undefined is not a function'.
Strangely, when I change the controller on the server side to return just a plain 'IQyeryable' list of all products - there are no errors... but this is not what I need - I only need those products which are associated with a specific category...
Thanks for any help !
Elior
You don't have to use the parameter in your controller method, you can remove the parameter and declare it in your breeze entity query with .where() the good thing about doing it this way is your controller doesn't have to be so specific.
Also, you don't need to use .include() as breeze allows you to expand the navigation property with .expand()
This leverages more of what breeze can help you with in your project.
Change ProductsFilteredByCategory(int categoryId) to
Public IQueryable<Product> Products()
{
return _contextProvider.Context.Products();
}
And your breeze query to
var query = EntityQuery.from('Products')
.where('categoryId', '==', categoryId)
.expand('Category');
If you don't want to do this, then I think you need to redo your controller method. You are supposed to be returning Product entity but you are querying for categories.
Thank you both very much for the support, I do appreciate it.
Kadumel, I could not use your solution because each 'Product' can belong to more than one 'Category' so there's no reference form 'Product' to 'Category'. However, I did change the controller method as you suggested and now things work.
Here's the code the represents everything so that hopefully it will also help others with similar situations. I'll be happy to hear and suggestions for improving the code.
public class Category
{
[Key]
public int CategoryId { get; set; }
[Required]
public string Name { get; set; }
public Int16? Order { get; set; }
[ForeignKey("ParentCategoryId")]
public Category ParentCategory { get; set; }
public int? ParentCategoryId { get; set; }
[ForeignKey("ParentCategory")]
[InverseProperty("ParentCategory")]
public List<Category> SubCategories { get; set; }
[ForeignKey("ProductId")]
public List<Product> Products { get; set; }
}
public class Product
{
[Key]
public int ProductId { get; set; }
[Required]
public string Name { get; set; }
public string Desc { get; set; }
public int Price { get; set; }
//[ForeignKey("CategoryId")]
//[InverseProperty("Products")]
//public List<Category> Categories { get; set; }
}
(as you can see I commented out the backwards list of links from 'Product' to 'Category' which could have been used as a backwards reference from 'Product' to 'Category' as Kdumel suggested I use these links, but it seems to me it's too 'heavey' to have so many references when I can do without them, do you agree?)
This is the code in the controller:
[HttpGet]
public IQueryable<Category> CategoryWithProducts(int categoryId)
{
return _contextProvider.Context.Categories.Include("Products").Where(c => c.CategoryId == categoryId);
}
and this is the breeze code:
var getProducts = function (productsObservable, parentCategoryId) {
var query = EntityQuery.from("CategoryWithProducts")
.withParameters( {categoryId: parentCategoryId } )
.select("Products");
return manager.executeQuery(query)
.then(querySucceeded)
.fail(queryFailed);
function querySucceeded(data) {
if (productsObservable) {
productsObservable(data.results[0].products);
}
log('Retrieved [Products] from remote data source',
data, true);
}
};
As you can see, the result is a single 'Category' from which I retrieve the 'products' in the 'querySuccedded()' function.
This works, but I was hoping to use a different approach which did NOT work: instead of passing 'parentCategoryId' I tried to pass the actual object and not the ID as 'parentCategoryObj', and then I thought of using the following line to load the 'products' navigation property without making an explicit call to the breeze controller:
parentCategoryObj.entityAspect.loadNavigationProperty("products")
However, this resulted in no 'products' being loaded as if the navigation property is null. Oddly, when I changed the word "products" to "subCategories" in this line just to check if navigation properties are the problem - the 'subCategories' data was loaded properly. I did not understand why one navigation property behaves differently from another (both are lists). I read more about this and noticed the currently Breeze does not support "Many to Many" relationships, I assume this is the reason, am I right?...
Thanks you guys again, It is a releif to know good people are willing to help !
Elior
I use breezejs in my Durandal web application.
Here is my code to get my invoice & lines behind it:
var getInvoiceById = function (invoiceId, invoiceObservable, forceRemote) {
// Input: invoiceId: the id of the invoice to retrieve
// Input: forceRemote: boolean to force the fetch from server
// Output: invoiceObservable: an observable filled with the invoice
if (forceRemote)
queryCacheInvoice = {};
var query = entityQuery.from('Invoices')
.where('id', '==', invoiceId)
.expand("Client, Client.Contacts, Lines")
.orderBy('Lines.Number');
var isInCache = queryCacheInvoice[invoiceId];
if (isInCache && !forceRemote) {
query = query.using(breeze.FetchStrategy.FromLocalCache);
} else {
queryCacheInvoice[invoiceId] = true;
query = query.using(breeze.FetchStrategy.FromServer);
}
return manager.executeQuery(query)
.then(querySucceeded)
.fail(queryFailed);
function querySucceeded(data) {
invoiceObservable(data.results[0]);
}
};
And here is the models for Invoice:
public class Invoice
{
[Key]
public int Id { get; set; }
public string Number { get; set; }
public DateTime? Date { get; set; }
public int? ClientId { get; set; }
public string Comment { get; set; }
public double? TotalExclVAT { get; set; }
public double? TotalInclVAT { get; set; }
public double? TotalVAT { get; set; }
public bool? WithoutVAT { get; set; }
public virtual List<InvoiceLine> Lines { get; set; }
public virtual Client Client { get; set; }
}
Please notice that for each invoice I have many invoice lines:
public virtual List<InvoiceLine> Lines { get; set; }
And here is the models for InvoiceLine:
public class InvoiceLine
{
[Key]
public int Id { get; set; }
[Required]
public int Number { get; set; }
[Required]
public string Description { get; set; }
public int InvoiceId { get; set; }
public Invoice Invoice { get; set; }
}
The problem: when I execute this breeze query I got the error below:
Error retreiving data. unable to locate property: Lines on type: Invoice
The problem is around the orderBy clause. I have a 1-to-many relationship between the Invoice and the InvoiceLine so it seems I cannot perform an order by in this case.
My question: how to proceed to be able to sort my lines of invoice by number?
Thanks.
Short answer: You can't. This is a limitation of Entity Framework, not Breeze.
You cannot filter, select, or order the related entities that you include with "expand" in an EF LINQ query.
You will probably manage the sort order of related entities on the client, e.g., the display of your order line items.
Note also that the collection of entities returned by a Breeze navigation path is unordered. I wasn't sure what happens if you tried to sort a Breeze entity navigation collection (e.g., Order.LineItems). I was afraid that would cause Breeze to think that you had made changes to the entities ... because a sort would seem to remove-and-add entities to the collection as it sorted. Your EntityManager would think there were changes pending when, in fact, nothing of substance has changed.
I tried an experiment and it all seems to work fine. I did something like this with Northwind:
fetched the Orders of a Customer ("Let's Stop N Shop")
checked the sequence of cust.Orders(); they have unordered OrderIDs: [10719, 10735, 10884, 10579]
executed a line like this: cust.Orders().sort(function(left, right){return left.OrderID() < right.OrderID()? -1 : 1})
checked the sequence of cust.Orders() again; this time they are sorted: [10579, 10719, 10735, 10884]
checked the customer's EntityManager.hasChanges() ... still false (no changes).
I confess that I am happily surprised. I need to write a proper test to ensure that this works reliably. And I have to make sure that the Knockout binding to the navigation property displays them in the sorted order. But I'm encouraged so far.
Important Notes:
Breeze won't keep the list sorted. You'll have to do that if you add new orders or if Breeze adds new orders to the collection as a result of subsequent queries.
Your sort affects every view that is bound to this navigation property. If you want each view to have its own sort of the entities in that collection, you'll have to maintain separate, view-specific collections that shadow the navigation property collection.
If I am wrong about all of this, you'll have to manage a shadow collection of the related entities, sorted as you wish, for each ViewModel.
Update 2 June
I suspected that we would have to let KO know about the array change after sort by calling valueHasMutated. I took 15 minutes for an experiment. And it seems to work fine.
I created a new KO Todo app from the ASP.NET SPA template (there's currently a phantom complaint about a missing jQuery.UI library which is totally unnecessary anyway).
I added a "sort link" to the index.cshtml just above the delete-TodoList link:
Sort
Then I implemented it in viewModel.js:
var sortAscending = true;
var viewmodel = {
...
sortList: sortList,
...
};
...
function sortList(list) {
list.todos().sort(function(left, right) {
return (sortAscending ? 1 : -1) *
(left.title().toLowerCase() < right.title().toLowerCase() ? -1 : 1);
});
sortAscending = !sortAscending; // reverse sort direction
list.todos.valueHasMutated(); // let KO know that we've sorted
}
Works like a charm. Sorting, adding, deleting Todos at will. The app is saving when expected as I add and delete ... but not during save.
Give valueHasMutated a try.
I work on a project with Entity Framework Code First + Durandal + Breeze.
I have these entities models:
public class Packing
{
[Key]
public int Id { get; set; }
public string PackingDescription { get; set; }
...
public virtual List<Isotope> Isotopes { get; set; }
public virtual List<PhysicalForm> PhysicalForms { get; set; }
public virtual List<ChemicalForm> ChemicalForms { get; set; }
}
public class Isotope
{
[Key]
public int Id { get; set; }
public string Name { get; set; }
...
public int PackingId { get; set; }
public virtual Packing Packing { get; set; }
}
public class ChemicalForm
{
[Key]
public int Id { get; set; }
public string Name { get; set; }
...
public int PackingId { get; set; }
public virtual Packing Packing { get; set; }
}
public class PhysicalForm
{
[Key]
public int Id { get; set; }
public string Name { get; set; }
...
public int PackingId { get; set; }
public virtual Packing Packing { get; set; }
}
When I run my project my Entity Framework Code First database is created.
First, I test the cascade delete 'by hand' directly in my database. When I have one Packing with multiple Isotopes and I delete the Packing, all cascaded Isotopes are deleted. This is ok for me.
Now at runtime in my project using breeze, when I try the same scenario: delete a Packing element like this:
var deletePackings = function (packingsObservable) {
// Input: packingsObservable: an observable filled with a list of packings to delete
// Output: none
for (var i = 0; i < packingsObservable().length; i++) {
packingsObservable()[i].entityAspect.setDeleted();
};
return manager.saveChanges();
};
I got the error below:
The UPDATE statement conflicted with the FOREIGN KEY constraint \"FK_dbo.Isotopes_dbo.Packings_PackingId\". The conflict occurred in database \"TRANSPORTBOEKDB\", table \"dbo.Packings\", column 'Id'.\r\nThe statement has been terminated."}
Now I read on another SO post that
Breeze does not yet support client side 'cascaded' deletes (we are considering this one), you will need to iterate over any client side orders that are already loaded and 'detach' them.
So is this the reason why I got the error in my application?
Do I have to iterate over any child entities that are already loaded and 'detach' them?
UPDATE
By manually detaching any child entities by code with breeze do the trick but this is painful:
var deletePackings = function (packingsObservable) {
// Input: packingsObservable: an observable filled with a list of packings to delete
// Output: none
// Remark: we loop from end to begin of the observable!
var entity;
// Since Breeze does not yet support client side 'cascaded' deletes (we are considering this one),
// you will need to iterate over any child entity that are already loaded and 'detach' them.
for (var i = packingsObservable().length - 1; i >= 0; i--) {
// Detach any child entities of type isotope
for (var j = packingsObservable()[i].isotopes().length - 1; j >= 0; j--) {
entity = packingsObservable()[i].isotopes()[j];
manager.detachEntity(entity);
}
// Detach any child entities of type chemicalForm
for (var j = packingsObservable()[i].chemicalForms().length - 1; j >= 0; j--) {
entity = packingsObservable()[i].chemicalForms()[j];
manager.detachEntity(entity);
}
// Detach any child entities of type physicalForm
for (var j = packingsObservable()[i].physicalForms().length - 1; j >= 0; j--) {
entity = packingsObservable()[i].physicalForms()[j];
manager.detachEntity(entity);
}
packingsObservable()[i].entityAspect.setDeleted();
};
return manager.saveChanges();
};
No better solution?
Do cascade delete in SQL if needed and forget in on client, just refresh data after.
Or you can add new IsActive column for agreggate entity, this is my prefered approach, I am scared of DB deletes:)
Support for cascade delete is a very reasonable request.
Please add a vote for client side cascade delete support here: Breeze User Voice. We take this venue very seriously in determining what features to add to the Breeze product.
The feature the breeze authors are considering is client-side cascade delete, but breeze also has no support for the server-side cascade delete. You have to do certain things in order to use a server-side cascade delete.
setDeleted nulls all referencing fkey values in the cache -- even if the fkey is non-nullable. If a referencing record has a non-nullable fkey constraint, then you need to prevent saving the child records since the UPDATE would fail. You're approach to evict the children from the cache before the save accomplishes this goal and allows you to call saveChanges() with no parameters. Alternatively, you can pass an array that contains the records you set deleted. You would then probably want to evict the children after the save. Or you can just stop using that entity manager.