I'd like to have some advice regarding mapping components (value types) and audit information.
I will explain a little bit in more detail:
First of all, it is necessary to know that each table in my database contains audit-trail columns. That is, each and every table contains a column 'CreatedAt' 'LastModifiedAt', 'CreatedBy' and 'LastModifiedBy' columns.
These columns need to be updated when appropriate. (That is, on every update the LastModifiedAt and LastModifiedBy columns need to be updated, on every insert, all four columns need to be given a value).
Now, what I have is a table 'Chapter' which is related to a table 'ChapterName'. So, in fact each Chapter can have one to multiple names. (This is necessary since I keep track of names in different languages).
The tables look like this:
Code:
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
)
As I see it, in my domain model, the Chapter class is an entity and ChapterName should be regarded as a value type.
My classes look like this:
Code:
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
}
The AuditableEntity base class contains some basic logic / fields (Id, the Audit properties, Equals & Gethashcode overrides).
Now, the question is, how do I map this situation best ? As you can see, the ChapterName table has a surrogate primary key as well (ChapterNameId).
Therefore, I think i should map this using an id-bag.
My mapping looks like this:
Code:
<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>
As you can see, I've mapped the Chapter entity to the Chapter table, and the ChapterNames collection as a 'component collection' using an idbag. (I've used an idbag so that the primary key column in the ChapterName table can be given a value.
To update the audit-information, i've created an AuditInterceptor which I use in my Session. This works ok for the Chapter class. However, when I add a ChapterName to the Chapter, it seems that the interceptor doesn't intercept the chaptername components.
Now, I have 2 questions:
- what do you think about my mappings ? Is the mapping that i've showed, ok ?
- how can I make sure that the audit information is correctly set for the ChapterNames as well ? I can solve it when I map the ChapterName class as an entity, but this is maybe not a good practice ?