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.
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 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.
i have create a database on windows phone 7 platform. one of table's defined as follow.
[Table]
public class Playlist : BaseTable
{
// Define ID: private field, public property, and database column.
private int _id;
[Column(DbType = "INT NOT NULL IDENTITY", IsDbGenerated = false, CanBeNull=false, IsPrimaryKey = true)]
public int Id
{
get { return _id; }
set
{
NotifyPropertyChanging("PlaylistId");
_id = value;
NotifyPropertyChanged("PlaylistId");
}
}
// some other field
//.......
}
i don't want the field "id" is gererated by db, so the "IsDbGenerated = false", but i got an exception when insert one record:
db.Playlists.InsertOnSubmit(new Playlist { Id = (int)DefalutPlaylist.Default, Name = "default playlist", Group = 0, Type = 0 });
it said "The column cannot be modified[ Column name = id ]"
who can help me...
Drop "INDENTITY" value in DbType as follow:
[Column(DbType = "INT NOT NULL", IsDbGenerated = false, CanBeNull=false, IsPrimaryKey = true)]
Cheers
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.
I've got the following classes:
public class SupplierCategory : IEquatable<SupplierCategory>
{
public string Name { get; set; }
public string Parent { get; set; }
#region IEquatable<SupplierCategory> Members
public bool Equals(SupplierCategory other)
{
return this.Name == other.Name && this.Parent == other.Parent;
}
#endregion
}
public class CategoryPathComparer : IEqualityComparer<List<SupplierCategory>>
{
#region IEqualityComparer<List<SupplierCategory>> Members
public bool Equals(List<SupplierCategory> x, List<SupplierCategory> y)
{
return x.SequenceEqual(y);
}
public int GetHashCode(List<SupplierCategory> obj)
{
return obj.GetHashCode();
}
#endregion
}
And i'm using the following linq query:
CategoryPathComparer comparer = new CategoryPathComparer();
List<List<SupplierCategory>> categoryPaths = (from i in infoList
select
new List<SupplierCategory>() {
new SupplierCategory() { Name = i[3] },
new SupplierCategory() { Name = i[4], Parent = i[3] },
new SupplierCategory() { Name = i[5], Parent = i[4] }}).Distinct(comparer).ToList();
But the distinct does not do what I want it to do, as the following code demonstrates:
comp.Equals(categoryPaths[0], categoryPaths[1]); //returns True
Am I using this in a wrong way? why are they not compared as I intend them to?
Edit:
To demonstrate the the comparer does work, the following returns true as it should:
List<SupplierCategory> list1 = new List<SupplierCategory>() {
new SupplierCategory() { Name = "Cat1" },
new SupplierCategory() { Name = "Cat2", Parent = "Cat1" },
new SupplierCategory() { Name = "Cat3", Parent = "Cat2" }
};
List<SupplierCategory> list1 = new List<SupplierCategory>() {
new SupplierCategory() { Name = "Cat1" },
new SupplierCategory() { Name = "Cat2", Parent = "Cat1" },
new SupplierCategory() { Name = "Cat3", Parent = "Cat2" }
};
CategoryPathComparer comp = new CategoryPathComparer();
Console.WriteLine(comp.Equals(list1, list2).ToString());
Your problem is that you didn't implement IEqualityComparer correctly.
When you implement IEqualityComparer<T>, you must implement GetHashCode so that any two equal objects have the same hashcode.
Otherwise, you will get incorrect behavior, as you're seeing here.
You should implement GetHashCode as follows: (courtesy of this answer)
public int GetHashCode(List<SupplierCategory> obj) {
int hash = 17;
foreach(var value in obj)
hash = hash * 23 + obj.GetHashCode();
return hash;
}
You also need to override GetHashCode in SupplierCategory to be consistent. For example:
public override int GetHashCode() {
int hash = 17;
hash = hash * 23 + Name.GetHashCode();
hash = hash * 23 + Parent.GetHashCode();
return hash;
}
Finally, although you don't need to, you should probably override Equals in SupplierCategory and make it call the Equals method you implemented for IEquatable.
Actually, this issue is even covered in documentation:
http://msdn.microsoft.com/en-us/library/bb338049.aspx.