Question

I am currently working on an old ASP application with a SQL Server 2000 database which we are trying to port to newer technologies using .NET and NHibernate.

In that DB, all the tables have an composite ID made like this :

CREATE TABLE [Languages](
    [languageIncId] [int] IDENTITY(1,1) NOT FOR REPLICATION NOT NULL,
    [languageSqlId] [smallint] NOT NULL,
    ...
    [createdByIncId] [int] NOT NULL,
    [createdBySqlId] [smallint] NOT NULL,
    ...
    [lastModifiedByIncId] [int] NULL,
    [lastModifiedBySqlId] [smallint] NULL,
    [rowguid] [uniqueidentifier] ROWGUIDCOL  NOT NULL,
        ...
 CONSTRAINT [PK_Languages] PRIMARY KEY CLUSTERED 
(
    [languageIncId] ASC,
    [languageSqlId] ASC
)WITH (PAD_INDEX  = OFF, STATISTICS_NORECOMPUTE  = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS  = ON, ALLOW_PAGE_LOCKS  = ON, FILLFACTOR = 90) ON [PRIMARY]
) ON [PRIMARY]

GO

That is, each table's primary key is made of :

  • XXXSqlId which is the Id of the SQL Server instance where the item was created
  • XXXIncId which is an IDENTITY field incremented when a new row is inserted

The point of the SqlId is that when replication happens, records are moved from a database to another and duplicate XXXIncId might happen. There is unfortunately no changing the database schema as so many applications rely on it (which is, indeed, painful).

This also means that whenever relations exist between tables, both fields need to be provided as in createdByIncId , createdBySqlId.

I am looking for the best way to map this structure with NHibernate (Fluent or not) but I am blocked. I have considered the following solutions :

  • Using a Composite-ID with SqlId and IncId (the most natural solution) but it does not work because the IncId is generated by the database, and CompositeIDs do not support the "generated" attribute
  • Ignoring completely those fields and consider "rowguid" as the real ID : this works well as long as I do not try to play with the relation between entities, which should use the "composite ID" as the link too...
  • Using a custom Composite user type (ICompositeUserType) : but this can not be used as an ID for an entity.

My question is rather similar to question 1615647, but the answer is not satisfaying for me.

Any idea of another lead to follow ?

Was it helpful?

Solution

We have tried 2 leads :

  • custom insert SQL queries (<sql-insert>) that do not include the IDENTITY field, and run an extra SQL query after each insert to populate the property manually. This worked fine in simple use cases (simple SELECT/INSERT), but did not work as soon as you are referencing other entities and collections of other entities...

  • use another field as the primary key : each table also has a rowguid column. We use this as the real primary key for NHibernate. This worked fine in our test cases of cascaded insert/updates. Our composite IDs are just regular components that are marked as generated on insert and not included during Update, like this :

    Component<CustomCompositeIdType>(e => e.Id,
        p =>
        {
            p.Map(i => i.SqlId, "languageSqlId")
                .Insert()
                .Not.Update()
                .Not.Nullable();
            p.Map(i => i.IncId, "languageIncId")
                .Not.Update()
                .Not.Nullable()
                .Generated.Insert();
        });
    

Then, for the references between entities, we specify the 2 columns that are used to establish the relationship :

Many-to-one :

    References<LanguageEntity>(e => e.Language)
        .Columns("languageSqlId",
                 "languageIncId")
        .PropertyRef(l => l.Id)
        .Fetch.Join()
        .Cascade.None();

One-to-many :

    HasMany<InterfaceTranslationEntity>(e => e.Translations)
        .KeyColumns.Add("InterfaceSqlId",
                        "InterfaceIncId")
        .PropertyRef("Id") //name of the property in InterfaceTranslationEntity
        .Inverse();
Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top