We use (and generally like) the ORM NHibernate for MS SQL Server data access, but we understand why you might not. Ohhhh, the things we have seen! We’d like to share some of our favorite horrors, so that you can avoid them.
Originally, our Repository<TEntity> base class implemented IEnumerable<TEntity>. This allowed us to do clever things like
foreach (var c in CountryRepository) { /* … */ }
This simple iteration was handy, especially for reference tables (e.g., Countries, States/Provinces, categories of shippable things). This “feature” turned out to be a giant, carnivorous bug. Why you ask? Because LINQ is powerful magic.
It first started when instead of IUserRepository.Get(userId) someone wrote IUserRepository.First(x => x.UserId == userId). Let’s take a look at the SQL that is generated by these statements.
-- IUserRepository.Get(userId) SELECT users0_.Id as Id2_0_, users0_.Username as Username2_0_, -- ... FROM [Users] users0_ WHERE users0_.Id=@p0;
But notice the missing WHERE clause in this next case:
-- IUserRepository.First(x => x.UserId == userId) SELECT users0_.Id as Id2_0_, users0_.Username as Username2_0_, -- ... FROM [Users] users0_
What’s really happening in the latter case is that the code we accidentally wrote is
Enumerable.First(session.Query(), u => u.Id == usersId)
instead of what the developer thought they were writing:
Queryable.First(session.Query(), u => u.Id == userId)
Unless you don’t mind loading your entire User’s table into memory every time you look someone up (for us this only took 12 GB of RAM), don’t allow your Repositories to implement IEnumerable.
We can’t tell you whether you should use nvarchar or varchar in your application, but we can say that the mappings for varchar suck (hint: start by using .Map(...).CustomType("AnsiString") to map your varchar mappings). Regardless of what you choose, you must know exactly what values you’re putting into your database, and equally important, what you’re getting back out.
The problem: this guy ಠ_ಠ
Or rather, the guy who chose that as his username.
First we mapped an Entity’s string property to a varchar column (non-Unicode string data) instead of nvarchar (Unicode string data). However, what we actually store in the database as his username is “?_?” since every high Unicode character is stored as a single question mark.
Second, we included a non-slug-ified, user-defined value (username) in the canonical URI of an API resource. In other words, the username “JohnDoe” would be included in the API URI as the path
/users/johndoe
But that guy… his URI is
/users/?_?
Got that? We made this transformation by mistake:
we turned his disapproving face into tears
For anyone who knows RFC-3986, you will understand why this doesn’t work. ‘?‘ separates the path from the query in a URI. Sadly it took us a few users who were unable to retrieve their profiles before we figured this one out.
Here’s the lesson: Know what you’re storing in your database. If you choose to store non-Unicode string data, prohibit arbitrary Unicode characters in data entry and user input validation. Otherwise, this is how we feel about you: ಠ_ಠ
You can’t have a Halloween blog without zombies, right?
Suppose you’re programming along, minding your own business, using your oh-so-convenient Session-per-Request pattern, when you come upon a stack trace containing CheckNotZombied(). In our case, this is another issue that arose from the intersection of a couple different problems.
At first we came across a blog stating this can be caused by one of two problems:
- The database was throwing exceptions with a severity of 20 or
higher - A transaction is shared by multiple threads
We initially hoped the problem was caused by the former since it would involve just following the stack trace. Naturally, it ended up being the latter.
It turns out that years ago, we wrote some slow code in a non-critical, uncommon code path (mobile push notifications). But the database query it ran really was too slow, so after receiving numerous timeout exceptions, we rewrote it to be more performant swallowed all exceptions and ran it on another thread.
It took about 3 years for the problem to arise, but then, every once in awhile, a random zombie exception would creep up in random places. When we finally traced the calls, we found that they were coming from inside this code!
ISession session = mySessionFactory.OpenSession(); new Thread(() => { try { // Intensive queries with session here... } catch (Exception) { if (session.Transaction != null && session.Transaction.IsOpen()) { try { session.Transaction.Rollback(); } catch (Exception) { // Q: What's the worst that could happen? // A: It’s pretty bad. } } } }).Start();
The non-critical, uncommon code path had became more popular over three years. Turns out the whole “smart phone” ended up being more than a fad. Luckily we were able to fix the problem fairly easily since uShip’s native mobile app necessiated a rewrite of push notifications using Amazon SNS.
Here are a couple of guidelines to help you avoid such a terrible fate:
- Trace, profile, and log ’til it hurts
- Don’t share NHibernate Sessions between threads
- When you want to accomplish work concurrently, and the work is IO-bound, prefer async methods to threads
- And don’t forget, just like zombies, sometimes all your ORM needs is a little love
Conclusion
NHibernate’s pluses have definitely outweighed the minuses for us, but it’s not without it’s hidden horrors. Are you able to relate? Share your NHibernate scary stories in the comments! And Happy NHalloween!