[NHibernate] Component mapping & audit info

Pagina: 1
Acties:

Onderwerpen


  • whoami
  • Registratie: December 2000
  • Laatst online: 11:22
Ik zou eigenlijk eens wat advies willen hebben ivm het mappen van 'value types' (components) & audit information.

Het is nl. zo dat iedere tabel in mijn database 'audit-columns' bevat. Per tabel heb ik dus zowiezo 4 columns (lastupdated, created, createdby, lastupdatedby).
Om deze columns op het juiste moment van informatie te voorzien, heb ik een AuditInterceptor geschreven. So far so good, dat werkt goed.

Echter, ik zit nu in deze situatie:

- Ik heb een class 'Chapter', en een Chapter kan namen hebben in verschillende talen. Dit resulteert in m'n databank dus in 2 tabellen: Chapter & ChapterName:

code:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
CREATE TABLE [dbo].[Chapter](
    [ChapterId] [uniqueidentifier] NOT NULL,
    [ChapterCode] [varchar](25) NOT NULL,
    [CreationDate] [datetime] NOT NULL,
    [ModificationDate] [datetime] NOT NULL,
    [Version] [int] NOT NULL,
    [CreatedBy] [varchar](50) NOT NULL,
    [LastUpdatedBy] [varchar](50) NOT NULL,
 CONSTRAINT [PK_Chapter] PRIMARY KEY  
(
    [ChapterId] ASC
)


CREATE TABLE [dbo].[ChapterName](
    [ChapterNameId] [uniqueidentifier] NOT NULL,
    [ChapterId] [uniqueidentifier] NOT NULL,
    [Name] [varchar](255) NOT NULL,
    [LanguageCode] [int] NOT NULL,
    [CreationDate] [datetime] NOT NULL,
    [ModificationDate] [datetime] NOT NULL,
    [Version] [int] NOT NULL,
    [CreatedBy] [varchar](50) NOT NULL,
    [LastUpdatedBy] [varchar](50) NOT NULL,
 CONSTRAINT [PK_ChapterName] PRIMARY KEY 
(
    [ChapterNameId] ASC
)


In mijn domain-model, heb ik 2 classes gemaakt: nl. Chapter & ChapterName. Een Chapter heeft een collectie van ChapterNames. Volgens mij moet die ChapterName als een 'value type' aanzien worden.

De classes zien er zo uit:
C#:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
    public class Chapter : AuditableEntity<Guid>
    {
        public string ChapterCode
        {
            get;
            set;
        }

        private IList<ChapterName> _names = new List<ChapterName> ();

        public ReadOnlyCollection<ChapterName> Names
        {
            get
            {
                return new List<ChapterName> (_names).AsReadOnly ();
            }
        }

        public string GetChapterNameInLanguage( LanguageCode language )
        {
            foreach( ChapterName name in _names )
            {
                if( name.Language == language )
                {
                    return name.Name;
                }
            }
            return string.Empty;
        }       
    }

    public class ChapterName : IAuditable
    {
        public LanguageCode Language
        {
            get;
            set;
        }

        public string Name
        {
            get;
            set;
        }

        public Chapter Chapter
        {
            get;
            internal set;
        }

        #region Equals / GetHashcode()

        public override bool Equals( object obj )
        {
            ChapterName other = obj as ChapterName;

            if( other == null )
            {
                return false;
            }

            return this.GetHashCode () == other.GetHashCode ();

        }
        
        public override int GetHashCode()
        {
            return ( "Language:" + Language + "|Name:" + Name ).GetHashCode ();
        }

        #endregion

        #region Auditable


        public DateTime Created
        {
            get;
            private set;
        }

        public string CreatedBy
        {
            get;
            private set;
        }

        public string CreatedByPropertyName
        {
            get
            {
                return "CreatedBy";
            }
        }

        public string CreatedPropertyName
        {
            get
            {
                return "Created";
            }
        }

        public string LastUpdatedBy
        {
            get;
            private set;
        }

        public string LastUpdatedByPropertyName
        {
            get
            {
                return "LastUpdatedBy";
            }
        }

        void IAuditable.SetCreatedBy( string createdBy )
        {
            CreatedBy = createdBy;
        }

        void IAuditable.SetCreationDate( DateTime created )
        {
            Created = created;
        }

        void IAuditable.SetLastUpdatedBy( string lastUpdatedBy )
        {
            LastUpdatedBy = lastUpdatedBy;
        }

        void IAuditable.SetUpdateDate( DateTime updated )
        {
            Updated = updated;
        }

        public DateTime Updated
        {
            get;
            private set;
        }

        public string UpdatedPropertyName
        {
            get
            {
                return "Updated";
            }
        }

        public int Version
        {
            get;
            private set;
        }

        #endregion
    }

(AuditableEntity is een abstract base class die de audit-velden, een Id veld, & gethashcode / equals overrides voorziet.

De vraag is nu, hoe map ik dit het best voor NHibernate ?
Volgens mij moet ChapterName dus als een component gezien worden. Zoals je kan zien, heeft de ChapterName tabel echter ook een surrogate primary key. Daarom gebruik ik dus een id-bag om de mapping te doen.
Mijn mapping ziet er als volgt uit:

code:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
<class name="Chapter" table="Chapter" lazy="false">

    <id name="Id" column="ChapterId">
      <generator class="guid.comb" />
    </id>

    <version name="Version" column="Version" />

    <property name="ChapterCode" column="ChapterCode" />

    <idbag name="Names" access="field.camelcase-underscore" lazy="false" table="ChapterName">

      <collection-id column="ChapterNameId" type="guid">
        <generator class="guid.comb" />
      </collection-id>
      
      <key column="ChapterId" />
      
      <composite-element class="ChapterName">

        <parent name="Chapter" />
      
        <property name="Language" column="LanguageCode" />
        <property name="Name" column="Name"/>

        <property name="Created" column="CreationDate" />
        <property name="Updated" column="ModificationDate" />
        <property name="CreatedBy" column="CreatedBy" />
        <property name="LastUpdatedBy" column="LastUpdatedBy" />        
      </composite-element>
    
    </idbag>

    
    <property name="Created" column="CreationDate" />
    <property name="Updated" column="ModificationDate" />
    <property name="CreatedBy" column="CreatedBy" />
    <property name="LastUpdatedBy" column="LastUpdatedBy" />
    
  </class>


De vraag is nu, wat vind je hiervan ? Is dit OK ?

Een andere vraag is: hoe zorg ik ervoor dat de Auditinformatie mbt de ChapterNames ook 'gezet' wordt in m'n databank ?
Zoals ik al eerder vemeldde, heb ik een speciale interceptor geschreven die dit kan afhandelen, en dat ook goed doet. Echter, het ziet er naar uit dat NHibernate die interceptor enkel gebruikt voor 'entities', en niet voor components. Moet ik in dit geval dan afstappen van het 'point of view' dat chaptername een component is, en deze ook als een entity mappen (Dit wil dan zeggen dat vrijwel al mijn classes in mijn domain model entities moeten zijn)? Of zijn er andere oplossingen ?

https://fgheysels.github.io/


  • EfBe
  • Registratie: Januari 2000
  • Niet online
valuetype -> geen identity. Jouw ChapterName heeft een PK, wel degelijk identity dus, en dus kun je het niet als valuetype gebruiken. Sowieso wanneer een element op een aparte table is gemapped kan deze niet echt een valuetype zijn, want je moet refereren naar rows in die table.

Overigens zou ik de audit info uitnormaliseren in een aparte table, want je hebt nu veel overhead te laden wanneer je entities laadt, tenminste ik neem niet aan dat jij je audit-info nodig hebt wanneer je entity data gebruikt in je applicatie.

Creator of: LLBLGen Pro | Camera mods for games
Photography portfolio: https://fransbouma.com


  • whoami
  • Registratie: December 2000
  • Laatst online: 11:22
Dus, dan is mijn 2de vermoeden juist: ChapterName als entity zien, en bijgevolg alles wat ik op een table map, is een entity.

Ivm het uitnormaliseren: misschien geen slecht idee, maar ik zie het niet zitten om die refactoring te maken op dit moment. :) Ik bedoel: wat is de verdere added value om dit uit te normaliseren ? Is het ophalen van die 4 extra velden per tabel zo'n overhead ?

https://fgheysels.github.io/


  • whoami
  • Registratie: December 2000
  • Laatst online: 11:22
Ik heb ondertussen mijn mapping aangepast, zodanig dat ChapterName ook als entity wordt aanzien:
XML:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
<class name="Chapter" table="Chapter" lazy="false">

    <id name="Id" column="ChapterId">
      <generator class="guid.comb" />
    </id>

    <version name="Version" column="Version" />

    <property name="ChapterCode" column="ChapterCode" />

    <set name="Names" access="field.camelcase-underscore" lazy="false" table="ChapterName" cascade="all-delete-orphan" inverse="true">
      <key column="ChapterId" />
      <one-to-many class="ChapterName" />
    </set>
        
    <property name="Created" column="CreationDate" />
    <property name="Updated" column="ModificationDate" />
    <property name="CreatedBy" column="CreatedBy" />
    <property name="LastUpdatedBy" column="LastUpdatedBy" />
    
  </class>

  <class name="ChapterName" table="ChapterName" lazy="false">

    <id name="Id" column="ChapterNameId">
      <generator class="guid.comb" />
    </id>

    <property name="Language" column="LanguageCode" />
    <property name="Name" column="Name" />

    <many-to-one name="Chapter" column="ChapterId" class="Chapter" />

    <property name="Created" column="CreationDate" />
    <property name="Updated" column="ModificationDate" />
    <property name="CreatedBy" column="CreatedBy" />
    <property name="LastUpdatedBy" column="LastUpdatedBy" />

  </class>

Het probleem is nu dat, als ik op de set de joinmode 'fetch' specifieer, NHibernate blijkbaar een soort van cartesiaans product doet.
Ik bedoel: De query die NHibernate genereert is goed.
Dit lijkt op iets als:
code:
1
select * from chapter left outer join chaptername on chapter.chapterId = chaptername.chapterId

Dit is correct.
Hiermee krijg je natuurlijk één resultset die voor iedere chapter / chaptername een record bevat.
Nu zou je verwachten dat NHibernate hier wel overweg mee kan. Dat NHibernate dus voor ieder Chapter een chapter-entity maakt, en de bijhorende chapternames daaraan toevoegt, maar niets is minder waar.
Voor ieder record in de recordset maakt hij gewoon een Chapter instance aan .... Ik zie niet wat ik hier eigenlijk fout doe ...

https://fgheysels.github.io/


  • whoami
  • Registratie: December 2000
  • Laatst online: 11:22
Om nog eens terug te komen op het entity / value types verhaal:

Eigenlijk zou ik in mijn bovenstaande code dus wel een value type 'Name' kunnen maken , die 2 properties bevat: een languagecode & een description , en deze dan als component mappen in de entity ChapterName ?

https://fgheysels.github.io/


  • EfBe
  • Registratie: Januari 2000
  • Niet online
whoami schreef op woensdag 17 september 2008 @ 12:57:
Dus, dan is mijn 2de vermoeden juist: ChapterName als entity zien, en bijgevolg alles wat ik op een table map, is een entity.

Ivm het uitnormaliseren: misschien geen slecht idee, maar ik zie het niet zitten om die refactoring te maken op dit moment. :) Ik bedoel: wat is de verdere added value om dit uit te normaliseren ? Is het ophalen van die 4 extra velden per tabel zo'n overhead ?
Bv 1000 entities * 4 velden, en dat dan elke keer. Ik denk dat het wel degelijk uitmaakt. Auditing is puur logging: je wilt dat bij data-wijzigingen iets wordt gelogd, maar met logs is het OOK zo dat je niet wilt dat je bij je normale werkzaamheden er mee te maken krijgt. M.a.w. auditing info hoort eigenlijk niet in een entity thuis maar erbuiten. Je ChapterName entity heeft evenveel auditing fields als normale fields, en eigenlijk maar 2 data fields!
whoami schreef op woensdag 17 september 2008 @ 16:24:
Om nog eens terug te komen op het entity / value types verhaal:

Eigenlijk zou ik in mijn bovenstaande code dus wel een value type 'Name' kunnen maken , die 2 properties bevat: een languagecode & een description , en deze dan als component mappen in de entity ChapterName ?
En hoe ga jij dan refereren aan de row in de ChapterName table? In edge cases krijg je het wellicht voor elkaar, maar NHibernate's docs zeggen zelf al dat je niet overboard moet gaan met entity mappings binnen entities bv. Valuetypes zijn andere types, die hebben geen identity: Zie valuetype als een type die 1 of meerdere fields van entity E groepeert onder een nieuwe naam, bv Customer.Street/City/HouseNo/Country onder Address. Je hebt dan in Customer een field VisitingAddress van het type Address en in je mappings map je Address.City op het City field in de Customer table etc.

In jouw geval werkt dat niet, want je relatie tussen Chapter en ChapterName is 1:n, door de taalcode. per definitie is een relatie tussen attribute en entity 1:1, dus ChapterName kan geen attribute zijn van een valuetype, het MOET een entity zijn.

Dat NHibernate zich geen raad weet met de hierarchische fetch is logisch, nhibernate kent geen hierarchische fetches, alhoewel het wel mogelijk moet zijn om voor een chapter automatisch de bijbehorende chaptername entities te laden omdat chapter de eigenaar is van 'chaptername' maar zo diep zit ik niet in de nhibernate mappings.

[ Voor 49% gewijzigd door EfBe op 17-09-2008 17:14 ]

Creator of: LLBLGen Pro | Camera mods for games
Photography portfolio: https://fransbouma.com

Pagina: 1