Fluent NHibernate one-to-many Cascade.SaveUpdate() blocks updates to entity - nhibernate

We have an Enrollment object that has a Student object and the Student object has many Enrollment objects. If I leave off the Cascade.SaveUpdate() from the Enrollment's Student reference, updates to the Student table do not execute, but updates to the Enrollment object succeed. But if I add the Cascade.SaveUpdate() on the Enrollment's Student reference, the updates to the Student table work fine, but updates to the Enrollment table fail. No exceptions are thrown, the updates just don't succeed.
There must be some way to be able to save objects on both sides of the relationship, but what am I missing?
Here's the code snips, let me know if you need more:
EnrollmentMap:
References(x => x.Student)
.Column("student_id");// without the cascade on the next line, this fails to update changes to Student
//.Cascade.SaveUpdate();// when uncommented this updates changes to Student but blocks updates to Enrollment
StudentMap:
HasMany(x => x.Enrollments)
.KeyColumn("student_id")
.Inverse()
.Cascade.SaveUpdate();
Database call:
public Application GetApplication(long applicationId)
{
using (var session = sessionFactory.OpenSession())
{
var query = session.Linq();
query.Expand(x => x.Enrollment);
query.Expand(x => x.Enrollment.Student);
var result = from entity in query
where entity.ApplicationId == applicationId
select entity;
return result.Count() > 0 ? result.First() : null;
}
}
Database save:
using (var session = sessionFactory.OpenSession())
{
using (var transaction = session.BeginTransaction())
{
try
{
session.SaveOrUpdate(entity);
transaction.Commit();
}
catch(Exception ex)
{
transaction.Rollback();
throw;
}
}
}

You should try and load your entity in the same session as you update it. I think that is your problem.
If you really can't do this, then it is possible to 'merge' the entity in to your session (google 'NHibernate merge').

Related

NHibernate (References) OneToMany

I have:
DailyWorkTimeMapping()
{
....
References(x => x.Employee);
}
So, when I do:
var x = new DailyWorkTime()
{
Employee = new Employee()
{
Id = IdFromAnExistingEmployee,
};
}
and when I call SaveOrUpdate I get the following error:
NHibernate.TransientObjectException: object references an unsaved
transient instance - save the transient instance before flushing or
set cascade action for the property to something that would make it
autosave. Type: ProjectTracker.Domain.Employees.Employee, Entity:
ProjectTracker.Domain.Employees.Employee bei
NHibernate.Engine.ForeignKeys.GetEntityIdentifierIfNotUnsaved(String
entityName, Object entity, ISessionImplementor session)
Any ideas?
If you are trying to call save on the DailyWorkTime instance you need to flag the Employee property as cascading.
References(x => x.Employee).Cascade.All();
Alternatively you can save the Employee object before you save the DailyWorkTime
var normalHours = new DailyWorkTime();
normalHours.Employee = new Employee() { Id = 1234};
session.SaveOrUpdate(normalHours.Employee);
session.SaveOrUpdate(normalHours);
in you mapping from OneToMany on the Employee table. Set the mapping as inverse. This will mean the the child controls the save.

Fluent NHibernate only cascade delete of association record

