Could you hire me? Contact me if you like what I’ve done in this article and think I can create value for your company with my skills.

January 14, 2008 / by Zsolt Soczó

SQL Server 2008 újdonságok 13. – HierarchyID adattípus 4.

Pár záró gondolat a típusról.

Eddig mindig arra használtam, hogy egy hierarchiában a node-okhoz legyen közvetlen pointerünk, ezzel egyes lekérdezéseket jelentősen fel lehetett gyorsítani. Ennek ellenére ez a típus nem más, mint egy nagyon tömör, számunkra elérési útként értelmezhető adatot tároló valami. Hogy ez az elérési út passzoljon a tényleges hierarchiához csakis a mi felelősségünk, az égvilágon senki nem fogja biztosítani, hogy a HiearchyID-ben tárolt adatnak bármi köze is van a valósághoz. Nem olyan, mint pl. egy foreign key-jel védett kapcsolat, ahol azért csökken a baromkodás esélye.
A típus igen kompakt, pár bájtot vesz csak igénybe a tárolása, még nagyon nagy fák esetén is. Ha kicsit utána akarunk nézni, hogyan implementálták, akkor reflectorral meg kell nyitni a Microsoft.SqlServer.Types.dll assemblyt a GAC-ból, ebben vannak a CLR SQL típusok implementálva (a többi is, amelyekről a későbbi cikkekben még lesz szó).

A File Disassembler pluginnal teljes egészében vissza lehet fejteni forráskódra, kicsit nézzünk bele. Habár C++/CLI-ben írták, C#-ként mutatok be egy-két részletet, mégha így pár dolog csúnya is (nincs const a C#-ban pl.). Maga a típus így van deklarálva:

[SqlUserDefinedType(Format.UserDefined, IsByteOrdered=true, MaxByteSize=0x37c, Name="SqlHierarchyId")]
public class SqlHierarchyId : IBinarySerialize, INullable
{
    // Fields
    private OrdPath ordpath;
...
}

Mivel a típus implementálja az IBinarySerialize interfészt, amikor le kell tárolni a típus adatait, a Write metódust hívja meg a szerver. Ennek lényegi része:

public void Write(BinaryWriter w)
{
    w.Write(this.ordpath.m_bytes, 0, (this.ordpath.m_bitLength + 7) / 8);
}

Azaz az OrdPath m_bytes adattagja, ennek bináris szerkezete dönti el, hogy indexelésnél hogyan viselkedik a típus. Ezt ügyesen úgy rakták össze, hogy mélységi módon rendezze az általa reprezentált fát.
Láthatóan a lényeg igazából az OrdPath típusban van, az SqlHieararchyID csak egy facade hozzá.

Az OrdPath belülről már bonyolultabb:

internal class OrdPath
{
    // Fields
    public ushort m_bitLength;
    public ushort[] m_bitOffsets;
    public byte[] m_bytes;
    public uint modopt(IsLong) m_level;
    public uint modopt(IsLong) m_parentBitLengthIndex;
    public uint modopt(IsLong) m_parsedLevels;
    public SubType m_subType;
...

Amikor stringből képeznek HierarchyID-t, akkor is az OrdPath dolgozik (Parse() hívás vagy SQL CAST vagy CONVERT). SqlHiearchyId.Parse:

public static SqlHierarchyId Parse(SqlString input)
{
    string chDottedString = input.Value;
...  
    SqlHierarchyId id = new SqlHierarchyId();
    id.ordpath = <strong>new OrdPath(chDottedString);</strong>
    return id;
}

Akit érdekel, az OrdPath konstruktorban megnézheti a konkrét bitkolbászolást, az már túl hosszú, hogy itt kielemezzem.

Érdemes még megnéznük, hogyan kell az adatokat módosítani, a fát kezelni a HierarchyID jelenlétében.

Csak kóstolóként mutatok egy példát, amiben Józsit előléptették, új főnököt kap a hierarchia magasabb szintjén.

begin tran

declare @jozsi hierarchyid = 
(select OrgNode 
from HumanResources.NewOrg 
where LoginID = 'adventure-works\dylan0') --2/1/4

declare @jozsiujfonoke hierarchyid = 
(select OrgNode 
from HumanResources.NewOrg 
where LoginID = 'adventure-works\david0') --/1/

select 
@jozsi.ToString() Jozsi, --/2/1/4/
@jozsiujfonoke.ToString() JozsiUjFonoke, -- /1/
@jozsiujfonoke.GetDescendant(null, null).ToString() ElsoBeosztott, --/1/1/
@jozsiujfonoke.GetDescendant(null, 
  @jozsiujfonoke.GetDescendant(null, null)).ToString() ElsoBeosztottElotti --/1/0/

update HumanResources.NewOrg
set OrgNode = @jozsiujfonoke.GetDescendant(
  null, @jozsiujfonoke.GetDescendant(null, null))
where OrgNode = @jozsi

rollback tran

Az egészben a GetDescendant metódus a kulcs. Ez sokféleképpen tud visszaadni gyereket, attól függően, hogy milyen két paramétert kap. A fontosabb esetek:

1. If parent is not NULL, and both child1 and child2 are NULL, returns a child of parent.

2. If parent and child1 are not NULL, and child2 is NULL, returns a child of parent greater than child1.

3. If parent and child2 are not NULL and child1 is NULL, returns a child of parent less than child2.

4. If parent, child1, and child2 are all not NULL, returns a child of parent greater than child1 and less than child2.

Az ElsoBeosztott az 1. szabály alapján képeztetett, az a child szemmel láthatóan az első jelenti (legalábbis most). Aztán a 3. szabály értelmében lekértem az ElsoBeosztottElotti id-t. Ide pozícionálom újra az update segítségével Józsit, így ő lesz az első gyerek a főnök alatt. Biztos boldog ettől. :)

UPDATE!

Babatologatás közben rájöttem, hogy a fenti példa hibás, elrontja a hierarchiát, mert csak egy node-ot mozgatok, nem egy komplett részfát, így árván maradnak Józsi beosztottjai. A következő részben bemutatom a javítást.

Zárásul érdekességként nézzük meg, hogy képezik a node azonosítót, ha két egész szám között már nincs hely további egésznek:

select @jozsiujfonoke.GetDescendant(
@jozsiujfonoke.GetDescendant(null, 
  @jozsiujfonoke.GetDescendant(null, null)).ToString(),
  @jozsiujfonoke.GetDescendant(null, null).ToString()).ToString()

Kimenet:

/1/0.1/

Kicsit vad, de mi mást lehetne kitalálni?

Could you hire me? Contact me if you like what I’ve done in this article and think I can create value for your company with my skills.