Gebruik geen GUIDs in NHibernate

 
12 juli 2011

Iedere tabel heeft een primaire sleutel. De vraag is wat bepaalt deze waarde? Er wordt vaak gebruik gemaakt van een GUID, maar er is een beter alternatief beschikbaar.

NHibernate is een Object-Relational-Mapper. Het enige doel van een ORM is om alles dat specifiek is voor een relationeel model, niet in je code hoeft te staan. De impedence mismatch laat je over aan de ORM. Een primaire sleutel is zo’n relationeel-specifiek concept. Een object-model zou dit niet moeten kennen. Dit wordt door sommigen zelfs gezien als een anti-pattern [citation needed].

De NHibernate documentatie geeft het volgende weer over de id mapping:

<id
    name="PropertyName"                      (1)
    type="typename"                          (2)
    column="column_name"                     (3)
    unsaved-value="any|none|null|id_value"   (4)
    access="field|property|nosetter|ClassName(5)">
    <generator class="generatorClass"/>
</id>

(1) name (optional): The name of the identifier property.
(2) type (optional): A name that indicates the NHibernate type.
(3) column (optional – defaults to the property name): The name of the primary key column.
(4) unsaved-value (optional – defaults to a “sensible” value): An identifier property value that indicates that an instance is newly instantiated (unsaved), distinguishing it from transient instances that were saved or loaded in a previous session.
(5) access (optional – defaults to property): The strategy NHibernate should use for accessing the property value.

De 5 punten daaronder geven aan dat geen attribuut verplicht is, maar dat is alleen om de XML validatie te laten slagen. NHibernate heeft wel een eis:

Als PropertyName niet is ingevuld (d.w.z. het object heeft geen property dat het id bevat), dan moet het type worden aangegeven, omdat NHibernate dit niet op basis van reflectie kan doen.

De onvermijdelijke vraag is: als wij in onze objecten het id niet bijhouden, hoe weet NHibernate het dan wel?

Identity map

Eigenlijk heel simpel. In de implementatie van de Session staat een referentietabel (Dictionary<object, Pk> of Map<Object, Pk>). Als het object een id heeft gekregen, dan wordt dit object in deze referentietabel opgeslagen. Zo’n referentietabel heet een identity map.

Maar de identity map  is per sessie! Als de sessie wordt gesloten, dan verdwijnt deze informatie ook uit NHibernate. De enige twee manieren om deze identity map weer te vullen is met queries en Session.Update(object, id).

De Session bevat een methode, GetIdentifier(object), waarmee de primaire sleutel uit die referentietabel kan worden opgehaald. Als je het object-model id-vrij wilt houden, dan zul je vaker dit patroon gebruiken in de services e.d.:

var customer = ...;
int id;
using (var session = SessionFactory.OpenSession())
using (var tx = session.BeginTransaction())
{
    session.Save(customer);
    // id = customer.Id; --> Dit geeft een compiler fout omdat Customer geen Id property heeft, zoals het zou moeten
    id = session.GetIdentifier(customer);
    tx.Commit();
}
public void Approve(int customerId)
{
    using (var session = SessionFactory.OpenSession())
    using (var tx = session.BeginTransaction())
    {
        var customer = session.Get<Customer>(customerId);
        customer.Approve();
        tx.Commit();
    }
}

Je kunt dus wel het object-model id-vrij maken, maar de gehele  applicatie kan niet.

Identitifier generators

NHibernate wordt standaard geleverd met 12 verschillende id-generators. Alle identity generators zijn onder te verdelen in pre-assigned en post-assigned. Pre-assigned generators genereren een id voordat een object toegevoegd wordt aan een tabel, een post-assigned generators laat het id over aan de database en vraagt achteraf wat het id is geworden.

Post-assigned generatoren moeten zoveel mogelijk worden vermeden. De enige keer dat dit bruikbaar is, is als er gebruik gemaakt wordt van een legacy database. Een post-assigned generator voert namelijk een extra query uit (“SELECT myseq.NEXTVAL FROM DUAL;” of “SELECT LAST_INSERT_ID();”). Omdat de database een extern systeem is, moet je proberen er zo weinig mogelijk mee te communiceren.

Een pre-assigned is beter. Dan kan NHibernate alvast identifiers genereren voordat er maar één enkele INSERT-statement uitgevoerd wordt. Als de sessie vervolgens wordt geflusht, dan kunnen alle INSERT- en UPDATE-statements in één keer worden uitgevoerd.

Van de pre-assigned generators, lijkt de GUID tegenwoordig een van de populairste. Daar hoop ik een kleine verandering in te brengen en de aandacht te richten op de ondergewaardeerde hi/lo algoritme.

