I currently have a MVC5 site with a TPH relationship with classes as follows:
public abstract class product{
public int productID {get;set;}
}
public class toy : product {
public virtual List<ChildComment> Comments {get;set;}
public virtual List<AdultComment> Comments {get;set;}
}
public class tool : product {
public virtual List<AdultComment> Comments {get;set;}
}
public class ChildComment {
public CommentID {get;set;}
public string commentText {get;set;}
public virtual product Product {get;set;}
}
public class AdultComment {
public CommentID {get;set;}
public string commentText {get;set;}
public virtual product Product {get;set;}
}
My Issue is when:
1) I am creating a new adult comment in the adult comment controller
2) I use db.Products.find(id) to add a product to the product virtual property of the comment
3) I go to the view of the Product I just added the comment to and see that there are 0 comments (lets say I tried to add a comment to a toy, but remember I didn't cast it as a toy when I added it to the virtual property)
4) When I go to the database, there are 3 key columns in the adultcomment table: one for product, one for toys, and one for tools. The correct id was placed in the product column and the others are null
Do I have to cast a product as either a toy or tool before adding it to the adultcomment's virtual property?
Why are there extra columns in the adultcomment table, is it possible to consolidate to one single id column (since after all, i have one products table in my tph), and should I do so if it is possible?
Add the foreignKey attribute to new Comment class
public class Comment
{
[Key]
public int CommentID { get; set; }
public string commentText { get; set; }
public int productID { get; set; }
[ForeignKey("productID")]
public virtual Product Product { get; set; }
}
so AdultComment now looks like this
public class AdultComment : Comment
{
}
you will have to add a unique Identifier when creating a new product despite database auto generating id
using (var context = new YOUR-CONTEXT())
{
var toy = new Toy
{
productID = 1, //Unique identifier
AdultComments = new List<AdultComment>()
{
new AdultComment { commentText = "Some comment" }
}
};
context.Products.Add(toys);
context.SaveChanges();
}
Related
I'm using entity framework code first approach
I have a class
public class Movie
{
public int Id { get; set; }
public string Title { get; set; }
public Person Director { get; set; }
public virtual ICollection<Person> Actors { get; set; }
}
and a class
public class Person
{
public int Id { get; set; }
public string Name { get; set; }
}
When the database is created I get one table Movies with Id, Title, Director_Id and a table Person with Id and Name.
I expect to have a table Movies_Persons with columns Movie_Id and Actor_Id
How can I achieve this?
Your Problem is, that you don`t tell the Person Class, that there can be multiple Movies per person.
So by adding the following line in your person class:
public virtual ICollection<Movie> Movies { get; set; }
Your entity knows that both your classes can have multiple references to the other class.
To fulfill this requirement Entity Framework will create a third table with Movie_ID and Person_ID.
If you want more informations just look for:
Entity Framework - Many to many relationship
or follow this link:
http://www.entityframeworktutorial.net/code-first/configure-many-to-many-relationship-in-code-first.aspx
You can check out the other articels on that page too, if you are new to entity framework.
UPDATE:
Sorry i missed, that you are already have another reference to your person table.
Here you have to tell your entity framework, which way you want to reference the two tables by fluent api.
Check out this stackoverflow answer. That should do the trick.
You have to insert this code into your OnModelCreating Function of your DbContext Class.
So your final code should look like this:
public class Movie
{
public int Id { get; set; }
public string Title { get; set; }
public virtual Person Director { get; set; }
public virtual ICollection<Person> Actors { get; set; }
}
public class Person
{
public int Id { get; set; }
public string Name { get; set; }
public virtual ICollection<Movie> Movies_Actors { get; set; }
public virtual ICollection<Movie> Movies_Directors { get; set; }
}
And in your OnModelCreating add following code:
modelBuilder.Entity<Movie>()
.HasMany(a => a.Actors)
.WithMany(a => a.Movies_Actors)
.Map(x =>
{
x.MapLeftKey("Movie_ID");
x.MapRightKey("Person_ID");
x.ToTable("Movie_Actor");
});
modelBuilder.Entity<Movie>()
.HasRequired<Person>(s => s.Director)
.WithMany(s => s.Movies_Directors);
I don't have the possibility to test the code, but that should do the trick.
If you have to do some adjustments to make it work, plz add them in the comments, so other ppl can benefit from it.
I want the following table structure:
Person
-person_id
Company
-company_id
Company_Person
-person_id
-company_id
-other_column
Location
-id
Currently my EF is resulting in a 'company_id' column in the Person table also as a FK.
My models look like:
public class Person
{
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int PersonId { get; set; }
public int LocationId {get;set;}
public virtual Location Location {get; set; }
public virtual ICollection<CompanyPerson> CompanyPersons {get; set;}
}
public class Company
{
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int CompanyId { get; set; }
public virtual ICollection<CompanyPerson> CompanyPersons {get; set;}
}
[Table("Company_Person")]
public class CompanyPerson
{
[Key, Column(Order = 0)]
public int PersonId { get; set; }
[Key, Column(Order = 1)]
public int CompanyId { get; set; }
public bool IsActive { get; set; }
public virtual Person Person { get; set; }
public virtual Company Company { get; set; }
}
public class Location
{
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int id {get;set;}
public virtual ICollection<Person> Persons {get;set;}
}
I followed the same pattern as in here: Create code first, many to many, with additional fields in association table
How can I get that extra CompanyId column from being generated in the Person table?
Update
Ok I figured it out, and it turns out it was another association that I didn't post (my bad once again).
In my Company model I had this which I commented out and it generated the correct table. I still want this association so can someone tell me what is why this is happening?
public class Company
{
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int CompanyId { get; set; }
public virtual ICollection<CompanyPerson> CompanyPersons {get; set;}
public virtual ICollection<History> Histories {get; set; }
}
public class History
{
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int Id { get; set; }
[ForeignKey("Company")]
public int CompanyId { get; set; }
public virtual Company Company { get; set; }
}
So when I commented out the everything in the History model except for the Id property, and the Company.History property it generated the table structure I was expecting.
I think that EF is treating your CompanyPerson property as a complex type, because essentially, it can't actually create a M2M relationship with what you've given it. Whether that's actually the problem or not, either way you'll need to fix your CompanyPerson properties to be:
public virtual ICollection<CompanyPerson> CompanyPersons { get; set; }
UPDATE
The oddest part is that your History class would perfectly explain the issue if it only actually was defined as:
public class History : Person
That's why I asked you about any subclasses of Person because EF's default behavior with inheritance is to use TPH (table per hierarchy). In other words, it will simply add all properties of all subclasses to the base class' table, instead of creating a table for each subclass. Plainly and simply, the only source of this column you aren't expecting is going to be one of either:
Company, or some subclass of Company has direct relationship to Person (not through CompanyPerson) and it's configured to be a one-to-one.
Person or some subclass of Person has a relationship to Company.
Ok I found the problem, and surprise surprise the real bug was with me!
In my Company table had this:
public virtual ICollection<Person> Histories { get; set; }
if you didn't catch that, the type should be History and not Person!
public virtual ICollection<History> Histories { get; set; }
Thanks for all that helped with this!
I have an order class with has a collection of products...
public class Order
{
int id { get; set; }
public virtual ICollection<Product> Products { get; set; }
}
public abstract class Product
{
int id { get; set; }
}
Derived from product, I have several different types of product...
public class ProductA : Product
{
public string CustomFieldA {get; set;}
}
public class ProductB : Product
{
public string CustomFieldB {get; set;}
}
I can add these derived products to the order and save them to the database...
var producta = new ProductA();
producta.CustomFieldA = "abc";
order.Products.Add(product);
order.SaveChanges();
This updates the database correctly.
What I can't then figure out is how I access this data!
Order.Products just contains the base class, how can I check which type of derived class each record in Order.Products is and access, for example, CustomFieldA of any that are ProductA.
I suspect either I'm missing something really obvious, or I've done the first part completely wrong!
using entity framework code first I have this :
public class Person {
public int PersonId {get;set;}
public string FirstName {get;set;}
public string LastName {get;set;}
public virtual List<Note> Notes {get;set;}
}
public class Product {
public int ProductId {get;set;}
public string Name {get;set;}
public virtual List<Note> Notes {get;set;}
}
public class Note {
public int NoteId {get;set;}
public string Body {get;set;}
public virtual User Author {get;set;}
// what should be here ?
}
How can I know if a note is from a Person or a Product ?
Should I need different classes (PersonNote, ProductNote) for that ?
Can I use a Interface approach ? like INoteable ?
I'm not sure what's the best strategy for the DB to be modeled or the classes.
Any suggestion is appreciated.
Thanks,
EDIT (based on answers)
the suggested answer means that for every entity that has notes, the table will have a new column which i don't like much. Is there any way to use a discriminator ? I don't like the idea of modifying the table everytime a new entity can have notes and potentially end up with like 10 FK (9 always null) if I have 10 entities that supports notes.
I ideally want to have something like an Interface (or whatever) so I can have in code, Product : INoteable and by that it means that a note can be created using this id.
Maybe with a discriminator column. Is that even possible ?
Sorry for not being clear the first time.
In real life scenario I have like : Products, Persons, Purchase Orders, Sales, Payments, Payment Orders, and a few more entities that I need to implement notes.
EDIT 2 :
What about this Database structure :
TABLE: Note
int PK NoteId
int FK NoteDataId
string Body
TABLE: Person
int PK PersonId
int FK NoteDataId
TABLE: Product
int PK ProductId
int FK NoteDataId
TABLE: NoteData
int PK NoteDataId
with this data structure, all entities that want to implement Notes I just add a navigation property NoteDataId and when creating notes, I just give the NoteDataId value. I think EF will take care of the creation of NoteDataId row if it doesn't exists.
EDIT 3:
Example data:
Person:
PersonId 1
Name Bart
NoteDataId 1
PersonId 2
Name Alex
NoteDataId 2
Product:
ProductId 1
Name "Dulce de Leche"
NoteDataId 3
NoteData:
NoteDataId 1
NoteDataId 2
NoteDataId 3
Note:
NoteId 1
NoteDataId 1
Body "first note"
NoteId 2
NoteDataId 1
Body "second note of Bart"
NoteId 3
NoteDataId 2
Body "first note of Alex"
NoteId 4
NoteDataId 3
Body "Note about Dulce de Leche"
how to get the notes of some person ?
SELECT * FROM Note
JOIN NoteData USING (NoteDataId)
JOIN Person USING (NoteDataId)
WHERE PersonId = 1
backwards ?
SELECT * FROM Note
JOIN NoteData USING (NoteDataId)
LEFT JOIN Person USING (NoteDataId) 'can be null, only one type exists'
LEFT JOIN Product using (NoteDataId) 'can be null, only one type exists'
WHERE NoteId = 2
Create nullable foreign keys:
public class Note {
public int NoteId {get;set;}
public string Body {get;set;}
public virtual User Author {get;set;}
public int? PersonId { get; set; }
public virtual Person Person { get; set; }
public int? ProductId { get; set; }
public virtual Product Product { get; set; }
}
You have to map the properties as optional.
Configuration for Note:
Property(n => n.PersonId).IsOptional();
Property(n => n.ProductId).IsOptional();
Configuration for Person and Product:
Property(p => p.Note).IsOptional();
Update:
You could also use the table-per-hierarchy solution.
Leave Note the same (might as well make it abstract) and create derived types. For example PersonNote:
public clas PersonNote : Note
{
public int PersonId { get; set; }
public virtual Person Person { get; set; }
}
Where Person gets a navigation property:
public virtual PersonNote Note { get; set; }
And ProductNote:
public clas ProductNote : Note
{
public int ProductId { get; set; }
public virtual Product Product { get; set; }
}
Where Product gets a navigation property:
public virtual ProductNote Note { get; set; }
Entity Framework will create one Note table containing all the properties of the derived types and a discriminator column. But this will result in a lot of classes in your code, but that's not a bad thing in my opinion.
Update 2
You could also let go of the navigation property from Note to a Product or Person if you don't need it. This will keep your code a lot simpler. You can leave Note as it is, and add navigation and foreign key properties to your other entities:
public class Person
{
// properties..
public int NoteId { get; set; }
public virtual Note Note { get; set; }
}
Edited: Thinking about it allover. This doesn't solve the first issue, we only added another table. Why don't you add a note type column that map to an enum.
According to your last edit, this is how I would do the code
public class Person {
public int PersonId {get;set;}
public string FirstName {get;set;}
public string LastName {get;set;}
public int NoteData NoteDataId{get;set;}
[ForeignKey("NoteDataId")]
public virtual NoteData NoteData{get;set;}
}
public class Product {
public int ProductId {get;set;}
public string Name {get;set;}
public int NoteData NoteDataId{get;set;}
[ForeignKey("NoteDataId")]
public virtual NoteData NoteData{get;set;}
}
public class NoteData {
public int NoteDataID {get;set;}
public virtual List<Note> Notes {get;set;}
}
public class Note {
public int NoteId {get;set;}
public string Body {get;set;}
public int NoteData NoteDataId{get;set;}
[ForeignKey("NoteDataId")]
public virtual NoteData NoteData{get;set;}
}
I'm not sure how it would work for EF, but in our model we use a generic "Parent" property that returns an object. This only works if you only have one object that owns the note and not many objects referencing the note.
public virtual object Parent { get; set; }
However, I don't think EF will automatically populate that for you...
I have a problem with a simple class. One property of my class is a reference to another class, but when I read, it's always null.
public class Product
{
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int Id { get; set; }
public String Description { get; set; }
public virtual Trademark Trademark { get; set; }
}
public class Trademark
{
public int TrademarkId { get; set; }
public String Description { get; set; }
}
Those are my classes, very simple.
Then when I get the first element:
Product p = context.Products.First();
And p contains the right product, but Trademark is null.
Even if I want to do a query using linq, like:
var prods = context.Products.Where(p => p.Trademark.TrademarkId == 1).ToList();
The database is generated Ok.
Using EF 4.3.1, with SqlServer compact edition 4.0
Thanks for any suggestions.
Add: This is my context class:
public class HPContext : DbContext
{
public HPContext()
: base()
{
this.Configuration.LazyLoadingEnabled = false; //Just for test
}
public DbSet<Product> Products { get; set; }
public DbSet<Trademark> Trademarks { get; set; }
}
Add: Database schema:
Table:
Products
Fields:
Id int primaryKey
Description nvarchar(4000)
Trademark_TrademarkId int
Table:
Trademarks
Fields:
TrademarkId int PrimaryKey
Description nvarchar(4000)
You shall use Include to include navigation properties if lazy loading is not enabled;
Product p = context.Products.Include("TradeMark").First();