Yesterday, I spent some time setting up and optimizing SysCache2 caching. (By the way, if you're doing this kind of NHibernate tuning, NHProf - a devlicio.us friend - is crucial, it would have definitely been a much tougher slog without it) With caching turned on, one of our previously well-functioning classes was consistently giving the following error EVERY time we tried to modify and save an instance:
ReadOnlyCache: Can't write to a readonly object
Looking this up, I ran across a LOT of information about how this meant the cache line in the mapping was wrong - that it needed to be marked read-write, not read-only. The odd thing was, this is what the cache line in our mapping was like already:
<cache usage="read-write" region="SomeRegion" />
So, our problem was NOT the obvious issue that our object was improperly marked as readonly. But... after a lot of flailing around, the solution emerged. Check out this definition of the ID on this entity:
<id name="Id" column="EntityID" type="Int32" unsaved-value = "0">
<generator class="assigned" />
Note the use of "assigned" for the id generator. It means NHibernate doesn't control the id-it's manually managed. Now if you're familiar with the assigned generator, you know that it's a somewhat troublesome thing to work with-specifically, if you call SaveOrUpdate on an entity using it, NHibernate won't know whether it's a new entity or not on its own, because NHibernate tracks the state of the entiy using the id. This affects cascade saves as well, so assigned ids aren't fun. If you take over managing that id yourself, NHibernate doesn't have anything to hang its tracking off of. Assigned ids are something you should probably avoid for new applications, but sometimes that's not an option for legacy apps.... If you NEED to use an assigned id there's an easy way to get this entity acting like a normal NHibernate entity-you can give NHibernate something to use like this:
<version name="LastModifiedOn" type="timestamp" column="LastModifiedOn" />
-with the matching column, and the matching property in the class - or maybe even just using the access="field.." option in the mapping so it's not mistakenly taken to be something to be part of the class's regular api by another programmer-this field is NOT FOR YOU, it's for NHibernate. Look at it if you want, but do not touch! Note that if you add a column like this to an existing table, you're going to have to prepopulate the existing rows for your your new column with an initial datetime or you'll get errors-any date at all, so long as it's in the past. 1/1/2009 12:00:00 is as good a value as any....
What does this have to do with my caching problem? The particular entity I was getting the erroneous readonly caching problems on didn't have the <version> property-a bad state of things for an assigned id entity. Oddly, it *was* in fact saving properly using SaveOrUpdate - perhaps it was the unsaved-value = "0" on the id allowing it to work, maybe not. Anyway, the solution ended up being adding a <version> property and column, and then it worked like a charm. I probably should have caught that this entity was missing <version> earlier, but it seemed to work ok, and the potential problem stayed hidden until the entity got thrown into the cache.
By the way, if you don't like <version>, there are alternatives, like IInterceptor.IsTransient()
10-08-2009 8:53 AM