views:

596

answers:

4

I've run into some trouble with unique constraints in NHibernate.

I have a User entity that is mapped with a unique constraint for the Username property. What I want to do is to be able to check if a particular username exists before a new user is added and also before an existing user updates it's username.

The first scenario (adding a new user) works just fine. However, when I try to check if the username exists before updating an existing user, I get a constraint violation. Here's what the code for my Save method looks like.

public void Save<T>(T entity) where T : User
 {
  using (var session = GetSession())
  using (var transaction = session.BeginTransaction())
  {
   try
   {
    CheckIfUsernameExists(entity);

    session.SaveOrUpdate(entity);
    session.Flush();
    transaction.Commit();
   }
   catch (HibernateException)
   {
    transaction.Rollback();
    throw;
   }
  }
 }

The constraint is violated in the CheckIfUsernameExists() method and it looks like this:

public void CheckIfUsernameExists<T>(T entity) where T : User
 {
  var user = GetUserByUsername(entity);
  if (user != null)
   throw new UsernameExistsException();
 }

 private T GetUserByUsername<T>(T entity) where T : User
 {
  var username = entity.Username;
  var idToExclude = entity.Id;

  var session = GetSession();

  var user = session.CreateCriteria<T>()
   .Add(Restrictions.Eq("Username", username))
   .Add(Restrictions.Not(Restrictions.IdEq(idToExclude)))
   .UniqueResult() as T;

  return user;
 }

It is the session.CreateCriteria() line that causes it to crash resulting in an NHibernateException (SQLiteException) with the message "Abort due to constraint violation. column Username is not unique".

Is it related to the NHibernate cash? The entity that is passed to the save method has been updated with the desired username at the time session.CreateCriteria() is called.

Maybe I'm doing this all wrong (I am an NHibernate beginner) so please feel free to state the obvious and suggest alternatives.

Any help is much appreciated!

A: 

Hmm, I'm not sure about the core of the problem, but for the strategy of trying to see whether a user already exists, why do you need the ".UniqueResult()"?

Couldn't you just assume to get a list of users which match that username and which do not have the same id as your current user (obviously). Pseudo-code like I'd do something like this

public bool ExistsUsername(string username, int idToExclude)
{
   IList<User> usersFound = someNHibernateCriteria excluding entries that have id = idToExclude

   return (usersFound.Count > 0)
}
Juri
The answer to your question is, I don't. I've changed the code to what you suggested but the result is the same. Thanks for input!
Kristoffer Ahl
ok...I guess you placed a unique constraint on the "username" col in your DB? Moreover I read that you have "entity that is passed to the save method has been updated with the desired username at the time session.." I wouldn't do that. I would call the ExistsUsername before so you can also avoid having the "idToAvoid".
Juri
Yes, there is a constraint in db for username. Regarding calling ExistsUsername before I update the entity; It might be a possibility but I will need to investigate further. Thanks.
Kristoffer Ahl
It was just an opinion for optimizing the logic. Assigning the username to the entity when you don't actually know whether the username is valid sounds wrong to me. It is logically not correct. Instead I would check that, say on your UI controller with a call ExistsUsername(username) and if so, display an appropriate message, otherwise call SaveUser(...).
Juri
@Juri: Your suggested strategy is what we went for. We now have a service on top of our repository that handles the unique username check. Thanks!
Kristoffer Ahl
nop. So you should accept my answer ;)
Juri
A: 

Two thoughts: - Why don't you just SaveOrUpdate and see if you succeed. Is that not possible in your scenario? - I've seen you mentioning SQLite. Is that your real production system, or just something you use for testing. If so, have you checked if it's SQLite that makes the problems, and the query works against a fully featured DBMS? - SQLite frequently makes that kind of problems, because it does not support every kind of constraint...

Thomas Weller
Thomas, I was hoping to be able to just call SaveOrUpdate but since I'm not able to check what type of exception was thrown, I have no way of notifying the user what went wrong. That is why I wanted to throw my own exception (UsernameExistsException). Or is there a way to get more specific NHibernate exceptions that I am not aware of?Regarding SQLite; yes, we use SQLite on our local dev mashines and for our integration tests. We currently have no production environment set up. I will keep it in mind but it seems unlikely that SQLite couldn't handle this simple scenario.
Kristoffer Ahl
Do you generate your SQLite instance via hbm2ddl from your NH mappings? Then that's the problem: SQLite does not support 'ALTER TABLE' statements, AFAIK they are silently ignored.You just can't use SQLite for integration tests, if your planning to target another DBMS in production - it's not a full replacement. It may be fast and easy, but it can give wrong results.You should take you half an hour to test this code against a full DBMS - I'm quite sure the test will pass...
Thomas Weller
@Thomas Weller: I'm afraid that is not the case. We drop the tables in the db and use schema export to add the new tables. Regarding SQLite for integration tests, your statement is the exact opposite to what I've heard from a lot of people. Why can't you use it for integration tests? Thanks for the input!
Kristoffer Ahl
@Kristoffer: You can't use it, because SQLite for example has no unique or foreign key constraints. So if you are testing a piece of logic, that relies on this, you will get wrong results. And what's even worse in my eyes: If you use schema export to generate your database on the fly from your NH mappings, you will get no hint to that at all (error or log message or whatever).I know that a lot of people like using SQLite for these kinds of tests, but nevertheless they are seriously flawed and - strictly spoken - worthless.
Thomas Weller
A: 

Are you sure the exception is thrown at CreateCriteria? Because, I don't see how you could get a SQLlite constraint exception from a select statement. I do virtually the same thing...

public bool NameAlreadyExists(string name, int? exclude_id)
{
    ICriteria crit = session.CreateCriteria<User>()
     .SetProjection(Projections.Constant(1))
     .Add(Restrictions.Eq(Projections.Property("name"), name));

    if (exclude_id.HasValue)
     crit.Add(Restrictions.Not(Restrictions.IdEq(exclude_id.Value)));

    return crit.List().Count > 0;
}

I would look at the order of the generated sql to see what's causing it. If that entity was loaded in that session, it could be getting updated before the query.

dotjoe
A: 

transaction.Rollback() doesn't remove your entity from session cache, use session.Evict() instead.

See: - https://www.hibernate.org/hib%5Fdocs/nhibernate/html/performance.html#performance-sessioncache

SHSE