Waarom geen GUID?

Je mag dan wel je object-model relationeel-vrij maken, dat wil niet zeggen dat je in de rest van de applicatie kunt doen alsof je geen database gebruikt. De technieken die voor een database belangrijk zijn, blijven relevant. Een GUID is natuurlijk niet altijd slecht, maar voor een database zijn er wel enkele belangrijke aandachtspunten:

  • GUID neemt 4x meer ruimte in beslag dan een INT
  • Men beweert dat het een negatieve invloed heeft op indexen [citation needed]
  • Je kunt een GUID niet gebruiken in een MIN/MAX query
  • GUIDs zijn niet handig om over te communiceren (“Het probleem zit in de data van de order met id 70c13422-5639-4dd0-ab62-8b96f5f770e” i.p.v. “Het probleem zit in de data van de order met id 15”
  • Dit geldt ook voor de informatie die je in je log beschrijft

Wat is het hi/lo algoritme?

Een id dat met dit algoritme wordt gegenereerd bestaat uit twee delen. De high-value en de low-value. Een SessionFactory vraagt per tabel op wat de huidige high-value is (als dat nodig is) en verhoogt deze. De low-value kan varieren tussen 0 en de capaciteit. De capaciteit is het aantal ids dat gegenereerd kan worden totdat een nieuwe high-value moet worden opgevraagd. Het uiteindelijke id wordt samengesteld d.m.v. de volgende formule:

high-value * capaciteit + low-value

Als de huidige high-value 17 is en de capaciteit is 100, dan kan in die SessionFactory veilig ids genereren tussen 17000 en 17999 zonder dat de database erbij betrokken is.

Dit algorime heeft enkele belangrijke voordelen:

  • Alle voordelen van een GUID
  • Het zijn simpele getallen, dus je kunt er makkelijker over communiceren
  • Dit geldt ook voor de informatie die je in je log beschrijft
  • Omdat het simpele getallen zijn, kun je ze ook makkelijker onthouden en herkennen. Zeker als je de database in moet duiken om een probleem te achterhalen
  • De ids zijn oplopend en daardoor beter voor de index die op de primaire sleutel ligt

De mapping configuratie is:

<generator class="hilo">
    <param name="table">hi_value</param>
    <param name="column">next_value</param>
    <param name="max_lo">100</param>
</generator>

Je hebt dus een extra tabel nodig, met twee kolommen: de kolomnaam voor de eerstvolgende high-value en de capaciteit. Alle parameters zijn optioneel, dus het volgende is voldoende:

<generator class="hilo" />

Dit is mijn richtlijn, anderen hebben weer andere richtlijnen. En een richtlijn is geen regel, dus je mag er vanaf wijken, maar de voordelen zijn groot genoeg om het op z’n minst te overwegen.


Werken met ?
Kijk dan bij onze mogelijkheden voor zowel starters als ervaren engineers.


Categorieën: Development

Tags: , , ,


Reacties (5)

  • Barend schreef:

    Index fragmentatie schijnt geen issue te zijn als je de guid.com generator gebruikt. Zie bijvoorbeeld de volgende post:
    http://davybrion.com/blog/2009/05/using-the-guidcomb-identifier-strategy/

    Geplaatst op 12 juli 2011 om 21:26 Permalink

  • Ramon Smits schreef:

    Ten tweede, het voordeel dat je noemt dat int leesbaar is… dat is zo tot zekere mate namelijk tot in de duizend tallen maar communiceer jij eventjes de sleutel 1143481647? Hierdoor zijn namelijk niet technische sleutels voor bedacht. Bijvoorbeeld klant code + jaartal + order = mijnklant_2010_00316. Maar deze sleutels zijn te groot om te gebruiken als primary key omdat tabellen die deze waarde als foreignkey gebruiken hierdoor onnodig groot worden.

    Geplaatst op 12 juli 2011 om 16:33 Permalink

  • Ramon Smits schreef:

    Het gebruik van guids is niet aan te raden als primary key indien de clustered index op basis van de primary key is. Dit is standaard wel het geval maar dient bij gebruik van niet oplopende guids een andere kolom te zijn. Bijvoorbeeld een insertedAt kolom.

    Je kunt beter zeggen.. gebruik hi/lo indien de voordelen van guids niet gebruikt worden. Bijvoorbeeld indien de guid server side gegenereerd wordt zoals vaak het geval indien data gecorreleerd wordt op een hoger niveau zoals in een service welke in zijn geheel niet direct communiceerd met een database.

    De titel van het artikel is dan ook nogal slecht.

    Geplaatst op 12 juli 2011 om 16:27 Permalink