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.