I'm struggling to understand why when I remove a child Settings object from MyUser.Settings and SAVE MyUser I get SQL errors like below:
Cannot insert the value NULL into column 'MyUserId', table '###.Settings'; column does not allow nulls. UPDATE fails.
The statement has been terminated.
What I would expect to happen is that removing the item from the collection, then saving MyUser causes NHibernate to issue a DELETE command for the given child. However, what it does is UPDATE the relevant row for the Settings object, setting MyUserId to NULL - which isn't allowed as I'm using a Composite Key.
I've tried so many combinations of Inverse() and the various Cascade options but nothing seems to work. I should point out that Adding to the collection works perfectly when I save MyUser.
I'm totally baffled!
Below is pseudo code to try and explain my entities and mappings.
public class SettingType
{
public virtual int SettingTypeId { get; set; }
public virtual string Name { get; set; }
public virtual bool Active { get; set; }
}
public class Setting
{
public virtual MyUser MyUser { get; set; }
public virtual SettingType SettingType { get; set; }
public virtual DateTime Created { get; set; }
}
public class MyUser
{
public virtual int MyUserId { get; set; }
public virtual IList<Setting> Settings { get; set; }
public virtual string Email { get; set; }
public void AddSetting(SettingType settingType, DateTime now)
{
var existing = _settings.SingleOrDefault(s => s.SettingType.SettingTypeId == settingType.SettingTypeId);
if (existing != null)
{
existing.Updated = now;
}
else
{
var setting = new Setting
{
MyUser = this,
SettingType = settingType,
Created = now,
};
_settings.Add(setting);
}
}
public void RemoveSetting(SettingType settingType)
{
var existingPref = _settings.SingleOrDefault(s => s.SettingType.SettingTypeId == settingType.SettingTypeId);
if (existingPref != null)
{
_settings.Remove(existingPref);
}
}
private readonly IList<Setting> _settings = new List<Setting>();
}
And my mappings:
public class SettingTypeMap : IAutoMappingOverride<SettingType>
{
public void Override(AutoMapping<SettingType> mapping)
{
mapping.Table("SettingTypes");
mapping.Id(m => m.SettingTypeId).GeneratedBy.Identity();
mapping.Map(m => m.Name).Not.Nullable().Length(100);
mapping.Map(m => m.Active).Not.Nullable().Default("0");
}
}
public class SettingMap : IAutoMappingOverride<Setting>
{
public void Override(AutoMapping<Setting> mapping)
{
mapping.Table("Settings");
mapping.CompositeId()
.KeyReference(m => m.MyUser)
.KeyReference(m => m.SettingType);
mapping.Map(m => m.Created).Not.Nullable().Default("CURRENT_TIMESTAMP");
mapping.Map(m => m.Updated).Nullable();
}
}
public class MyUserMappingOverride : IAutoMappingOverride<MyUser>
{
public void Override(AutoMapping<MyUser> mapping)
{
mapping.Table("MyUsers");
mapping.Id(m => m.MyUserId).GeneratedBy.Identity();
mapping.Map(m => m.Email).Not.Nullable().Length(200);
mapping.HasMany(m => m.Settings).KeyColumn("MyUserId").Cascade.DeleteOrphan()
.Access.ReadOnlyPropertyThroughCamelCaseField(Prefix.Underscore);
}
}
All using:
FluentNHibernate v1.3.0.733
NHibernate v3.3.1.4000
UPDATE: After a few suggestions I've tried to change the mapping for MyUser entity.
First to this:
mapping.HasMany(m => m.Settings)
.KeyColumn("MyUserId")
.Inverse()
.Cascade.DeleteOrphan()
.Access.ReadOnlyPropertyThroughCamelCaseField(Prefix.Underscore);
This gives the error: Given key was not present in the dictionary
So tried to add second key column:
mapping.HasMany(m => m.Settings)
.KeyColumn("MyUserId")
.KeyColumn("SettingTypeId")
.Inverse()
.Cascade.DeleteOrphan()
.Access.ReadOnlyPropertyThroughCamelCaseField(Prefix.Underscore);
But this then causes odd behaviour when loading the Settings collection from the DB for a given MyUserId. Looking at the nh profiler I see a second SELECT ... FROM Settings but setting the SettingTypeId same as value for MyUserId.
Still totally baffled. Has cost me too much time so going to revert to adding a primary key id field to the Settings entity. Maybe you just can't do what I'm trying using NHibernate. In pure SQL this is simple.
You should use the Inverse mapping
mapping.HasMany(m => m.Settings)
.KeyColumn("MyUserId")
.Inverse()
.Cascade.DeleteOrphan()
.Access.ReadOnlyPropertyThroughCamelCaseField(Prefix.Underscore);
This will allow NHibernate to ask the setting itself to be deleted. Otherwise, NHibernate firstly tries to delete the relation, and would try to delete the entity.
See: 6.4. One-To-Many Associations
Very Important Note: If the column of a
association is declared NOT NULL, NHibernate may cause constraint
violations when it creates or updates the association. To prevent this
problem, you must use a bidirectional association with the many valued
end (the set or bag) marked as inverse="true". See the discussion of
bidirectional associations later in this chapter.
Related
This is a tale of optional owned entities and foreign keys.
I'm working with EF 5 (code first) and I do this :
public class Parent {
public Guid Id { get; private set; }
public OwnedType1? Owned1 { get; private set; }
public OwnedType2? Owned2 { get; private set; }
public Parent(Guid id, OwnedType1? owned1, OwnedType2? owned2) {
Id = id; Owned1 = owned1; Owned2 = owned2;
}
}
public class OwnedType1 {
public Guid? OptionalExternalId { get; private set; }
public OwnedType1 (Guid? optionalExternalId) {
OptionalExternalId = optionalExternalId;
}
}
public class OwnedType2 {
public Guid? OptionalExternalId { get; private set; }
public OwnedType2 (Guid? optionalExternalId) {
OptionalExternalId = optionalExternalId;
}
}
public class Shared {
public Guid Id { get; private set; }
public Shared (Guid id) {
Id = id;
}
}
Now, the configuration :
//-------- for Parent ------------
public void Configure(EntityTypeBuilder<Parent> builder) {
builder
.ToTable("Parents")
.HasKey(p => p.Id);
builder
.OwnsOne(p => p.Owned1)
.HasOne<Shared>()
.WithMany()
.HasForeignKey(x => x.OptionalExternalId);
builder
.OwnsOne(p => p.Owned2)
.HasOne<Shared>()
.WithMany()
.HasForeignKey(x => x.OptionalExternalId);
}
//-------- for OwnedType1 ------------
// (there's no builder as they're owned and EntityTypeBuilder<Parent> is enough)
//-------- for OwnedType2 ------------
// (there's no builder as they're owned and EntityTypeBuilder<Parent> is enough)
//-------- for Shared ---------------
public void Configure(EntityTypeBuilder<Shared> builder) {
builder
.ToTable("Shareds")
.HasKey(p => p.Id);
}
Side note : If you're wondering why OwnedType1 and OwnedType2 don't each have a property called 'ParentId', it's because it's created implicitly by the "OwnsOne".
My problem is this :
When I create a new Migration, then OwnedType1 works like a charm, but for OwnedType2 (which is quasi-identical), I get his error :
The property 'OptionalExternalId' cannot be added to the type
'MyNameSpace.OwnedType2' because no property type was specified and
there is no corresponding CLR property or field. To add a shadow state
property, the property type must be specified.
I don't understand what it's complaining about. And why it's complaining only for one of them.
I know that you probably can't work it out with this simplified version of my schema, but what I'm asking is what you think it might be (follow your guts of EF guru) :
Some missing constructor?
Incorrect visibility on one of the fields?
Bad navigation definition?
A typo?
Something tricky (like : If you're going to have TWO different entity classes having a one-to-many relation with Shared, then they can't use the same name for external key. Or I need to use a composite key. Or whatnot).
It was a configuration issue that had nothing to do with Owned entities. Another case of "EF error message is obscure but issue is somewhere there in plain sight".
Unfortunately I don't remember how I fixed it. But it was along the lines of "Need an extra constructor with all the paramaters" or "one of the fields had a different name in the constructor parameters" or one of those classic EF mishaps.
I have the following model:
public class Book
{
public Guid Id { get; set; }
public List<IndexPage> IndexPages { get; set; }
}
public class IndexPage
{
public Guid Id { get; set; }
public List<IndexWord> Words { get; set; }
public int IndexType { get; set; }
public Guid BookId { get; set; }
}
public class IndexWord {
public Guid Id { get; set; }
public String Value { get; set; }
public IndexPageId { get; set; }
}
It is configured with the following configuration:
public class BookConfiguration : IEntityTypeConfiguration<Book>
{
public void Configure(EntityTypeBuilder<Book> builder)
{
builder.ToTable("Books");
builder.HasKey(b => b.Id);
builder.HasMany(b => b.IndexPages)
.WithOne()
.HasForeignKey(b => b.BookId);
}
}
public class IndexPageConfiguration : IEntityTypeConfiguration<IndexPage>
{
public void Configure(EntityTypeBuilder<IndexPage> builder)
{
builder.ToTable("IndexPages");
builder.HasKey(ip => new { ip.BookId, ip.IndexType });
builder.HasMany(ip => ip.IndexWords)
.WithOne()
.HasForeignKey(iw => iw.IndexPageId)
.HasPrincipalKey(ip => ip.Id);
builder.HasIndex(ip => ip.Id).IsUnique();
}
}
public class IndexWordConfiguration : IEntityTypeConfiguration<IndexWord>
{
public void Configure(EntityTypeBuilder<IndexWord> builder)
{
builder.ToTable("IndexWords");
builder.HasKey(iw => iw.Id);
builder.Property(iw => iw.Value).IsRequired();
}
}
Some context; The queries are performed with AsNoTracking() and the update is called as following:
DbContext.Set<Book>.Update(book);
If the book is updated, the indexpages are in total replaced with a new set of indexpages.
If calling update the first time it all seems to work correctly and inserts the rows. However when called the second time it raises an Primary Key constraint exception; Which makes sense, however I expected the old indexpages to be removed and the new indexpages to be inserted. Due to the fact that the primary key exists and update is called, not Add.
The reason behind the composite key is that a book can only have a fixed subset of indexpages. this is also the reason why the HasPrincipalKey has been used.
When only using the IndexPage Id as a key in the configuration. The relation between the book exists, but it just keeps inserting new indexpages on top of the old indexpages (I guess due to the AsNoTracking?).
Furthermore, in the code behind I add the indexpages on the book, but these initially have empty id's.
As per docs Update begins tracking the entity. Which means that if you modify it, it's enough to call DbContext.SaveChanges() for it to be saved. If you call Update on a tracked entity it tries to insert it.
If I understand correctly, the old IndexPage entities are not being replaced by the new ones when you attempt to Update ?
You mentioned that you're using No Tracking for your queries. Without tracking the original entity, returned from your query, EF Core has no way to tell that the entity has been modified. If you want the new IndexPage objects to replace the old ones, then you need to track the queries and configure the delete behavior for your relationships.
You can look at the EF Core docs for a reference regarding required relationships and delete behaviors. Be careful to configure these things on the proper entities, so you don't end up deleting everything by removing an IndexPage.
Allow for change tracking and configure if the relationships are required and their delete behavior. That should accomplish what you're seeking to achieve.
https://learn.microsoft.com/en-us/ef/core/modeling/relationships?tabs=fluent-api%2Cfluent-api-simple-key%2Csimple-key#manual-configuration
I have a one-to-many relationship that am trying to update but get the error
Database operation expected to affect 1 row(s) but actually affected 0 row(s). Data may have been modified or deleted since entities were loaded. See http://go.microsoft.com/fwlink/?LinkId=527962 for information on understanding and handling optimistic concurrency exceptions.
UPDATED
The update method will work for as long as the beneficiaries collection is not being changed or updated.
The code looks like
public class Employee
{
public int Id { get; set; }
public string Name { get; set; }
private readonly List<Beneficiary> _beneficiaries;
public IEnumerable<Beneficiary> Beneficiaries => _beneficiaries;
public void AddBeneficiary(string name)
{
var beneficiary = new Beneficiary(Id, name);
_beneficiaries.Add(beneficiary);
}
}
public sealed class Beneficiary
{
public int Id { get; set; }
public int EmployeeId { get; private set; }
public string Name { get; private set; }
public Beneficiary(int employeeId, string name)
{
Name = name;
EmployeeId = employeeId;
}
}
//Extracted from repo method
public void Update(TEntity entity)
{
if (entity != null)
_dbSet.Update(entity);
}
// Extracted Actual update from application service
if (incomingEmployee.Beneficiaries.Any())
{
if (employeeFromStore.Beneficiaries.Any())
{
employeeFromStore.Beneficiaries.ToList()
.ForEach(existingBeneficiary =>
employeeFromStore.RemoveBeneficiary(existingBeneficiary)); //Method skipped for brevity
//Tried calling this too
//_employeeRepository.UnitOfWork.SaveChanges();
}
incomingEmployee.Beneficiaries.ToList().ForEach(beneficiary =>
{
employeeFromStore.AddBeneficiary(beneficiary.Name);
});
}
_employeeRepository.Update(employeeFromStore);
_employeeRepository.UnitOfWork.SaveChanges();
I feel the issue is in how the tracker treats the update for navigation properties collection but I could be wrong.
I have also attempted to use the change tracker for updates with a method like below
public void ApplyCurrentValues<TEntity>(TEntity original, TEntity current)
where TEntity : class
{
//if it is not attached, attach original and set current values
base.Entry<TEntity>(original).CurrentValues.SetValues(current);
}
Does anyone have an idea how to go about this?
I finally made headway after reading about how update works. Basically as mentioned here
, and this issue. When Update method is invoked the state of the whole entity including all reachable entities through navigation properties is marked as modified.
If you are trying to add beneficiaries to an existing employee, the SaveChanges() throws since it expects to modify a record(s) with the primary key(s) provided but finds none in the database. Update method seemed not the best candidate.
I opted to make the Update method from the repository base overridable in the EmployeeRepository as below
//Update method in the repository base
//Made method virtual to enable overriding
public virtual void Update(TEntity entity)
{
if (entity != null)
_dbSet.Update(entity);
}
//Update method from EmployeeRepository
public override void Update(Employee employee)
{
var employeeFromStore = _unitOfWork.Employees.Where(p => p.Id == employee.Id)
.Include(p => p.Beneficiaries)
.SingleOrDefault();
if (employeeFromStore != null)
{
context.Entry(employeeFromStore).CurrentValues.SetValues(employee);
//Replacing the whole collection
foreach (var beneficiary in employeeFromStore.Beneficiaries)
{
if (employee.Beneficiaries.Any())
context.Entry(beneficiary).State = EntityState.Deleted;
}
//Adding new collection
foreach (var beneficiaryToAdd in employee.Beneficiaries)
{
var newBeneficiary = new Beneficiary(beneficiaryToAdd.EmployeeId, beneficiaryToAdd.Name);
employeeFromStore.AddBeneficiary(newBeneficiary.Name);
}
}
}
I believe this answer can be improved or tweaked to suit specific situations. For instance, other than clearing the persisted collection, one can opt for an update
I'm having a rather interesting problem and I am not quite sure how to properly get past it. In order to fully understand my problem, please keep in mind the following:
I am "Modulizing" my features. For example I have written a "Logger" dll that is then turned into a package. This DLL has its own DbContext and knows about certain tables as a result. Then I have written a "Tracker" dll which extends the Logger dll. The tracker dll is another module with its own Db Context and its own tables. It knows about the Logger dll only in the fact that it knows about its service layer and its model layer. Let me show you what that looks like:
Here are the Models (representing tables)
//Logger Module
public class LogError : ILogError
{
public Guid Id { get; set; }
//more stuff not relavent to the problem
}
//Tracker Module
public class ErrorTicket : IErrorTicket
{
public Guid Id { get; set; }
public Guid LogErrorId { get; set; }
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int TicketNumber { get; set; }
//More properties not related to the problem
public virtual LogError LogError { get; set; }
public virtual ILogError MyLogError => LogError;
public virtual ICollection<ErrorTicketNote> ErrorTicketNotes { get; set; }
public virtual IEnumerable<IErrorTicketNote> MyErrorTicketNotes => ErrorTicketNotes;
}
Please keep in mind for the ErroTicket Class, I am using interfaces to expose certain methods. For example my interface only has getters and no setters, so when an interface is passed, the class cannot be updated. I would rather not go into a discussion as to why I do that as I am very certain it is not part of the problem. Just wanted to make a not so you understand why I have LogError and then MyLogError listed up there.
Now for my DbContext I have the following:
//Logger Module
public class LoggerDbContext : DbContext, ILoggerDbContext
{
public DbSet<Model.LogError> LogError { get; set; }
public DbSet<Model.LogInfo> LogInfo { get; set; }
public IEnumerable<ILogError> LogErrors => LogError;
public IEnumerable<ILogInfo> LogInfos => LogInfo;
public LoggerDbContext(string connectionString = "DefaultConnection") : base(connectionString) { }
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
modelBuilder.Conventions.Remove<PluralizingTableNameConvention>();
base.OnModelCreating(modelBuilder);
}
public void CreateLog(ILogError logError)
{
LogError.Add((Model.LogError) logError);
SaveChanges();
}
public void CreateLog(ILogInfo logInfo)
{
LogInfo.Add((Model.LogInfo) logInfo);
SaveChanges();
}
}
//Tracker Module
public class TrackerDbContext : DbContext, ITrackerDbContext
{
public DbSet<ErrorTicket> ErrorTicket { get; set; }
public DbSet<ErrorTicketNote> ErrorTicketNote { get; set; }
public IEnumerable<IErrorTicket> ErrorTickets => ErrorTicket;
public IEnumerable<IErrorTicketNote> ErrorTicketNotes => ErrorTicketNote;
public TrackerDbContext(string connectionString = "DefaultConnection") : base(connectionString) { }
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
modelBuilder.Conventions.Remove<PluralizingTableNameConvention>();
base.OnModelCreating(modelBuilder);
}
public void CreateTicket(IErrorTicket errorTicket)
{
ErrorTicket.Add((ErrorTicket) errorTicket);
SaveChanges();
}
public void ModifyTicket(IErrorTicket errorTicket)
{
Entry(errorTicket).State = EntityState.Modified;
SaveChanges();
}
public void CreateTicketNote(IErrorTicketNote errorTicketNote)
{
ErrorTicketNote.Add((ErrorTicketNote) errorTicketNote);
SaveChanges();
}
public void ModifyTicketNote(IErrorTicketNote errorTicketNote)
{
Entry(errorTicketNote).State = EntityState.Modified;
SaveChanges();
}
}
As you can see two DbContext classes do not know about each other, but through my models I create a relation of foreign key. Now to my problem.
When an error occurs I have code that runs the following:
//the line bellow calls a Logger service that ends up invoking the method from the Logger DbContext - public void CreateLog(ILogError logError)
var var logError = _databaseLoggerService.Error(exception, message);
//Then I try to create my ErrorTicket and I assign the logError object to the class to create the relation of the foreign key.
var currentTime = DateTime.Now;
var errorTicket = new ErrorTicket
{
Id = Guid.NewGuid(),
LogErrorId = logError.Id,
TimeCreated = currentTime,
TimeResolved = null,
TimeOfFirstOccurrence = currentTime,
TimeOfLastOccurrence = currentTime,
TotalOccurrences = 1,
Resolved = false,
Resolution = string.Empty,
CommitNumber = string.Empty,
TimeImplemented = null,
LogError = (LogError) logError
};
_trackerDbContext.CreateTicket(errorTicket);
The problem that I get is the following:
Violation of PRIMARY KEY constraint 'PK_dbo.LogError'. Cannot insert duplicate key in object 'dbo.LogError'. The duplicate key value is (769fb127-a8d8-40de-9492-fc61ca86cb16).
The statement has been terminated.
If I look at my LogError table, a record indeed exists with that key. I assume it was created when I called _databaseLoggerService.Error(exception, message);
What I do not understand, is why is this a problem for EF6 or how to get past it?
I have done a lot of research on the topic and I have found articles which state that because it is 2 sepparate DbContextes the second one may not know that the record exist so it when I caled .Add method, it marked ALL objects for insertoin and thus generated the INSERT queries. Which makes sense, and I could simply not call my db creation and let my tracker just create both objects for me. Which is all fine, however, the problem I have with that is when I try to modify 'the record, I get the exact same problem. Even though I marked the record as modify, it generates the insert queries.
My question is how do I get past this problem?
i have searched the "whole" internetz for this question, and its a damn hard one to search for as its rather complicated. Try searching for "Fluent NHibernate Many to Many with a bridge table with extra columns" etc...
Okay, to make it easier to explain ill define some tables i can refer to.
Table: User, Table: Function, Table: User_Has_Function.
One User can have many Functions, and a Function can have many Users, this is linked in the bridge table User_Has_Function. The bridge table has extra columns which is only relevant to the relationship.
Well anyways iv found that FNH doesn't have any automatic solution to this, basically you have to use a one to many relation from User to User_Has_Function and many to one from User_Has_Function to Function, hence "[One] to [Many - Many] to [One]".
I have solved it like in this link http://sessionfactory.blogspot.com/2010/12/many-to-many-relationships-with.html just with FNH class mapping instead of xml obviously.
But im not satisfied with the solution, do i really have to do all this manually work to make this function properly? Also as it is now it inserts duplicates in the bridge table.
In my head i'm doing something wrong, cause i cant imagine there is no support for this. Just use SaveAndUpdate(), no duplicates are inserted and when i remove an entity the relation is removed as well, if no relations are left remove the entity itself etc.
Okay here are my entities and mappings, I am VERY new to Fluent NHibernate so don't yell to much if i have done something very wrong. :)
Entities:
public class XUser
{
public virtual int Id { get; set; }
...
public virtual IList<XUserHasXFunction> XUserHasXFunctions { get; set; }
public XUser()
{
XUserHasXFunctions = new List<XUserHasXFunction>();
}
public virtual void AddXFunction(XFunction xFunction, int isActive)
{
var xUserHasXFunction = new XUserHasXFunction()
{
XUser = this,
XFunction = xFunction,
DeployedDate = DateTime.Now
};
XUserHasXFunctions.Add(xUserHasXFunction);
xFunction.XUserHasXFunctions.Add(xUserHasXFunction);
}
public virtual void RemoveXFunction(XFunction xFunction)
{
var xUserHasXFunction = XUserHasXFunctions.Single(x => x.XFunction == xFunction);
XUserHasXFunctions.Remove(xUserHasXFunction);
xFunction.XUserHasXFunctions.Remove(xUserHasXFunction);
}
}
public class XFunction
{
public virtual int Id { get; set; }
...
public virtual IList<XUserHasXFunction> XUserHasXFunctions { get; set; }
public XFunction()
{
XUserHasXFunctions = new List<XUserHasXFunction>();
}
public virtual void AddXUser(XUser xUser, int isActive)
{
var xUserHasXFunction = new XUserHasXFunction()
{
XUser = xUser,
XFunction = this,
DeployedDate = DateTime.Now
};
XUserHasXFunctions.Add(xUserHasXFunction);
xUser.XUserHasXFunctions.Add(xUserHasXFunction);
}
public virtual void RemoveXUser(XUser xUser)
{
var xUserHasXFunction = XUserHasXFunctions.Single(x => x.XUser == xUser);
XUserHasXFunctions.Remove(xUserHasXFunction);
xUser.XUserHasXFunctions.Remove(xUserHasXFunction);
}
}
public class XUserHasXFunction
{
public virtual int Id { get; set; }
public virtual XUser XUser { get; set; }
public virtual XFunction XFunction { get; set; }
public virtual DateTime DeployedDate { get; set; }
}
Mappings:
public class XUserMap : ClassMap<XUser>
{
public XUserMap()
{
Id(x => x.Id, "ID").GeneratedBy.Sequence("SEQ").Column("ID");
Table("XUSER");
...
HasMany(x => x.XUserHasXFunctions).Cascade.All();
}
}
public class XFunctionMap : ClassMap<XFunction>
{
public XFunctionMap()
{
Id(x => x.Id, "ID").GeneratedBy.Sequence("SEQ").Column("ID");
Table("XFUNCTION");
...
HasMany(x => x.XUserHasXFunctions).Cascade.All();
}
}
public class XUserHasXFunctionMap : ClassMap<XUserHasXFunction>
{
public XUserHasXFunctionMap()
{
Id(x => x.Id, "ID").GeneratedBy.Sequence("SEQ").Column("ID");
Table("USER_HAS_FUNCTION");
Map(x => x.DeployedDate, "DEPLOYED_DATE");
References(x => x.XUser).ForeignKey("XUSER_ID").Cascade.SaveUpdate();
References(x => x.XFunction).ForeignKey("XFUNCTION_ID").Cascade.SaveUpdate();
}
}
I don't understand the "do i really have to do all this manual work" part. What "all this manual work"? There is nothing special there. The mapping is simple and the c# code doesn't have to do anything with persistency, it's plain old OO design.
If you get duplicated rows, there is something wrong with your mapping. It might be because of a inverse collection which had not been mapped as inverse.
If you don't need to navigate from Function to User, it's very easy. Either map the relation as entity, as described in the blog, or even easier, map it as a composite element.
(Sorry, I don't know Fluent)
<bag name="Functions" table="User_Has_Function">
<key column="UserId" />
<composite-element>
<many-to-one class="Function"/>
</composite-element>
</bag>
Edit:
From the comments:
The manual work I am talking about is the manual getting and checking
to remove and add relations from a user or function.
Are you talking about the required Add and Remove methods, which maintain the consistency of the relations? This is plain OO design. If you hadn't NHibernate, you would have to write it exactly the same (given the same class model).
delete a user from a function make it cascade all the way to user and
so forth...
No. Delete-cascading happens when an object is deleted. When you delete a user, you should cascade the user_has_function. From there, you may or may not cascade the functions. The same in the other direction. There is also the concept of "cascade-all-delete-orphans". It means that additionally to regular cascading, an object is deleted automatically when it is removed from the collection. This is not cascading. It is a kind of very basic garbage collection. If you want to make use of this in your case, you should not apply it to both the user->user_has_function collection and the function->user_has_function collection, because it would try to delete the object twice.
Don't forget to map both collections inverse. If you don't, you may get duplicated entries.
Make sure that the three mappings (the user->user_has_function collection, the function->user_has_function and the user_has_function class mapping) are using the same table name and foreign key names.
You don't need to mess around with composite keys.
I ended up doing something similar a while ago with user, group, user_group and ended up having to use a hacky method of having both objects exist on both sides and also manually choose between save or update.
I don't think there is a NICE way to do what you want, and I agree it is something that from a database point of view is fairly logical to do, but from a modelling point of view is a pain.
As I also assume you are having to use a composite key for your user_has_function table to make sure that you can have multiple functions for multiple users. Which I think most people try to avoid and end up using surrogate keys or some other approach.
I know this isn't an answer, but I never found a real answer to the same question when I posted it.
Here is a similar question I posted a while back:
Nhibernate composite key question
I ended up using an ISet instead of having the relations in ILists. ISet does not allow duplicates, but IList does. To use ISet you have to override the Equals and GetHashCode methods for the object stored in the ISet.
I cascade from XUser and XFunction and not the other way around, ended up that every record in all 3 tables were deleted when i deleted one entity because of cascading.
Here is how i solved it.
Entities:
public class XUser
{
public virtual int Id { get; set; }
...
public virtual ISet<XUserHasXFunction> XUserHasXFunctions { get; set; }
public XUser()
{
XUserHasXFunctions = new HashedSet<XUserHasXFunction>();
}
public virtual void AddXFunction(XFunction xFunction, int isActive)
{
var xUserHasXFunction = new XUserHasXFunction()
{
XUser = this,
XFunction = xFunction,
IsActive = isActive,
DeployedDate = DateTime.Now
};
if (XUserHasXFunctions.Contains(xUserHasXFunction) && xFunction.XUserHasXFunctions.Contains(xUserHasXFunction))
{
return;
}
XUserHasXFunctions.Add(xUserHasXFunction);
xFunction.XUserHasXFunctions.Add(xUserHasXFunction);
}
public virtual void RemoveXFunction(XFunction xFunction)
{
var xUserHasXFunction = XUserHasXFunctions.Single(x => x.XFunction == xFunction);
XUserHasXFunctions.Remove(xUserHasXFunction);
xFunction.XUserHasXFunctions.Remove(xUserHasXFunction);
}
}
public class XFunction
{
public virtual int Id { get; set; }
...
public virtual ISet<XUserHasXFunction> XUserHasXFunctions { get; set; }
public XFunction()
{
XUserHasXFunctions = new HashedSet<XUserHasXFunction>();
}
public virtual void AddXUser(XUser xUser, int isActive)
{
var xUserHasXFunction = new XUserHasXFunction()
{
XUser = xUser,
XFunction = this,
IsActive = isActive,
DeployedDate = DateTime.Now
};
if (XUserHasXFunctions.Contains(xUserHasXFunction) && xUser.XUserHasXFunctions.Contains(xUserHasXFunction))
{
return;
}
XUserHasXFunctions.Add(xUserHasXFunction);
xUser.XUserHasXFunctions.Add(xUserHasXFunction);
}
public virtual void RemoveXUser(XUser xUser)
{
var xUserHasXFunction = XUserHasXFunctions.Single(x => x.XUser == xUser);
XUserHasXFunctions.Remove(xUserHasXFunction);
xUser.XUserHasXFunctions.Remove(xUserHasXFunction);
}
}
public class XUserHasXFunction
{
public virtual int Id { get; set; }
...
public virtual DateTime DeployedDate { get; set; }
public override bool Equals(object obj)
{
if (obj == null)
return false;
var t = obj as XUserHasXFunction;
if (t == null)
return false;
return XUser == t.XUser && XFunction == t.XFunction;
}
public override int GetHashCode()
{
return (XUser.Id + "|" + XFunction.Id).GetHashCode();
}
}
Mappings:
public class XUserMap : ClassMap<XUser>
{
public XUserMap()
{
Id(x => x.Id, "ID").GeneratedBy.Sequence("SEQ").Column("ID");
Table("XUSER");
...
HasMany(x => x.XUserHasXFunctions).KeyColumn("XUSER_ID").Cascade.All();
}
}
public class XFunctionMap : ClassMap<XFunction>
{
public XFunctionMap()
{
Id(x => x.Id, "ID").GeneratedBy.Sequence("SEQ").Column("ID");
Table("XFUNCTION");
...
HasMany(x => x.XUserHasXFunctions)KeyColumn("XFUNCTION_ID").Cascade.All();
}
}
public class XUserHasXFunctionMap : ClassMap<XUserHasXFunction>
{
public XUserHasXFunctionMap()
{
Id(x => x.Id, "ID").GeneratedBy.Sequence("SEQ").Column("ID");
Table("XUSER_HAS_XFUNCTION");
...
Map(x => x.DeployedDate, "DEPLOYED_DATE");
References(x => x.XUser).Column("XUSER_ID");
References(x => x.XFunction).Column("XFUNCTION_ID");
}
}
Usage:
To add relations.
xFunction.AddXUser(xUser, isActive); //visa versa if you like to add a function to a user...
dao.Store(xFunction); //to actually add the relation in the db
now to remove relation
xFunction.RemoveXUser(xUser); //Realtion is removed but neither of the objects xFunction or xUser
dao.Store(xFunction); //...same
to remove a user and its relations.
dao.delete(xUser); //but the xFunction object it was connected to is not removed
//if you want the xFunction object to be removed you have to do that manually.