I have a database that unfortunately have no real foreign keys (I plan to add this later, but prefer not to do it right now to make migration easier). I have manually written domain objects that map to the database to set up relationships (following this tutorial http://www.codeproject.com/Articles/43025/A-LINQ-Tutorial-Mapping-Tables-to-Objects), and I've finally gotten the code to run properly. However, I've noticed I now have the SELECT N + 1 problem. Instead of selecting all Product's they're selected one by one with this SQL:
SELECT [t0].[id] AS [ProductID], [t0].[Name], [t0].[info] AS [Description]
FROM [products] AS [t0]
WHERE [t0].[id] = #p0
-- #p0: Input Int (Size = -1; Prec = 0; Scale = 0) [65]
Controller:
public ViewResult List(string category, int page = 1)
{
var cat = categoriesRepository.Categories.SelectMany(c => c.LocalizedCategories).Where(lc => lc.CountryID == 1).First(lc => lc.Name == category).Category;
var productsToShow = cat.Products;
var viewModel = new ProductsListViewModel
{
Products = productsToShow.Skip((page - 1) * PageSize).Take(PageSize).ToList(),
PagingInfo = new PagingInfo
{
CurrentPage = page,
ItemsPerPage = PageSize,
TotalItems = productsToShow.Count()
},
CurrentCategory = cat
};
return View("List", viewModel);
}
Since I wasn't sure if my LINQ expression was correct I tried to just use this but I still got N+1:
var cat = categoriesRepository.Categories.First();
Domain objects:
[Table(Name = "products")]
public class Product
{
[Column(Name = "id", IsPrimaryKey = true, IsDbGenerated = true, AutoSync = AutoSync.OnInsert)]
public int ProductID { get; set; }
[Column]
public string Name { get; set; }
[Column(Name = "info")]
public string Description { get; set; }
private EntitySet<ProductCategory> _productCategories = new EntitySet<ProductCategory>();
[System.Data.Linq.Mapping.Association(Storage = "_productCategories", OtherKey = "productId", ThisKey = "ProductID")]
private ICollection<ProductCategory> ProductCategories
{
get { return _productCategories; }
set { _productCategories.Assign(value); }
}
public ICollection<Category> Categories
{
get { return (from pc in ProductCategories select pc.Category).ToList(); }
}
}
[Table(Name = "products_menu")]
class ProductCategory
{
[Column(IsPrimaryKey = true, Name = "products_id")]
private int productId;
private EntityRef<Product> _product = new EntityRef<Product>();
[System.Data.Linq.Mapping.Association(Storage = "_product", ThisKey = "productId")]
public Product Product
{
get { return _product.Entity; }
set { _product.Entity = value; }
}
[Column(IsPrimaryKey = true, Name = "products_types_id")]
private int categoryId;
private EntityRef<Category> _category = new EntityRef<Category>();
[System.Data.Linq.Mapping.Association(Storage = "_category", ThisKey = "categoryId")]
public Category Category
{
get { return _category.Entity; }
set { _category.Entity = value; }
}
}
[Table(Name = "products_types")]
public class Category
{
[Column(Name = "id", IsPrimaryKey = true, IsDbGenerated = true, AutoSync = AutoSync.OnInsert)]
public int CategoryID { get; set; }
private EntitySet<ProductCategory> _productCategories = new EntitySet<ProductCategory>();
[System.Data.Linq.Mapping.Association(Storage = "_productCategories", OtherKey = "categoryId", ThisKey = "CategoryID")]
private ICollection<ProductCategory> ProductCategories
{
get { return _productCategories; }
set { _productCategories.Assign(value); }
}
public ICollection<Product> Products
{
get { return (from pc in ProductCategories select pc.Product).ToList(); }
}
private EntitySet<LocalizedCategory> _LocalizedCategories = new EntitySet<LocalizedCategory>();
[System.Data.Linq.Mapping.Association(Storage = "_LocalizedCategories", OtherKey = "CategoryID")]
public ICollection<LocalizedCategory> LocalizedCategories
{
get { return _LocalizedCategories; }
set { _LocalizedCategories.Assign(value); }
}
}
[Table(Name = "products_types_localized")]
public class LocalizedCategory
{
[Column(Name = "id", IsPrimaryKey = true, IsDbGenerated = true, AutoSync = AutoSync.OnInsert)]
public int LocalizedCategoryID { get; set; }
[Column(Name = "products_types_id")]
private int CategoryID;
private EntityRef<Category> _Category = new EntityRef<Category>();
[System.Data.Linq.Mapping.Association(Storage = "_Category", ThisKey = "CategoryID")]
public Category Category
{
get { return _Category.Entity; }
set { _Category.Entity = value; }
}
[Column(Name = "country_id")]
public int CountryID { get; set; }
[Column]
public string Name { get; set; }
}
I've tried to comment out everything from my View, so nothing there seems to influence this. The ViewModel is as simple as it looks, so shouldn't be anything there.
When reading this ( http://www.hookedonlinq.com/LinqToSQL5MinuteOVerview.ashx) I started suspecting it might be because I have no real foreign keys in the database and that I might need to use manual joins in my code. Is that correct? How would I go about it? Should I remove my mapping code from my domain model or is it something that I need to add/change to it?
Note: I've stripped parts of the code out that I don't think is relevant to make it cleaner for this question. Please let me know if something is missing.
EDIT: Gert Arnold solved the issue of all Products from the Category being queried one by one. However I'm still having the issue that all Products displayed on the page gets queried one by one.
This happens from my view code:
List.cshtml:
#model MaxFPS.WebUI.Models.ProductsListViewModel
#foreach(var product in Model.Products) {
Html.RenderPartial("ProductSummary", product);
}
ProductSummary.cshtml:
#model MaxFPS.Domain.Entities.Product
<div class="item">
<h3>#Model.Name</h3>
#Model.Description
#if (Model.ProductSubs.Count == 1)
{
using(Html.BeginForm("AddToCart", "Cart")) {
#Html.HiddenFor(x => x.ProductSubs.First().ProductSubID);
#Html.Hidden("returnUrl", Request.Url.PathAndQuery);
<input type="submit" value="+ Add to cart" />
}
}
else
{
<p>TODO: länk eller dropdown för produkter med varianter</p>
}
<h4>#Model.LowestPrice.ToString("c")</h4>
</div>
Is it something with .First() again? I tried .Take(1) but then I couldn't select the ID anyway...
EDIT: I tried adding some code to my repository to access the DataContext and this code to create a DataLoadOptions. But it still generates a query for each ProductSub.
var dlo = new System.Data.Linq.DataLoadOptions();
dlo.LoadWith<Product>(p => p.ProductSubs);
localizedCategoriesRepository.DataContext.LoadOptions = dlo;
var productsInCategory = localizedCategoriesRepository.LocalizedCategories.Where(lc => lc.CountryID == 1 && lc.Name == category)
.Take(1)
.SelectMany(lc => lc.Category.ProductCategories)
.Select(pc => pc.Product);
The SQL generated is slightly different though, and the order of the queries is also different.
For the queries that select ProductSub the DataLoadOptions-code generates variables named #x1 and without them the variables are named #p0.
SELECT [t0].[products_id] AS [ProductID], [t0].[id] AS [ProductSubID], [t0].[Name], [t0].[Price]
FROM [products_sub] AS [t0]
WHERE [t0].[products_id] = #x1
The difference in order for queries to me indicate that DataLoadOptions is in fact doing something, but not what I expect. What I'd expect is for it to generate something like this:
SELECT [t0].[products_id] AS [ProductID], [t0].[id] AS [ProductSubID], [t0].[Name], [t0].[Price]
FROM [products_sub] AS [t0]
WHERE [t0].[products_id] = #x1 OR [t0].[products_id] = #x2 OR [t0].[products_id] = #x3 ... and so on
It is the First(). It triggers execution of the part before it and the part following it is fetched by lazy loading in separate queries. Tricky, hard to spot.
This is what you can do to prevent it and fetch everything in one shot:
LocalizedCategories.Where(lc => lc.CountryID == 1 && lc.Name == category)
.Take(1)
.SelectMany(lc => lc.Category.ProductCategories)
.Select (pc => pc.Product)
You should make the member ProductCategories public. I think it is also better to remove the derived properties Category.Products and Product.Categories, because I think they will trigger a query whenever their owner is materialized or addressed.
Related
I use this method to retrieve Items for a Tree component in Blazor Serverside, In the DAL I have:
public List<TreeItem> GetTreeItems()
{
var tree = new List<TreeItem>();
TreeItem item = new TreeItem()
{
DepartmentID = 0,
CategoryID = 0,
Text = "Root",
Childs = context.Departments.OrderBy(d => d.Order).Select(d => new TreeItem()
{
DepartmentID = d.Id,
CategoryID = 0,
Text = d.Title,
Childs = d.Categories.OrderBy(c => c.Order).Select(c => new TreeItem()
{
DepartmentID = d.Id,
CategoryID = c.Id,
Text = c.Title
}).ToList()
}).ToList()
};
tree.Add(item);
return tree;
}
The TreeItem class is the following, (The model shared by the blazor Component and Dal Class):
public class TreeItem
{
public int DepartmentID { get; set; }
public int CategoryID { get; set; }
public string Text { get; set; }
public List<TreeItem> Childs { get; set; }
}
But when I was to retrieve Items for the tree in the blazor component I get the exception: Operation is not valid due to the current state of the object., admin is the DAL class I inject to Blazor component as follows:
private void GetTreeModel()
{
try
{
Items = admin.GetTreeItems();
TreeSuccess = true;
TreeMessage = "Success";
return;
}
catch (Exception ex) // Error here
{
TreeSuccess = false;
TreeMessage = "Can not load tree items";
return;
}
}
What is this error and How to solve it?
I solved my problem using First loading entities and then using Linq to Objects, Like this:
var tree = new List<TreeItem>();
var departments = context.Departments.OrderBy(d => d.Order).ToList();
var categories = context.Categories.OrderBy(c => c.Order).ToList();
TreeItem item = new TreeItem()
{
DepartmentID = 0,
CategoryID = 0,
Text = "Root",
Childs = departments.Select(d => new TreeItem()
{
DepartmentID = d.Id,
CategoryID = 0,
Text = d.Title,
Childs = categories.Where(c => c.DepartmentID == d.Id).OrderBy(c => c.Order).Select(c => new TreeItem()
{
DepartmentID = d.Id,
CategoryID = c.Id,
Text = c.Title
}).ToList()
}).ToList()
};
tree.Add(item);
return tree;
}
I'm trying to setup a FK relationship between two columns that will delete all children in the Db when a parent row is deleted. My definitions look like:
[Table]
public class Parent
{
[Column(IsPrimaryKey = true, IsDbGenerated = true, DbType = "INT NOT NULL Identity", CanBeNull = false, AutoSync = AutoSync.OnInsert)]
public int Id { get; set; }
[Column]
public string Dummy
{
get { return "dummy"; }
set { }
}
private EntitySet<Child> _children;
[Association(Name = "FK_Parent_Child", DeleteRule = "CASCADE", OtherKey = "ParentId", ThisKey="Id", Storage="_children")]
public EntitySet<Child> Children
{
get
{
return _children;
}
set
{
_children.Assign(value);
}
}
public Parent()
{
_children = new EntitySet<Child>(
item => item.Parent = this,
item => item.Parent = null);
}
}
[Table]
public class Child
{
[Column(IsPrimaryKey = true, IsDbGenerated = true, DbType = "INT NOT NULL Identity", CanBeNull = false, AutoSync = AutoSync.OnInsert)]
public int Id { get; set; }
[Column]
public int? ParentId { get; set; }
private EntityRef<Parent> _parent;
[Association(Name="FK_Child_Parent", ThisKey = "ParentId", Storage = "_parent", OtherKey = "Id", IsForeignKey = true, DeleteRule="CASCADE")]
public Parent Parent
{
get
{
return _parent.Entity;
}
set
{
var previousValue = _parent.Entity;
if (previousValue != value || !this._parent.HasLoadedOrAssignedValue)
{
if (previousValue != null)
_parent.Entity = null;
_parent.Entity = value;
if (value != null)
ParentId = value.Id;
else
ParentId = null;
}
}
}
}
From what I can tell this seems implementation of FKs seems to work. Adding a parent row to the Db will automatically add child rows; selecting a parent row properly fills in the Children property with all related children.
I would also like to be able to delete a parent row in the database and have that delete also remove all related children. With this setup, when I delete a parent I get the error "The primary key value cannot be deleted because references to this key still exist. [ Foreign key constraint name = FK_Child_Parent ]".
It appears the DeleteRule="Cascade" isn't being honored, but I'm not sure why.
I know it's very late, but I have had the same problem and this was the first post I found.
All I want to say is that everything works.
You should probably not capitalize rule name. And set DeleteRule on parent entity.
Here is my working code.
Parent entity field.
private EntitySet<ExerciseDataContext> _exercises = new EntitySet<ExerciseDataContext>();
[Association(Name = Constants.ForeignKeysNames.KF_GROUP_EXERCISE, Storage = "_exercises", OtherKey = "GroupID", ThisKey = "ID", DeleteRule = "Cascade")]
public ICollection<ExerciseDataContext> Exercises
{
get { return _exercises; }
set { _exercises.Assign(value); }
}
And child entity field.
private EntityRef<GroupDataContext> _group = new EntityRef<GroupDataContext>();
[Association(Name = Constants.ForeignKeysNames.KF_GROUP_EXERCISE, IsForeignKey = true, Storage = "_group", ThisKey = "GroupID")]
public GroupDataContext Group
{
get { return _group.Entity; }
set { _group.Entity = value; }
}
Hope it will help someone.
I been practicing with EF Code First, SQL Express, and ASP.Net MVC3.
When I run the website first the correct tables are generated by the FooInitializer and Student and Image are populated but for some reason the join table (StudentImages) is not being populated.
What could be the issue?
Tables: Student, Image, and StudentImages
public class Student
{
public int Id { get; set; }
public string Name { get; set; }
public virtual ICollection<Image> Images { get; set; }
}
public class Image
{
public int Id { get; set; }
public string Filename { get; set; }
public string Extension { get; set; }
public virtual ICollection<Student> Students { get; set; }
}
public class FooInitializer : DropCreateDatabaseIfModelChanges<DBContext>
{
protected override void Seed(DBContext context)
{
var students = new List<Student> {
new Student { Id = 1, Name = "John" },
new Student { Id = 2, Name = "Jane" }
};
students.ForEach(s => context.Students.Add(s));
context.SaveChanges();
var images = new List<Image> {
new Image { Id = 1, Filename = "IMG_4596.JPG", Extension = ".jpg" },
new Image { Id = 2, Filename = "IMG_4600.JPG", Extension = ".jpg" }
};
images.ForEach(i => context.Images.Add(i));
students[0].Images.Add(images[0]);
context.SaveChanges();
}
}
From what I can tell your Image class does not have a reference to the StudentID. Try adding:
public int StudentID { get; set; }
to the Image class maybe?
Also having an ICollection would mean that one image could have multiple students - is this correct? Maybe it should be a public virtual Student Student {...}
EDIT: Also I found this, with a many to many relationship (if thats what you need):
In your OnModelCreating() Method:
modelBuilder.Entity<Student>()
.HasMany(c => c.Images).WithMany(i => i.Students)
.Map(t => t.MapLeftKey("StudentId")
.MapRightKey("ImageID")
.ToTable("StudentImages"));
taken from this link that states:
A many-to-many relationship between the Instructor and Course
entities. The code specifies the table and column names for the join
table. Code First can configure the many-to-many relationship for you
without this code, but if you don't call it, you will get default
names such as InstructorInstructorID for the InstructorID column.
EDIT: Here is the code I used the other night, with my implementation of the code first MVC site:
var users = new List<User>
{
new User { UserID = new Guid(), Email = "me#me.com", LastOnline = DateTime.Now, Password = "pword", RegistrationDate = DateTime.Now, SecurityAnswer = "me", SecurityQuestion = "who?", Roles = new List<Role>() },
};
users.ForEach(s => context.Users.Add(s));
context.SaveChanges();
var roles = new List<Role>
{
new Role { RoleID = "Admin", Description = "Administration Users", Users = new List<User>() }
};
roles.ForEach(r => context.Roles.Add(r));
users[0].Roles.Add(roles[0]);
context.SaveChanges();
var userLicense = new List<UserLicense>
{
new UserLicense { AddDateTime = DateTime.Now, LicenseType = "Farmer", Manufacturer = "Motorola", Model = "Droid", PhoneIdentifier = "c0e4223a910f", UserID = users[0].UserID, User = new User() }
};
userLicense[0].User = users[0];
userLicense.ForEach(u => context.UserLicenses.Add(u));
context.SaveChanges();
userLicense[0].User = users[0];
context.SaveChanges();
Notice in each instantiated item, I am also instantiating a new referenced item within the parent object.
EDIT:
Ok try this:
var students = new List<Student> {
new Student { Id = 1, Name = "John", Images = new List<Image>() },
new Student { Id = 2, Name = "Jane", Images = new List<Image>() }
};
students.ForEach(s => context.Students.Add(s));
context.SaveChanges();
var images = new List<Image> {
new Image { Id = 1, Filename = "IMG_4596.JPG", Extension = ".jpg", Students = new List<Student>() },
new Image { Id = 2, Filename = "IMG_4600.JPG", Extension = ".jpg", Students = new List<Student>() }
};
images.ForEach(i => context.Images.Add(i));
students[0].Images.Add(images[0]);
students[1].Images.Add(images[1]);
context.SaveChanges();
Try adding this before saving changes for each student:
foreach (Image i in s1.Images)
context.ObjectStateManager.ChangeObjectState(i, System.Data.EntityState.Added);
Also try with System.Data.EntityState.Modified.
Hope this works...
I want to group by the categoryid and then do a count on this. But I don't know how to do this. I have tried a couple of ways without success. Here is my latest:
public class Count
{
public int TradersCount { get; set; }
public int Id { get; set; }
public string Description { get; set; }
}
public IQueryable<Count> CountTradersAttachedToCategories()
{
var data = from tc in _db.tblTradersCategories
select new Count
{
Description = tc.tblCategory.description,
Id = tc.tblCategory.categoryId,
TradersCount = tc.Select(x => x.categoryid).GroupBy().Count()
};
return data;
}
tblTradersCategories joins both
tblTraders/tblCategories
A single trader can have many categories
A single category can have many traders
Thanks in advance for any help.
Clare
Try this:
var data = from tc in _db.tblTradersCategories
group tc by new { tc.tblCategory.categoryId,
tc.tblCategory.description } into g
select new { Count = g.Count(),
Id = g.Key.categoryId,
Description = g.Key.description };
If you want that in your Count class you may need to use AsEnumerable() to perform the conversion in process:
var converted = data.AsEnumerable()
.Select(c => new Count { TradersCount = c.Count,
Id = c.Id,
Description = c.Description });
You can try doing them all in one go:
var data = from tc in _db.tblTradersCategories
group tc by new { tc.tblCategory.categoryId,
tc.tblCategory.description } into g
select new Count { TradersCount = g.Count,()
Id = g.Key.categoryId,
Description = g.Key.description };
But I don't know if that will work. It depends on how the LINQ provider handles it.
I would like to perform LINQ to SQL mapping in C#, in a many-to-many relationship, but where data is not mandatory.
To be clear:
I have a news site/blog, and there's a table called Posts. A blog can relate to many categories at once, so there is a table called CategoriesPosts that links with foreign keys with the Posts table and with Categories table. I've made each table with an identity primary key, an id field in each one, if it matters in this case.
In C# I defined a class for each table, defined each field as explicitly as possible. The Post class, as well as Category class, have a EntitySet to link to CategoryPost objects, and CategoryPost class has 2 EntityRef members to link to 2 objects of each other type.
The problem is that a Post may relate or not to any category, as well as a category may have posts in it or not. I didn't find a way to make an EntitySet<CategoryPost?> or something like that.
So when I added the first post, all went well with not a single SQL statement. Also, this post was present in the output. When I tried to add the second post I got an exception, Object reference not set to an instance of an object, regarding to the CategoryPost member.
Post:
[Table(Name="tm_posts")]
public class Post : IDataErrorInfo
{
public Post()
{
//Initialization of NOT NULL fields with their default values
}
[Column(Name = "id", DbType = "int", CanBeNull = false, IsDbGenerated = true, IsPrimaryKey = true)]
public int ID { get; set; }
private EntitySet<CategoryPost> _categoryRef = new EntitySet<CategoryPost>();
[Association(Name = "tm_rel_categories_posts_fk2", IsForeignKey = true, Storage = "_categoryRef", ThisKey = "ID", OtherKey = "PostID")]
public EntitySet<CategoryPost> CategoryRef
{
get { return _categoryRef; }
set { _categoryRef.Assign(value); }
}
}
CategoryPost
[Table(Name = "tm_rel_categories_posts")]
public class CategoryPost
{
[Column(Name = "id", DbType = "int", CanBeNull = false, IsDbGenerated = true, IsPrimaryKey = true)]
public int ID { get; set; }
[Column(Name = "fk_post", DbType = "int", CanBeNull = false)]
public int PostID { get; set; }
[Column(Name = "fk_category", DbType = "int", CanBeNull = false)]
public int CategoryID { get; set; }
private EntityRef<Post> _post = new EntityRef<Post>();
[Association(Name = "tm_rel_categories_posts_fk2", IsForeignKey = true, Storage = "_post", ThisKey = "PostID", OtherKey = "ID")]
public Post Post
{
get { return _post.Entity; }
set { _post.Entity = value; }
}
private EntityRef<Category> _category = new EntityRef<Category>();
[Association(Name = "tm_rel_categories_posts_fk", IsForeignKey = true, Storage = "_category", ThisKey = "CategoryID", OtherKey = "ID")]
public Category Category
{
get { return _category.Entity; }
set { _category.Entity = value; }
}
}
Category
[Table(Name="tm_categories")]
public class Category
{
[Column(Name = "id", DbType = "int", CanBeNull = false, IsDbGenerated = true, IsPrimaryKey = true)]
public int ID { get; set; }
[Column(Name = "fk_parent", DbType = "int", CanBeNull = true)]
public int ParentID { get; set; }
private EntityRef<Category> _parent = new EntityRef<Category>();
[Association(Name = "tm_posts_fk2", IsForeignKey = true, Storage = "_parent", ThisKey = "ParentID", OtherKey = "ID")]
public Category Parent
{
get { return _parent.Entity; }
set { _parent.Entity = value; }
}
[Column(Name = "name", DbType = "varchar(100)", CanBeNull = false)]
public string Name { get; set; }
}
So what am I doing wrong? How to make it possible to insert a post that doesn't belong to any category? How to insert categories with no posts?
It seems that the error has nothing to do with mapping. Mapping is correct.
As I wrote, the first post got inserted without problems, and the rest failed to insert. After deleting it from the database, I still couldn't add posts. It became clear that it had nothing to do with either I had something in the DB or not, and only with the fact that I've made some changes to the code.
So what are the changes? In Apress "ASP.NET MVC Pro", the first example illustrated a way to validate data in an iterative way (non-declarative, using the facilities provided by IDataErrorInfo), to which I stuck. I done everything by that example, and the function call that should have validated the input screwed up my data flow, and threw that exception upon submitting to the database.
Removed that validation, and everything worked fine.
Sorry for the false alarms.