Context
We have Users and Invites. A User can have many associated Invites, an Invite is associated with one user.
I'm representing this via a column usersk on the invite table that corresponds to the psk primary key column on the user table.
There is also a concept of a Match, but that is only relevant to this problem to the extent that an Invite's primary key is a composite of its User foreign key and its Match foreign key.
I want to end up with:
User has a property that is a collection of Invites they're involved in
Invite has a property that is the User it's associated with
At a high level, my approach is creating a Set of the Invites tied to a User via a OneToMany relationship, and then having a ManyToOne mapping from the Invite to the User.
The Error
Here's my ManyToOne setup:
ManyToOne(x => x.User, map => {
map.PropertyRef("PSK");
});
Throws this error:
{"ERROR: 42703: column invit0_.user does not exist"}
Well that's correct, because there's no user column. But trying to specify what column to use gives me a different error:
ManyToOne(x => x.User, map => {
map.PropertyRef("PSK");
map.Column("usersk");
});
throws an error:
{"Error performing LoadByUniqueKey[SQL: SQL not available]"}
due to inner exception:
{"The given key was not present in the dictionary."}
Tools
MVC4 WebApi, NHibernate Conformist ByCode mapping
Code
If I just try to get the first goal accomplished, creating a Set of Invites for a property on the User, things work swimmingly. That code is below:
User.cs
public class User {
public User() { }
public virtual int PSK { get; set; }
public virtual ICollection<Invite> Invites { get; set; }
}
UserMap.cs
public class UserMap : ClassMapping<User>
{
public UserMap()
{
Lazy(true);
Schema("public");
Table("user");
Id(x => x.PSK, map =>
{
map.Column("psk");
map.Generator(Generators.Sequence, g => g.Params(new {sequence = "user_psk_seq"}));
});
Set(x => x.Invites,
mapping =>
{
mapping.Key(k =>
{
k.Column("usersk");
});
mapping.Inverse(true);
},
r => r.OneToMany());
}
}
Invite.cs
public class Invite {
public virtual int MatchSK { get; set; }
public virtual int UserSK { get; set; }
public virtual User User { get; set; }
#region NHibernate Composite Key Requirements
public override bool Equals(object obj) {
if (obj == null) return false;
var t = obj as Invite;
if (t == null) return false;
if (MatchSK == t.MatchSK
&& UserSK == t.UserSK)
return true;
return false;
}
public override int GetHashCode() {
int hash = GetType().GetHashCode();
hash = (hash * CONSTANT) ^ MatchSK.GetHashCode();
hash = (hash * CONSTANT) ^ UserSK.GetHashCode();
return hash;
}
#endregion
}
InviteMap.cs
public class InviteMap : ClassMapping<Invite> {
public InviteMap() {
Lazy(true);
Schema("public");
Table("invite");
ComposedId(compId =>
{
compId.Property(x => x.MatchSK, m => m.Column("matchsk"));
compId.Property(x => x.UserSK, m => m.Column("usersk"));
});
ManyToOne(x => x.User, map => {
map.PropertyRef("PSK");
});
}
}
With this in place, fetching a User gives me a populated Invites collection exactly as expected, provided I comment out the ManyToOne code in InviteMap.
Conclusion
Help? I'm really hoping that this is some obvious conceptual issue I've missed. But... feel free to disappoint me on that front.
Thanks for your time.
As we found by discussing the whole topic on question's comments, the problem was so simple: it's about using .Column(...) instead of .PropertyRef(...) in your many-to-one mapping.
Related
I have 3 tables: users, posts, and likes. I want to do a lambda expression to return an array that holds username, postText, and liked (true or false)
var myList = _context.Posts.Join(_context.Users,
post => post.UserID_FK,
user => user.ID,
(post, user) => new { Post = post, User = user })
.Join(
_context.Likes,
u => u.User.ID,
likes => likes.UserID,
(u, likes) => new PostDTO
{
ID = u.Post.ID,
username = u.Patient.UserName,
Text = u.Post.Text,
Likes = u.Post.Likes,
liked = (likes.PostID == u.Post.ID && likes.UserID == userModel.ID)}
.OrderByDescending(d => d.Date);
return myList;
My problem is with my code I am getting everything I want, but i am getting repeated records. I am trying to understand why I am getting duplicate records? I have searched lambda expressions and I can not figure out where my issue is.
I thank you guys in advance!
This occurs because like with an SQL query joining tables you are building a Cartesian for all combinations. As a simple example of a Post with 2 Likes where we run an SQL statement with an inner join:
SELECT p.PostId, l.LikeId FROM Posts p INNER JOIN Likes l ON p.PostId == l.PostId WHERE p.PostId = 1
we would get back:
PostId LikeId
1 1
1 2
Obviously, returning more data from Post and Like to populate a meaningful view, but the raw data will look to have a lot of duplication. This compounds with the more 1-to-many relationships, so you need to manually interpret the data to resolve that you are getting back one post with two Likes.
When you utilize navigation properties correctly to map the relationships between your entities, EF will do the heavy lifting to turn these Cartesian combinations back into a meaningful model structure.
For example, take the following model structure:
public class Post
{
public int PostId { get; set; }
public DateTime Date { get; set; }
// ... Other Post-related data fields.
public virtual User Patient { get; set; }
public virtual ICollection<Like> Likes { get; set; } = new List<Like>();
}
public class Like
{
public int LikeId { get; set; }
public virtual Post Post { get; set; }
public virtual User User { get; set; }
}
public class User
{
public int UserId { get; set; }
public string UserName { get; set; }
}
EF can likely work out all of these relationships automatically via convention, but I recommend being familiar with explicit configuration because there is always a time when convention fails in a particular situation where you want or need to use a different naming convention. There are four options for resolving relationships in EF: Convention, Attributes, EntityTypeConfiguration, and using OnModelCreating's modelBuilder. The below example outlines EntityTypeConfiguration via EF6. EF Code uses an interface called IEntityTypeConfiguration which works in a similar way but you implement a method for the configuration rather than call base-class methods in a constructor. Attributes and Convention are generally the least amount of work, but I typically run into situations they don't map. (They do seem to be more reliable with EF Core) Configuring via modelBuilder is an option for smaller applications but it gets cluttered very quicky.
public class PostConfiguration : EntityTypeConfiguration<Post>
{
public PostConfiguration()
{
ToTable("Posts");
HasKey(x => x.PostId)
.Property(x => x.PostId)
.HasDatabaseGeneratedOption(DatabaseGeneratedOption.Identity);
HasRequired(x => x.Patient)
.WithMany()
.Map(x => x.MapKey("PatientUserId"));
HasMany(x => x.Likes)
.WithRequired(x => x.Post)
.Map(x => x.MapKey("PostId"));
}
}
public class UserConfiguration : EntityTypeConfiguration<User>
{
public UserConfiguration()
{
ToTable("Users");
HasKey(x => x.UserId)
.Property(x => x.UserId)
.HasDatabaseGeneratedOption(DatabaseGeneratedOption.Identity);
}
}
public class LikeConfiguration : EntityTypeConfiguration<Like>
{
public LikeConfiguration()
{
ToTable("Likes");
HasKey(x => x.LikeId)
.Property(x => x.LikeId)
.HasDatabaseGeneratedOption(DatabaseGeneratedOption.Identity);
HasRequired(x => x.User)
.WithMany()
.Map(x => x.MapKey("UserId"));
}
}
// In your DbContext
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
modelBuilder.Configurations.AddFromAssembly(GetType().Assembly);
}
Now when you want to query your data, EF will manage the FK relationships and resolving any Joins and resulting Cartesian products. The question just becomes what data do you want out of the domain model?
var viewData = _context.Posts
.Select(p => new PostDTO
{
ID = p.PostID,
UserName = p.Patient.UserName,
Text = p.Text,
Likes = p.Likes
.Select(l => new LikeDTO
{
ID = l.LikeId
UserName = l.User.UserName
}).ToList(),
Liked = p.Likes.Any(l => l.User.UserId == userModel.UserId)
}).OrderByDescending(p => p.Date)
.ToList();
This is a rough guess based on your original query where you want the posts, patient name, likes, and an indicator whether the current user liked the post or not. Note there are no explicit joins or even eager loading. (Include) EF will build the necessary statement with just the columns you need to populate your DTOs and the IDs necessary to relate the data.
You should also avoid mixing DTOs with Entities when returning data. In the above example I introduced a DTO for the Like as well as the Post since we want to return some detail about the Likes from our domain. We don't pass back references to Entities because when these get serialized, the serializer would try and touch each property which can cause lazy loading to get triggered and overall would return more information than our consumer needs or should potentially see. With the relationships mapped and expressed through navigation properties EF will build a query with the required joins automatically and work through the returned data to populate what you expect to see.
This may seem like a duplicate question EF Core One-to-Many relationship list returns null, but the answer to that question didn't help me. My situation:
public class Section
{
public int Id { get; set; }
// ...
public IEnumerable<Topic> Topics { get; set; }
}
public class Topic
{
public int Id { get; set; }
// ...
public int SectionId { get; set; }
public Section Section { get; set; }
}
But I have not implemented the OnModelCreating method in DbContext because in that case, errors occurs with users identity. There are topics in the database with the specified SectionId. But no matter how I try to get the section, I get null in the Topics property. For example:
var section = _dbContext.Sections.Include(s => s.Topics).FirstOrDefault(s => s.Id == id);
What is the reason for this problem? Have I declared something wrong? Or maybe there is a problem in creating a topic?
UPDATE
I tried to override the OnModelCreating method this way:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Section>()
.HasMany(s => s.Topics)
.WithOne(t => t.Section);
base.OnModelCreating(modelBuilder);
}
And this way:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Section>()
.HasMany(s => s.Topics)
.WithOne(t => t.Section)
.HasForeignKey(prop => prop.SectionId);
base.OnModelCreating(modelBuilder);
}
I also added the virtual attribute to the dependencies again. It did not help. Added a test migration (thought there might be something wrong with the database structure), but the migration was empty.
SOLUTION
As a result, I solved the problem with a crutch:
var section = _dbContext.Sections.Include(s => s.Topics).FirstOrDefault(s => s.Id == id);
if (section == null)
{
return Error();
}
section.Topics = _dbContext.Topics.Where(t => t.SectionId == section.Id).Include(t => t.Author).ToList();
foreach(var topic in section.Topics)
{
topic.Author = _dbContext.Users.FirstOrDefault(u => u.Id == topic.AuthorId);
topic.Posts = _dbContext.Posts.Where(t => t.TopicId == topic.Id).ToList();
}
As you can see, I had to explicitly get data from the dbContext and assign them to the appropriate properties. Include method calls can be deleted because they do not perform the desired action.
Several suggestions:
Try to make your Section define Topics as ICollection rather than IEnumerable and virtual so that they can be lazy loaded if necessary
public class Section
{
public int Id { get; set; }
// ...
public virtual ICollection<Topic> Topics { get; set; }
}
In your solution sample, you should be able to leverage EF Core's ThenInclude method to get the Sections, Topics, and Authors in one fell swoop:
var section = _dbContext.Sections
.Include(s => s.Topics)
.ThenInclude(t => t.Authors)
.FirstOrDefault(s => s.Id == id);
However to get the collection of authors and posts in the same child collection, you may want to consider a custom projection into a new type. EF Core 3.1 should piece all of this into a single query. Earlier versions of Core would break it apart into multiple database queries depending on the version and exact syntax you used. Something like:
var items =
from section in _dbContext.Sections
from topic in section.Topics
select new {
section.Name,
topic.Description,
Authors = topic.Authors.ToList(),
Posts = topic.Posts.ToList()
};
Following the guide on this link https://www.learnentityframeworkcore.com/lazy-loading
Install the Microsoft.EntityFrameworkCore.Abstractions package into the project containing your model classes:
[Package Manager Console]
install-package Microsoft.EntityFrameworkCore.Abstractions
[Dotnet CLI]
add package Microsoft.EntityFrameworkCore.Abstractions
Alter the principal entity to include
a using directive for Microsoft.EntityFrameworkCore.Infrastructure
a field for the ILazyLoader instance
an empty constructor, and one that takes an ILazyLoader as a parameter (which can be private, if you prefer)
a field for the collection navigation property
a getter in the public property that uses the ILazyLoader.Load method
using Microsoft.EntityFrameworkCore.Infrastructure;
public class Author
{
private readonly ILazyLoader _lazyLoader;
public Author()
{
}
public Author(ILazyLoader lazyLoader)
{
_lazyLoader = lazyLoader;
}
private List<Book> _books;
public int AuthorId { get; set; }
public List<Book> Books
{
get => _lazyLoader.Load(this, ref _books);
set => _books = value;
}
}
The solution in the answer will not work in case of many-to-many relationship.
A little context:
I have many instances of SomeCollection which store many Rule information that needs to be persisted to the database. Each Rule in the database have an auto incremented ID (PK) and a custom RuleId. The Rule table also contaisn a foreign key, which is the ID of the collection. The Rule table contains a unique constraint on both RuleId and CollectionId.
Question:
How are foreign keys inserted into tables when mapped with HasMany, is it updated after the child is inserted? I ask as null is shown in the messages below, but I know the values are set
If the above is true or false, how can I enforce multi-column unique constraint and avoid the issue detailed below.
When I set enforce the unique constraint in SomeCollectionMapping, I get the following violation:
FCE detected: System.Data.SqlClient.SqlException (0x80131904):
Violation of UNIQUE KEY constraint 'UQ__supporte__4D7ABD34DC8C8A20'.
Cannot insert duplicate key in object 'dbo.rules'. The
duplicate key value is (123, NULL).
The data is try inserting is:
123, myId1
123, myId2
If I omit the lines: part.Not.Nullable() and part.UniqueKey("uniqueRef") the data is successfully inserted into both tables, but doesn't enforce uniqueness.
Even though the insert using Fluent NHibernate fails, I'm able to insert data using Microsoft SQl Server Management Studio without issues, and the multi-column constraint is working fine.
SomeCollection.cs
internal class SomeCollection
{
public SomeCollection()
{
this.CollectionId = string.Empty;
this.CollectionName = string.Empty;
this.Rules = new List<Rule>();
}
public virtual string CollectionId { get; set; }
public virtual string CollectionName { get; set; }
public virtual IList<Rule> Rules { get; set; }
}
Rule.cs
internal class Rule
{
public Rule()
{
this.DisplayName = string.Empty;
this.RuleId = string.Empty;
this.MyValue = 0;
}
public virtual string DisplayName { get; set; }
public virtual string RuleId { get; set; }
public virtual int MyValue { get; set; }
}
SomeCollectionMapping.cs
internal sealed class SomeCollectionMapping : ClassMap<SomeCollection>
{
private const string TableName = "some_collections";
public SomeCollectionMapping()
{
this.Table(TableName);
this.Id(x => x.CollectionId)
.Not.Nullable()
.Column("collectionId")
.UniqueKey("abc123");
this.Map(x => x.CollectionName)
.Column("name")
.Not.Nullable();
this.HasMany<Rule>(x => x.Rules)
.KeyColumns.Add("collectionId", part =>
{
// No violations if I omit these two lines
part.Not.Nullable();
part.UniqueKey("uniqueRef");
})
.Cascade.All();
}
}
RuleMapping.cs
internal sealed class RuleMapping : ClassMap<Rule>
{
private const string TableName = "rules";
public RuleMapping()
{
this.Table(TableName);
this.Id().GeneratedBy.Increment().Unique();
this.Map(x => x.RuleId)
.Not.Nullable()
.UniqueKey("uniqueRef")
.Column("ruleId");
this.Map(x => x.DisplayName)
.Column("name")
.Not.Nullable();
this.Map(x => x.MyValue)
.Column("myValue")
.Not
.Nullable();
}
}
Nhibernate 4.1.1
FluentNHibernate 2.0.3.0
SQLServer
Right.
After randomly trying things, i added Not.KeyNullable() to the HasMany chain. The data is now correctly inserted and no exceptions are thrown. The unique and not null constraints are working fine.
This appears to be a fix, but if someone else wants to answer with more detail, I will gladly receive and accept those instead.
While I am trying to delete my entity I am getting following error in my function;
A relationship from the 'ProjectWebsiteTag_ProjectUser' AssociationSet is
in the 'Deleted' state.
Given multiplicity constraints, a corresponding 'ProjectWebsiteTag_ProjectUser_Target' must also in the 'Deleted' state.
Here is my code to delete;
public bool Delete(int id)
{
try
{
using (ProjectDataContext context = new ProjectDataContext())
{
ProjectWebsiteTag websiteTag = context.WebsiteTags.FirstOrDefault(p => p.WebsiteTagId == id);
context.WebsiteTags.Remove(websiteTag);
int saveChanges = context.SaveChanges();
return saveChanges > 0;
}
}
catch (DbEntityValidationException e)
{
FormattedDbEntityValidationException newException = new FormattedDbEntityValidationException(e);
throw newException;
}
}
Here is my data class;
public class ProjectWebsiteTag
{
public int WebsiteTagId { get; set; }
public ProjectUser ProjectUser { get; set; }
public ProjectWebsite ProjectWebsite { get; set; }
}
My Config Class;
public ProjectWebsiteTagConfiguration()
{
ToTable("ProjectWebsiteTags");
HasKey(p => p.WebsiteTagId);
HasRequired(p => p.ProjectUser).WithRequiredDependent().WillCascadeOnDelete(false);
HasRequired(p => p.ProjectWebsite).WithRequiredDependent().WillCascadeOnDelete(false);
}
It looks like it is trying to delete User record, but I do not want that.
I just want to delete "ProjectWebsiteTag" and that's it.
What I am missing here?
It isn't trying to delete the ProjectUser. It's trying to insert it. ProjectWebsiteTagConfiguration() is saying that the ProjectWebsiteTags table has a ProjectUser foreign key in it. When you call
ProjectWebsiteTag websiteTag = context.WebsiteTags.FirstOrDefault(p => p.WebsiteTagId == id)
websiteTag has a ProjectUser with an empty string for its UserId property. So either the record in the ProjectWebsiteTags table has an empty string for the foreign key, or EF is newing a ProjectUser (with an empty UserId) when you get from the context. Either way, EF isn't aware of the existence of a ProjectUser with an empty string id, so when you call SaveChanges() it tries to add it. It can't because the UserId field is required and empty.
I had to fix my problem by adding one-to-many relationship configuration in my User class.
It comes to a resolution of I need to add relationship hint on both sides of Configuration declarations.
public class ProjectUserConfiguration : EntityTypeConfiguration<ProjectUser>
{
public ProjectUserConfiguration()
{
ToTable("ProjectUsers");
HasKey(p => p.UserId);
HasMany(p=>p.ProjectWebsiteTags).WithRequired(q=>q.ProjectUser).WillCascadeOnDelete(false);
}
}
So I have a table of (new) users and a table of groups. What I'm trying to do is add the users to the groups.
What I thought I'd do is :-
using(context = new MyEntity())
{
foreach(csvUser from csvSource)
{
User oUser = new User();
oUser.Firstname = csvUser.Firstname;
Group oGroup = new Group();
// Set the primary key for attach
oGroup.ID = csvUser.GroupID;
context.Group.Attach(oGroup);
oUser.Groups.Add(oGroup);
context.Users.Add(oUser);
}
context.saveChnages();
}
So bascially loop through all the new users, grab their group id from the CSV File (group already exists in db). So I would attach to the group and then add the group.
However I'm running into an error because as soon as a user with group id which has already been attached tries to attach it booms.
An object with a key that matches the key of the supplied object could
not be found in the ObjectStateManager. Verify that the key values of
the supplied object match the key values of the object to which
changes must be applied.
Which I can understand, its trying to re-attach an object its already attached to in memory. However is there a way around this? All I need to do is attach a new user to a pre-existing group from the database.
That error is usually associated with the ApplyCurrentValues in some form or shape - when it tries to update your (previously) detached entity.
It's not entirely clear why is that happening in your case - but maybe you have something else going on - or you're just 'confusing' EF with having attaching the same group over again.
The simplest way I think is to just use Find - and avoid Attach
context.Group.Find(csvUser.GroupID);
Which loads from cache if there is one - or from Db if needed.
If an entity with the given primary key values exists in the context,
then it is returned immediately without making a request to the store.
Otherwise, a request is made to the store for an entity with the given
primary key values and this entity, if found, is attached to the
context and returned. If no entity is found in the context or the
store, then null is returned
That should fix things for you.
You could also turn off applying values form Db I think (but I'm unable to check that at the moment).
It's seems like you're adding a new Group for each User you're iterating on, try this:
using(context = new MyEntity())
{
// fetch Groups and add them first
foreach(groupId in csvSource.Select(x => x.GroupID).Distinct())
{
context.Groups.Add(new Group { ID = groupId });
}
// add users
foreach(csvUser from csvSource)
{
User oUser = new User();
oUser.Firstname = csvUser.Firstname;
var existingGroup = context.Groups.Single(x => x.Id == csvUser.GroupID);
oUser.Groups.Add(existingGroup);
context.Users.Add(oUser);
}
context.saveChnages();
}
It seems like you have a many-to-many relationship between Users and Groups. If that is the case and you are using Code-First then you model could be defined like this...
public class User
{
public int Id { get; set; }
public string Firstname { get; set; }
// Other User properties...
public virtual ICollection<UserGroup> UserGroups { get; set; }
}
public class Group
{
public int Id { get; set; }
// Other Group properties...
public virtual ICollection<UserGroup> UserGroups { get; set; }
}
public class UserGroup
{
public int UserId { get; set; }
public User User { get; set; }
public int GroupId { get; set; }
public Group Group { get; set; }
}
Next, configure the many-to-many relationship...
public class UserGroupsConfiguration : EntityTypeConfiguration<UserGroup>
{
public UserGroupsConfiguration()
{
// Define a composite key
HasKey(a => new { a.UserId, a.GroupId });
// User has many Groups
HasRequired(a => a.User)
.WithMany(s => s.UserGroups)
.HasForeignKey(a => a.UserId)
.WillCascadeOnDelete(false);
// Group has many Users
HasRequired(a => a.Group)
.WithMany(p => p.UserGroups)
.HasForeignKey(a => a.GroupId)
.WillCascadeOnDelete(false);
}
}
Add the configuration in your DbContext class...
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
modelBuilder.Configurations.Add(new UserGroupsConfiguration());
...
}
Now your task is simpler...
foreach (var csvUser in csvSource)
{
User oUser = new User();
oUser.Firstname = csvUser.Firstname;
// Find Group
var group = context.Groups.Find(csvUser.GroupID);
if(group == null)
{
// TODO: Handle case that group is null
}
else
{
// Group found, assign it to the new user
oUser.UserGroups.Add(new UserGroup { Group = group });
context.Users.Add(oUser);
}
}
context.SaveChanges();