We're using NHibernate for our membership system. A User can be a member of many Roles and a Role can have many users.
When a Role or a User is deleted, it should only cascade the delete of the association record ("RoleUsers" table).
Deleting a Role works as expected. However, deleting a User does not delete the association record and as such fails due to a foreign key constraint.
My mapping on the Role side:
HasManyToMany(r => r.Users)
.Access.CamelCaseField()
.Table("RoleUsers")
.ParentKeyColumn("RoleId")
.ChildKeyColumn("UserId")
.AsSet();
Mapping on the User side:
HasManyToMany(u => u.Roles)
.Access.CamelCaseField()
.Table("RoleUsers")
.ParentKeyColumn("UserId")
.ChildKeyColumn("RoleId")
.Inverse(); // we'll add user to role, not role to user
And the failing test:
[Test]
public void Deleting_user_should_not_delete_roles()
{
var user = new User("john#doe.com", "John", "Doe", "Secr3t");
var role = new Role("Admin");
role.AddUser(user);
object id;
using (var txn = Session.BeginTransaction())
{
id = Session.Save(user);
Session.Save(role);
txn.Commit();
}
Session.Clear();
var fromDb = Session.Get<User>(id);
using (var txn = Session.BeginTransaction())
{
Session.Delete(fromDb);
txn.Commit();
}
Session.Query<Role>().Count().ShouldEqual(1);
}
I've tried every combination of Cascade on the user mapping and it either fails or deletes the association record AND the role (not what I want).
Inverse and cascading are two different concepts. And of course, both are supported on <many-to-many> relation. See the documentation 6.8 (scroll down almost to 6.9)
http://nhibernate.info/doc/nh/en/index.html#collections-bidirectional
1) Firstly, we can remove the inverse setting of the User.Roles collection. This will straightens the behavior of the Users`s relations to Roles, and force their deletion before User itself is deleted.
2) Secondly. If the Roles collection of the User is marked as inverse,
HasManyToMany(u => u.Roles)
...
.Inverse(); // we'll add user to role, not role to user
deletion of any User, will never trigger deletion of the pair. That's because we are explicitly saying: the one and only one who care about the relation is the Role.
So if we would like to continue in your scenario:
.Inverse(); // we'll add user to role, not role to user
we should be consitent. "we'll remove user from roles, not roles from user":
[Test]
public void Deleting_user_should_not_delete_roles()
{
var user = new User("john#doe.com", "John", "Doe", "Secr3t");
var role = new Role("Admin");
role.AddUser(user);
object roleId;
object id;
using (var txn = Session.BeginTransaction())
{
id = Session.Save(user);
roleId = Session.Save(role);
txn.Commit();
}
Session.Clear();
// take both from DB
var userFromDb = Session.Get<User>(id);
var roleFromDb = Session.Get<Role>(roleId);
using (var txn = Session.BeginTransaction())
{
// HERE just remove the user from collection
roleFromDb.Users.Remove(userFromDb);
// all relations will be deleted
Session.Save(roleFromDb);
txn.Commit();
}
...
// assertions
// "John's" relation to Role "admin" is deleted
}
NOTE:
3) Cascade was not used in there, but could help to reduce Session.Save(user)...
EDIT: Extending the point 3)
Deletion of the user as Ben Foster noticed in a comment.
3) We should even allow the Role to manage its Users collection completely. Let's introduce the casdace="all" (casdace="all-delete-orhpan" if User without any Role should be deleted at all). Now we can add/update users only via Role object.
the mapping of the Role's Users collection should look like:
HasManyToMany(r => r.Users)
.Access.CamelCaseField()
.Table("RoleUsers")
.ParentKeyColumn("RoleId")
.ChildKeyColumn("UserId")
//.Cascade.All(); // just save or update instance of users
.Cascade.AllDeleteOrphan(); // will even delete User without any Role
.AsSet();
Having inverse and cascade we can adjust the test:
[Test]
public void Deleting_user_should_not_delete_roles()
{
var user = new User("john#doe.com", "John", "Doe", "Secr3t");
var role = new Role("Admin");
role.AddUser(user);
object roleId;
using (var txn = Session.BeginTransaction())
{
// I. no need to save user
roleId = Session.Save(role);
...
And later call this to get rid of a User
...
using (var txn = Session.BeginTransaction())
{
var user = Session.Get<User>(id);
var roles = user.Roles.ToList();
roles.ForEach(role => role.RemoveUser(user))
// II. not only relations, but even the User is deleted
// becuase updated roles triggered delete orhpan
// (no Session.Update() call there)
txn.Commit();
}

When to call NHibernate Rollback?

I'm using NHibernate to insert some data into Table A. I want to update the status in Table B if the Table A transaction fails. How do I check if it has failed?
Below is my current code:
// Add userId to Receiver
Receiver receiver = new Receiver();
receiver.User = User.GetById(Convert.ToInt32(listItem.Value));
receiver.Notification = Notification.GetById(notification.NotificationId);
receiver.Save();
Where do I call the NHibernate Transaction? If it fails where do I call NHibernate Rollback and update the Table B status?
Take a look at the Official NHibernate documentation on Exception handling: http://nhibernate.info/doc/nh/en/index.html#manipulatingdata-exceptions
using ( ISession session = sessionFactory.OpenSession() )
{
using ( ITransaction transaction = session.BeginTransaction() )
{
try
{
// Do your save/update here for Table A
transaction.Commit();
}
catch( Exception e )
{
// Your save or update failed so this is where you
// could capture that info and update your Table B
transaction.Rollback();
}
}
}
From what I remember, you don't actually have to call tx.Rollback() because when your code leaves the using blocks, it will do that automatically but again, I can't remember exactly. Give it a try and if it doesn't behave as I just described, you can manually rollback in the catch.
using (ISession session = factory.OpenSession())
using (ITransaction tx = session.BeginTransaction())
{
// do some work
tx.Commit();
}
or manually
ISession session = factory.openSession();
try
{
// do some work
session.Flush();
currentTransaction.Commit();
}
catch (Exception e)
{
currentTransaction.Rollback();
throw;
}
finally
{
session.Close();
}
Take a look NHibernate transactions for more details

NHibvernate 3.2 many to many not populating join table

I have two classes, user and role, defined as:
public class User : Entity
{
// other properties ...
public virtual string Username
public virtual ICollection<Role> Roles { get; set; }
}
public class Role : Entity
{
public virtual string Name { get; set; }
public virtual ICollection<User> Users { get; set; }
}
In my mapping code, I have the following:
mapper.Class<User>(map =>
{
map.Bag(x=>x.Roles,
cm=>
{
cm.Table("UserRole");
cm.Cascade(Cascade.All);
cm.Key(k => k.Column("[User]"));
},
em=>
{
em.ManyToMany(mm =>
{
mm.Column("[Role]");
});
});
});
mapper.Class<Role>(map =>
{
map.Bag(x=>x.Users,
cm=>
{
cm.Inverse(true);
cm.Table("UserRole");
cm.Key(k=>k.Column("[Role]"));
},
em =>
{
em.ManyToMany(mm =>
{
mm.Column("[User]");
});
});
});
The mappings generate the expected schema, but the join table is never populated. Adding a new user with a new Role in its collection persists the role and then the user to the appropriate tables, but the join table is left empty. Why?
Edit: I still have not made any progress on this. I'm absolutely sure the mapping is correct, and the correct schema is generated, but the join table simply isn't populated. For test purposes, I'm generating entities using NBuilder like so:
var roles = new Role[]
{
new Role("Admin"),
new Role("Manager"),
new Role("User")
};
var users = Builder<User>.CreateListOfSize(10)
.TheFirst(1)
.Do(x =>
{
x.Roles.Add(roles[0]);
x.Roles.Add(roles[1]);
roles[0].Users.Add(x);
roles[1].Users.Add(x);
})
.All()
.With(x => x.Id = 0)
.And(x => x.Version = 0)
.And(x => x.Username = "test user")
.And(x => x.Password = "test password")
.Do(x =>
{
x.Roles.Add(roles[2]);
roles[2].Users.Add(x);
}
.Build();
foreach (var u in users) session.Save(u);
The User and Role entities are persisted correctly, but the join table remains empty. This means I cannot effective query the roles for a given user later, which nullifies the point.
Make sure you have both classes referencing each other.
I think that code, similar to one below, should work for you:
role.Users.Add(user);
user.Roles.Add(role);
session.Save(user); // NH only saves user and role, so it can get auto-generated identity fields
session.Flush(); // NH now can save into cross-ref table, because it knows required information (Flush is also called inside of Transaction.Commit())
I found a good answer to a question about many-to-many with lot of explanations and quotes from NH documentation. I think it worth to read it.
[EDIT]
In answer to this somewhat similar question there is discussion in which need for explicit transaction to save into cross-table is mentioned.
I also edited code above with adding session.Flush() to reflect my findings.
I ended up downloading the NHibernate source and referencing that directly so I could step through it. It turns out that it had something to do with the fact that my code for generating the test data was not wrapped in an explicit session transaction. Once I added that, it was fine. I'd love to see some kind of explanation on this, as I wasn't able to follow the code very clearly, but I'm at least satisfied that the problem is solved.

Rollback transaction in Repository from another class

well my problem is:
I have a method like:
class Manager
{
void method1()
{
// save object in database to get ID
int newId = this.Repository.Save(obj);
try {
// call remote webservice to save another object with same ID as in local DB
webservice.Save(remoteObj, id);
}
catch(Exception e)
{
// do Rollback in Repository here
}
}
}
Bassically this is the code. Repository use NHibernate to save to DB. I need to save in DB to know the new ID and then send this ID to webservice. If something fail calling webservice I want to rollback and discard saved object.... and here is my problem. I can't open and control a transaction in Repository from my class Manager.
I already try with this also:
class Manager
{
void method1()
{
using (TransactionScope scope = new TransactionScope())
{
// save object in database to get ID
int newId = this.Repository.Save(obj);
// call remote webservice to save another object with same ID
// as in local DB
webservice.Save(remoteObj, id);
scope.Complete();
}
}
}
Here the problem is that the rollback is OK but not the Save(Create in NHibernate). I get error about that object "Transaction" is not found or the transaction is already closed just after the line : "scope.Complete();".
I think that something is wrong trying to control NHibernate transaction with TransactionScope .
I dont know if is a problem about approach, maybe another way should be used to handle this situation... ??
any help or idea where to find ??
Thanks a lot !!
Assuming you already have an opened session in a CurrentSession property/variable and that you could pass that working session to your repository, I would do the following:
using(var trx = CurrentSession.BeginTransaction())
{
try
{
int newId = this.Repository.Save(obj, CurrentSession);
webservice.Save(remoteObj, id);
trx.Commit();
}
catch
{
trx.Rollback();
}
}