Avoiding non string identifiers in RavenDB
RavenDB, as of 4.0, requires that the document identifier will be a string. In fact, that has always been the requirement, but in previous versions, we allowed you to pretend that this isn’t the case. That has led to… some complexities, because people had a number id in their model, but inside RavenDB that was represented as a string, always.
I just got the following question:
In my entities, can I have the Id property of any type instead string to avoid primitive obsession? I would use a generic Id<tentity> type for ids. This type can be converted into string before saving in DB by calling ToString() and transformed from string into Id<tentity> (when fetching from DB) by invocation of static method like public Id<tentity> FromString(string id).
The short answer for this is that no, there is no way to do this. A document id in your model has to be a string.
The longer answer is that you can absolutely do this, but you have to understand the divergence of your entity model vs. the document model. The key is that RavenDB doesn’t actually require that your model would have an Id property. It is usually defined, because it makes things easier, but that isn’t required. RavenDB is perfectly happy managing the document key internally. Combine that with the ability to modify how documents are converted to entities, and you have a solution. Let’s look at the code…
And here is how it looks like:
The idea is that we customize a few things inside of RavenDB.
- We tell the serializer that it should ignore the UserId property
- We tell RavenDB that after creating an entity from the server, we should setup the Id property as we want it.
- We do the same just before we store the entity in the server, just to be sure that we got the complete package.
- We disable the usual identity generation logic for the documents we care about and tell RavenDB that it should ignore trying to set the identity property on the document on its own.
The end result is that we have an entity with a strongly typed identifier in our model. It took a bit of work, but not overly so.
That said, I would suggest that you should either have a string identifier property in your model or not have one at all (either option takes no code in RavenDB). Having an identifier and jumping through hoops like that tend to make for awkward experience. For example, RavenDB has no idea about this property, so if you need to support queries as well, you’ll need to extend the query support. It’s possible, but shows that there is additional complexity that can be avoided.
Comments
Thanks Ayende!
For me, the ideal ID class is like this:
Using the example of your book "Inside RavenDB 4.0", the
Child
class should be like this:So, if in business code I have the child and I need the parent to do some operation with the parent, I can do:
Usually, before getting the child from DB, I should called
Include()
method of session and the parents must be stay in the entity map, so the await operation must be finished inmediatly. Is this correct?When I need to show data in the presentation layer, I usually use proyections from query with
ProyectInto()
method. Imagine this model class:So, I think that I can use a proyection like this:
Is this proyection well done?
I didn't test any code of this comment, I'll test tomorrow and I'll post a reply.
Thanks you Ayende!
Ivan, This would _work_, yes, but the projections wouldn't understand the
await
syntax at all, or indeed anyasync
code. That is why I mentioned that it is better to use this sparingly, because you run into integrations issues throughout the stack. You now have a different model than what RavenDB expects. You can usesession.Load
in a projection, but not calls like this.And in your
GetEntityAsync
, you'll need to be sure to use the same session (scoped?) in order to take advantage of the include.Hi Ayende, thanks for your response.
I'm using the same session because is scoped in the container, so the session is created once per HTTP request (using ASP.NET Core). I'll investigate about the proyection problem the next week.
Comming back to the example, I need to use HiLo algorithm with stronge-typed generic
Id<TEntity>
class and use it into one-to-many relationships, so I wrote a basicPerson
class:I wrote also a custom JsonConverter to save ID as string in database:
Then, I make some instances of this class and save it into db:
To generate id with the HiLo algorithm and assing it into
Id
property, I modified theAsyncDocumentIdGenerator
convention like the post, but detecting if entity has aId
property of typeId<>
At this point, exception is thrown in first
StoreAsync
(mother) call:To avoid this exception, I added modiefied
FindIdentityProperty
convention like the post, to detect the entities that have Id property of typeId<>
:Doing this, I can save and load entities from database using Id<TEntity> as identity type instead of string with HiLo generator. The main problem is the lack of HiLo generator. There are any form to access the HiLo generator instance anywhere?
Ivan,
RavenDB enforce the fact that identity properties must be a string.
You can make it appear to RavenDB that this doesn't have an id property, which is what the code in my post does.
I'll recommend you to cache the
hiLoGenerator
instances, and reuse them. They are meant to be kept around for the lifetime of the store (remember to dispose of them, too).But it would be easier all around to do:
I tried something like this to reuse the default document generator, but I get NullReferenceError in line
var generatedId = await defaultDocumentIdGenerator(dbName, entity);
when I callsession.StoreAsync(mother)
.I created a thread into RavenDB google group about this: https://groups.google.com/forum/#!topic/ravendb/Izpdcc7nGA8
Comment preview