NHibernate trickery: what happened to my associations?
I usually post things of moderate difficulty, let us see if you can not only figure this one out, but where that bug exists.
Given the following class & mapping:
public class User { public virtual string Username { get; set; } public virtual ICollection<string> AllowedPaths { get; set; } public User() { AllowedPaths = new HashSet<string>(); } }
<class name="User" table="Users"> <id name="Username"> <generator class="assigned"/> </id> <set name="AllowedPaths" table="UsersAllowPaths"> <key column="`User`" not-null="true"/> <element column="Path" type="System.String" not-null="true"/> </set> </class>
I have the following code:
public class Program { public static void Main() { Configuration configuration = new Configuration().Configure("nhibernate.config"); ISessionFactory sessionFactory = configuration.BuildSessionFactory(); new SchemaExport(configuration).Create(true, true); using (ISession s = sessionFactory.OpenSession()) using (ITransaction tx = s.BeginTransaction()) { s.Save(new User { Username = "ayende", AllowedPaths = {"/users", "/meetings"} }); tx.Commit(); } using (ISession s = sessionFactory.OpenSession()) using (ITransaction tx = s.BeginTransaction()) { var user = s.Get<User>("Ayende"); Console.WriteLine("Found user {0}", user.Username); Console.WriteLine("Allowed paths:"); foreach (string allowedPath in user.AllowedPaths) { Console.WriteLine("\t{0}", allowedPath); } tx.Commit(); } } }
First, and without running the code!
- What is the expected output?
- Am I doing something wrong here?
Then, run the code, and answer them again.
You can download the project here: http://github.com/ayende/NHibernate-Challanges, sub folder TrickyBug
Comments
Hi!
"ayende" vs "Ayende"?
Non case-sensitive DB?
Tapio
It is the capitalisation of the username: "ayende" in the Save and "Ayende" (capital A) in the Get. This is fine for the Get but confuses the loading of the AllowedPaths. I am guessing that this is because Get issues a SQL query and SQL is not case sensitive and that either the AllowedPaths uses a case sensitive SQL query (which I doubt) or the matching is done in code with a case sensitive comparer.
Is it possible to map <set to ICollection <t without an IUserCollectionType?
That should have read:
Is it possible to map <set> to ICollection<T> without an IUserCollectionType?
Where is the article/blog why you should use a transaction with commit on a read operation?
I think I have to read me up on that!
The problem (I think) is with case-sensitivity
What seems to happen is that SqlServer returns the expected result but for some reason Hibernate checks for case-sensitivity when filtering the set (collection).
What is I think the most interesting part (and probably the bug you are talking about) is that NHibernate returns a User object with the Username "Ayende" (capital A) which isn't in the Database (it should be "ayende"). It looks like NHibernate uses the Get value for the Key (not the value from the Database) which isn't a problem in most cases (int or Guid).
I would expect the output to be:
Ayende
\t/meetings
\t/users
since this seems to be the intent of the fetch. However, NHibernate blindly performs a case-sensitive comparison on the join even though the database comparison is case-insensitive.
More to the point, I question whether this is really a bug in the code or a bug in NHibernate. Even though you requested to load Ayende, the database actually returned ayende and NHIbernate is ignoring that. Shouldn't the data in the database be the key instead of the data in the query?
Even so, you'll still run into problems in cases in which the collections contain mixed case (e.g. ayende in some records and Ayende in others). We had to work around this issue by creating our own case-insensitive comparer.
Interesting. One of our apps that interacts with legacy databases had a problem of this exact sort. Our quick workaround was to normalize the case across tables. I'm looking forward to details on what's happening here!
Sorry to bring up something off topic but why do you have the line tx.Commit(); line in the 2nd using block? It is just for consistency? I thought the idea of a commit is only when performing updates and inserts.
Benny,
nhprof.com/.../Alert
An array is not a valid Set. The user will not be saved.
I used to have my collections with "private set".
@Lothan - that seems to be the problem.
NHibernate seems to want a collection called [TrickyBug.User.AllowedPaths#Ayende], but it actually loads a collection called [TrickyBug.User.AllowedPaths#ayende] (based ion the database), so you get a log something like:
collection fully initialized: [TrickyBug.User.AllowedPaths#Ayende]
collection fully initialized: [TrickyBug.User.AllowedPaths#ayende]
2 collections initialized for role: TrickyBug.User.AllowedPaths
Note that it says there were 2 collections loaded! Normally only one would be found for an association like this.
@ayende - very subtle bug. Well highlighted.
What the conclusion?
Comment preview