I have a hierarchical relationship defined for one of my tables where the relationship is stored in a separate join table. The join table also contains information about the type of relationship. The (simplified) schema looks like:
Bills
ID int IDENTITY(1,1) NOT NULL (PK)
Code varchar(5) NOT NULL
Number varchar(5) NOT NULL
...
BillRelations
ID int IDENTITY(1,1) NOT NULL (PK)
BillID int NOT NULL
RelatedBillID int NOT NULL
Relationship int NOT NULL
...
I have FK relationships defined on BillID and RelatedBillID to ID in the Bills table.
I'm trying to map this in Entity Framework Code First with little success. My classes look like the following. Ignore the RelationshipWrapper stuff, it's a wrapper class around an enum named Relationship that corresponds to the value in the Relationship column.
public class Bill
{
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int ID { get; set; }
[Required]
[StringLength(5)]
public string Code { get; set; }
[Required]
[StringLength(5)]
public string Number { get; set; }
public virtual ICollection<BillRelation> RelatedBills { get; set; }
...
}
public class BillRelation
{
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int ID { get; set; }
public long BillID { get; set; }
[ForeignKey("BillID")]
public virtual Bill Bill { get; set; }
public long RelatedBillID { get; set; }
[ForeignKey("RelatedBillID")]
public virtual Bill RelatedBill { get; set; }
public RelationshipWrapper Relationship { get; set; }
...
}
I've tried this in various incarnations, both using explicitly defined relationships via the ModelBuilder on the DbContext and data annotations only, but the above defines what I'm looking for. I'm using collections because the the foreign key properties aren't primary keys.
Using this set up I get an error: Bill_ID column is not defined (or something similar).
If I got the ModelBuilder route using the following, I get an error that "The table BillRelations does not exist in the database."
modelBuilder.Entity<Bill>().HasMany( b => b.RelatedBills )
.WithRequired( r => r.Bill )
.Map( m => m.MapKey( "BillID" ).ToTable( "BillRelations" ) );
modelBuilder.Entity<BillRelation>().HasRequired( r => r.RelatedBill )
.WithRequiredDependent()
.Map( m => m.MapKey( "RemoteBillID" ).ToTable( "BillRelations" ) );
I have been able to make it work by defining only one half of the relationship, Bills -> BillRelations, then using a Join in my repository to fill in an [NotMapped] RelatedBill property on the BillRelations class for each of the related bills in the Bill's RelatedBills collection. I'd rather not do this if I can help it.
The only other solution I've thought of is to model each relationship in a separate table (there are 4 types) and use a standard Bill<->Bill mapping through the join table for each of the 4 relationship types -- again I'd rather not do this if I can avoid it.
Can anyone see what I'm doing wrong or tell me if what I want to do is even possible in EF Code First 4.1?
Just a few ideas:
Your mapping with data annotations doesn't work because EF Code First conventions don't recognize which navigation properties belong together. Obviously you want to associate Bill.RelatedBills with BillRelation.Bill. But because there is a second navigation property BillRelation.RelatedBill refering to the Bill entity as well the AssociationInverseDiscoveryConvention can't be applied to recognize the correct relation. This convention only works if you have exactly one pair of navigation properties on the entities. As a consequence, EF assumes actually three relationships, each with only one exposed end in the model. The relationship where Bill.RelatedBills belongs to assumes a not exposed foreign key on the other side according to EF default naming conventions - which is Bill_ID with underscore. It doesn't exist in the database, hence the exception.
In your Fluent API mapping I would just try to remove ...ToTable(...) altogether. I believe that it is not necessary as the mapping knows anyway which table the foreign key belongs to. This will possibly fix the second error ("Table ... does not exist....")
Your second mapping - the one-to-one relationship - does possibly not work as expected because one-to-one relationships are "usually" mapped with a shared primary key association between the tables in the database. (I am not sure though if shared primary keys are really required by EF.) Because your BillId column seems to be a foreign key which is not a primary key at the same time I would try to map the relationship as one-to-many. Moreover because your foreign key columns are exposed as properties in the model you should use HasForeignKey instead of MapKey:
modelBuilder.Entity<Bill>()
.HasMany( b => b.RelatedBills )
.WithRequired( r => r.Bill )
.HasForeignKey ( r => r.BillID );
// turn off/on cascade delete by chaining
// .WillCascadeOnDelete(true/false)
// here at the end
modelBuilder.Entity<BillRelation>()
.HasRequired( r => r.RelatedBill )
.WithMany()
.HasForeignKey ( r => r.RelatedBillID );
// turn off/on cascade delete by chaining
// .WillCascadeOnDelete(true/false)
// here at the end
Edit
It is possible that the whole Fluent mapping is not necessary if you put the [InverseProperty] attribute on one of the navigation properties - for example in your Bill class:
[InverseProperty("Bill")]
public virtual ICollection<BillRelation> RelatedBills { get; set; }
In this attribute you specify the name of the associated navigation property on the related entity. This binds Bill.RelatedBills and BillRelation.Bill together to a pair of navigation properties being the ends of the same association. I hope that EF will do the correct thing with the remaining navigation property BillRelation.RelatedBill, i.e. create a one-to-many relationship - I hope... If the default cascading delete won't work for you, you are forced to use Fluent API though since there is no data annotation attribute to configure cascading delete.
Related
I’m using MySQL 5.5.37, JPA 2.0, and Hibernate 4.1.0.Final (I’m willing to upgrade if it solves my problem). I have the following entity …
#Entity
#Table(name = "url")
public class Url implements Serializable
{
…
#ElementCollection(fetch=FetchType.EAGER)
#MapKeyColumn(name="property_name")
#Column(name="property_value")
#CollectionTable(name="url_property", joinColumns=#JoinColumn(name="url_id"))
private Map<String,String> properties;
The “url_property” table has an ID (primary key) column, and perhaps for this reason, when I create a new Url entity with multiple properties, I feet the exception
[ERROR]: org.hibernate.engine.jdbc.spi.SqlExceptionHelper - Duplicate entry '' for key 'PRIMARY'
upon saving. Does anyone know what I have to do to auto-generate IDs for my url_property table? I would prefer not to write a trigger, but rather do something JPA, or at least, Hibernate sanctioned.
Edit: Per the first suggestion in the answer, I tried
#ElementCollection(fetch=FetchType.EAGER)
#Column(name="property_value")
#CollectionTable(name="url_property", joinColumns=#JoinColumn(name="url_id"))
private Set<UrlProperty> properties;
but it resulted in the exception, "org.hibernate.MappingException: Foreign key (FK24E4A95BB0648B:url_property [properties_id])) must have same number of columns as the referenced primary key (url_property [url_id,properties_id])".
My UrlProperty entity is
#Entity
#Table(name = "url_property")
public class UrlProperty
{
#Id
#GeneratedValue(generator = "uuid-strategy")
private String id;
#ManyToOne(fetch=FetchType.EAGER)
#JoinColumn(name="URL_ID")
private SubdomainUrl url;
#Column(name="PROPERTY_NAME")
private String propertyName;
#Column(name="PROPERTY_VALUE")
private String propertyValue;
You have only told JPA about 3 fields in the table ("property_name","property_value" and "url_id"), so it has no way of knowing about the 4th field used as the pk. Since it is not an entity, it doesn't have an Identity that is maintained. Options are:
1) Map the "url_property" table to a Property entity, which would have an ID, value and reference to the Url. The Url would then have a 1:M reference to the Property class, and can still be keyed on the name. http://wiki.eclipse.org/EclipseLink/Examples/JPA/2.0/MapKeyColumns has an example
2) Change your table to remove the ID field, and instead use "property_name","property_value" and "url_id" as the primary key.
3) Set a trigger to populate the ID. Doesn't seem useful though since the application is never aware of the field anyway.
I know a lot of people is asking about one-to-one relationships on EF 4.1 but I can't seem to find an answer for this problem.
I have these POCO classes:
public class Contact
{
public decimal nCont_id { get; set; }
public virtual WebsiteMembership WebsiteMembership { get; set; }
//a bunch of other properties
}
public class WebsiteMembership
{
public int WebsiteMembershipId { get; set; }
public virtual Contact Contact { get; set; }
}
Website membership's primary key (WebsiteMembershipId) is also a foreign key that references Contact's nCont_id.
I'm trying to set this up using the fluent API:
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
//This is being done because of the unconventional column names I have to live with
modelBuilder.Conventions.Remove<NavigationPropertyNameForeignKeyDiscoveryConvention>();
//bunch of other stuff
modelBuilder.Entity<Contact>().ToTable("contacts");
modelBuilder.Entity<Contact>().HasKey(b => b.nCont_id);
modelBuilder.Entity<Contact>().HasOptional(b => b.WebsiteMembership).WithRequired(b => b.Contact).Map(b => b.MapKey("WebsiteMembershipId"));
modelBuilder.Entity<WebsiteMembership>().ToTable("WebsiteMembership");
modelBuilder.Entity<WebsiteMembership>().HasKey(b => b.WebsiteMembershipId);
}
But I get an exception when I try doing:
var websiteMembership = context.WebsiteMemberships.Where(c => c.WebsiteMembershipId == 1645942).FirstOrDefault();
Exception:
Schema specified is not valid. Errors: (263,6) : error 0019: Each
property name in a type must be unique. Property name
'WebsiteMembershipId' was already defined.
Does this mean I can't set the same primary key as a foreign key on EF CodeFirst? Or I'm doing something wrong here? Would it work if the primary key wasn't the same column as the foreign key?
Advice is very much appreciated!
I think when you define this mapping...
modelBuilder.Entity<Contact>()
.HasOptional(b => b.WebsiteMembership)
.WithRequired(b => b.Contact)
.Map(b => b.MapKey("WebsiteMembershipId"));
...you implicitely define what's the principal and what's the dependent of the relationship. Obviously Contact is the principal because having the WebsiteMembership property set is optional for the Contact, it can be stored without the WebsiteMembership. The WebsiteMembership entity on the other side has a required reference to a Contact which means it depends on the Contact.
The principal (Contact) is the entity with the primary key, the dependent (WebsiteMembership) the entity with the foreign key. So, when you use MapKey to set the name WebsiteMembershipId of the foreign key column you define a column for the table WebsiteMembership, not for Contact. But WebsiteMembership already has a property called WebsiteMembershipId. (I believe that's the point where EF complains that a "Property name 'WebsiteMembershipId' was already defined.) In a one-to-many relationship the foreign key would be the ContactId as a separate property/column and you would have to use HasForeignKey instead of MapKey. But in a one-to-one relationship it is clear that the foreign key is the primary key at the same time, so you don't need to define the foreign key at all.
To cut a long story short: Just remove MapKey:
modelBuilder.Entity<Contact>()
.HasOptional(b => b.WebsiteMembership)
.WithRequired(b => b.Contact);
(But now I am curious if you can define a relationship between an int and a decimal property or if you get the next error.)
I'm using EF4.1 with Code first and TPT (Table per Type) inheritance. I have a structure like this
public class Customer
{
public virtual ICollection<Product> Products {get; set;}
}
public class Product
{
[Required]
public int Id { get; set; }
[Required]
public virtual Customer {get; set;}
public decimal Price { get; set; }
}
public class SpecializedProduct : Product
{
public string SpecialAttribute { get; set; }
}
when i delete a customer i want all the products associated with that customer to be deleted. I can specify a WillCascadeOnDelete(true) between the Customer and the Product:
modelBuilder.Entity<Customer>().HasMany(e => e.Products).WithRequired(p => p.Customer).WillCascadeOnDelete(true);
but since there's a foreighn key relationship between SpecializedProduct and Product i get an Exception when I try to delete the Customer:
The DELETE statement conflicted with the REFERENCE constraint "SpecializedProduct _TypeConstraint_From_Product_To_SpecializedProduct". The conflict occurred in database "Test", table "dbo.SpecializedProduct", column 'Id'. The statement has been terminated.
If i manually set a on delete cascade on the SpecializedProduct _TypeConstraint_From_Product_To_SpecializedProduct constraint it works, but i would like to be able to specify this using the modelbuilder or some other way in code. Is this possible?
Thanks in advance!
Best Regards
Simon
When it comes to database, a TPT inheritance is implemented with a Shared Primary Key Association between the base class (e.g. Product) and all the derived classes (e.g. SpecializedProduct). Now, when you delete a Customer object without fetching its Products property, EF has no idea that this Customer has a bunch of products that also needs to be deleted as per your requirement. If you enable cascade deletes by marking your customer-product association as required, then database will take care of deleting the child record(s) from the product table but if this child record is a SpecializedProduct then the related row on the SpecializedProduct won't get deleted and hence the exception that you are getting. So basically the following code won't work:
// This works only if customer's products are not SpecializedProduct
Customer customer = context.Customers.Single(c => c.CustomerId == 1);
context.Customers.Remove(customer);
context.SaveChanges();
This code will cause EF to submit the following SQL to the database:
exec sp_executesql N'delete [dbo].[Customer] where ([CustomerId] = #0)',N'#0 int',#0=1
That said, There is no way to enable the cascade deletes between Product and SpecializedProduct tables, that's just how EF Code First implements a TPT inheritance and you cannot override it.
So what's the solution?
One way is what you already figured out, manually switching the cascades on between Product and SpecializedProduct tables to avoid the exception when you deleting a customer with SpecializedProducts.
The second way is to let EF take care of the customer's SpecializedProducts when you removing the customer. Like I said before, this happens because the Customer object has not been properly fetched, and EF has no knowledge of customer's SpecializedProducts which means by fetching the customer object properly, Ef will start tracking the customer's associations and will submit necessary SQL statements to make sure that every related record is removed before removing the customer:
Customer customer = context.Customers
.Include(c => c.Products)
.Single(c => c.CustomerId == 1);
context.Customers.Remove(customer);
context.SaveChanges();
As a result, EF will submit the following SQL statements to the database which perfectly removes everything in order:
exec sp_executesql N'delete [dbo].[SpecializedProduct] where ([Id] = #0)',N'#0 int',#0=1
exec sp_executesql N'delete [dbo].[Product] where (([Id] = #0) and ([Customer_CustomerId] = #1))',N'#0 int,#1 int',#0=1,#1=1
exec sp_executesql N'delete [dbo].[Customer] where ([CustomerId] = #0)',N'#0 int',#0=1
I am new to Entity Framework and hence this question may seem a little noobish.
I will try to explain my scenario with he Department-Employee example I have two tables "Department" and "Employee". Department has an identity column DeptID. I am trying to create a new Department and add newly created Employees to it all in one go. Below is my code:
using (MyDB context = new MyDB())
{
Department dept = new Department();
dept.Name = "My Department";
Employee emp = new Employee();
emp.Name = "Emp Name";
emp.Department = dept; //Tried dept.Employees.Add(emp) also, same result
context.AddObject("Department", dept);
context.SaveChanges()
}
But for some reason, the record doesn't get inserted. It throws an error in the second insert query.
Below are the queries:
INSERT INTO Department
(Name)
VALUES ('Dept1' /* #gp1 */);
SELECT ID
FROM Department
WHERE row_count() > 0
AND `ID` = last_insert_id()
--------------------------
INSERT INTO Employee
(DeptID,
Name)
VALUES (19,
'Name'); /* #gp1 */
SELECT id
FROM Employee
WHERE row_count() > 0
AND `id` = last_insert_id()
The error it throws is at line 4 of second query. So I am guessing something is wrong with the Identity thing. I am using MySQL.
Can anyone please explain what could be wrong?
EDIT: I have modified the SQL to suit this example. I can't give my real table details.
What is the structure of your classes? I'd assume there is something slightly wrong and EF isn't correctly building the model.
Also I had some problems with EF4.1 until I manually defined the key. The part of EF that 'assumes' which variable is your key doesn't seem to work on some complex objects like objects that are derived from a base class and also fails in other cases.
Here is what I would expect your code to look like:
public class Department
{
[Key]
public Int64 DepartmentId { get; set;}
public String Name { get; set;}
}
public class Employee
{
[Key]
public Int64 EmployeeId { get; set;}
public String Name { get; set;}
//Adding virtual here allows lazy loading of department
public virtual Department Department {get; set;}
}
public class MyDatabase : DbContext
{
DbSet<Department> Departments;
DbSet<Employee> Employees;
}
I have an entire project at work that relies on Entity framework correctly mapping the above into a Many to One relationship and I've had no issues using code just as shown.
'Name' /* #gp1 */
is missing a close parenthesis. i've added it here.
'Name' ) /* #gp1 */
PetaPoco has introduced Multi-POCO queries in experimental form (for now). As their blog post suggests and the code it provides this looks nice and all in One-to-One relations when we load multi POCOs per row as long as they don't repeat over the records.
What happens when at least one side is many relation? Actually example code is Many-to-One relational data.
Example code is clearly a Many-to-One relation. I haven't tested any PetaPoco code but what does the provided code on the blog post do? Does every Article have their own User object instance even though some may be the same user or do they share the same user object instance?
And what about other Many relation types? How do they work of they work at all?
Usually I map these one-to-many queries myself like the following example.
[TableName("Blogs"), PrimaryKey("BlogId")]
public class Blog {
public int BlogId {get;set;}
public string Title {get;set;}
[Ignore]
public IList<Post> Posts {get;set;}
}
[TableName("Posts"), PrimaryKey("PostId")]
public class Post {
public int PostId {get;set;}
public int BlogId {get;set;}
public string Subject {get;set;}
public string Content {get;set;}
}
public class FlatBlogPost {
public int BlogId {get;set;}
public string Title {get;set;}
public int PostId {get;set;}
public string Subject {get;set;}
public string Content {get;set;}
}
There are two ways I could display a list of posts for one blog or without too much work, all blogs.
1.Two queries -
var Blog = Db.Query<Blog>(1);
var Posts = Db.Query<Post>("where BlogId = #0", 1);
2.One query =
var flat = Db.Query<FlatBlogPost>("select b.blogid, b.title, p.postid, p.subject,
p.content from blogs b inner join posts p on b.blogid = p.blogid where
b.blogid = #0", 1);
var blog = flat
.GroupBy(x=> new { x.BlogId, x.Title })
.Select(x=> new Blog {
BlogId = x.Key.BlogId,
Title = x.Key.Title,
Posts = x.Select(y=> new Post{
PostId = y.PostId,
BlogId = x.Key.BlogId,
Subject = y.Subject,
Content = y.Content
}).ToList()
});
However usually in number 2 I would map directly from the FlatBlogPost object to my viewmodel for which I need to display the data.
Update
Check out these helpers which extend PetaPoco to support basic One-to-Many and Many-to-One queries. schotime.net/blog/index.php/2011/08/21/petapoco-one-to-many-and-many-to-one/ https://schotime.wordpress.com/2011/08/21/petapoco-one-to-many-and-many-to-one/
My 'One to Many' recipe for Petapoco is below. The docs are not clear enough for me. Create a db connection in Linqpad, it will show you all Navigation properties you can add to generated Petapoco poco classes. Execute the same SQL in Linqpad, to make sure it gets the data you expect.
// subclass the generated Parent table pocos, add navigation prop for children
[ResultColumn] public List<DecoratedChild> Child { get; set; }
// subclass the generated Child table pocos, add navigation prop for parent
[ResultColumn] public DecoratedParent Parent { get; set; }
// to get children with parent info
List<DecoratedChild> children = db.Fetch<DecoratedChild, DecoratedParent>(SELECT child.*, parent.* from ...)
// to get children with parent info, using PetapocoRelationExtensions
List<Child> children = db.FetchManyToOne<Child, Parent>(child => child.ID, "select child.*, parent.* from ...
// to get parents with children info, using PetapocoRelationExtensions
List<Parent> parents = db.FetchOneToMany<Parent, Child>(par => par.ID, child => child.ID != int.MinValue, "select parent.*, child.* from ...
SQL select order important, same as in Fetch types list !!!
navigation props will have parent or children data ...
with 3 levels the call will be like:
List<DecoratedGrandChild> grandChildColl = db.Fetch<DecoratedGrandChild, DecoratedChild, DecoratedParent>(SELECT grandch.* , child.*, parent.* from ...)
Personally I don't think you can avoid another database call to get the comments. You could get a list of all comments for the 10 articles (in the same order the articles are stored) by using an IN clause, and loop through them adding them to each article.comments as you go along and the comment.articleid changes. The only way I can see getting this information in a single sql call would be to use a join but then you'd get duplicate article details for each comment, so maybe this isn't a problem with petapoco, just one of those things that'll never be perfect