Generating NHibernate Classes for MSSQL in TSQL

February 19, 2012

This series describes the generation of C# classes and database access for NHibernate 2.1.2GA and FluentNHibernate 1.0. These are not the most current versions, but I need to document my existing code to prepare support of current versions of these libraries. I have previously written about NHibernate accessing Oracle in various post.

We generate the C# classes from the meta model contained in SQL Server system catalog views. However, generation does not directly access the catalog views, but caches information in meta tables that can hold additional information on tables and columns:

CREATE TABLE [dbo].[Meta_Table](
    [OID] [int] IDENTITY(1,1) NOT NULL,
    [ID] [nvarchar](50) NOT NULL,
    [TableType] [varchar](1) NOT NULL,
    [UseDAL] [bit] NOT NULL,
    CONSTRAINT [PK_Meta_Table] PRIMARY KEY CLUSTERED 
    ( [OID] ASC )
)
CREATE TABLE [dbo].[Meta_Column](
    [OID] [int] IDENTITY(1,1) NOT NULL,
    [Tbl_OID] [int] NOT NULL,
    [IsActive] [bit] NOT NULL,
    [Seq] [int] NOT NULL,
    [ID] [nvarchar](50) NOT NULL,
    [DataType] [varchar](50) NOT NULL,
    [Length] [int] NULL,
    [IsNullable] [bit] NOT NULL,
    [IsIdentity] [bit] NOT NULL,
    [LookupTable] [nvarchar](50) NULL,
    CONSTRAINT [PK_Meta_Column] PRIMARY KEY CLUSTERED 
    ( [OID] ASC )
) 

These tables are filled from the system catalog views using the following stored procedure

CREATE PROCEDURE [dbo].[dev_Retrieve_Meta] AS

  SET NOCOUNT ON;

  INSERT INTO Meta_Table (ID, TableType)
  SELECT o.name, o.type
  FROM sys.objects o
  LEFT OUTER JOIN Meta_Table t ON o.name = t.ID
  WHERE o.type IN ('V', 'U')
  AND t.OID IS NULL  ;

  UPDATE Meta_Column SET IsActive = 0;

  INSERT INTO Meta_Column (Tbl_OID, Seq, ID, DataType)  
  SELECT t.OID, c.clumn_id, c.name, ty.name 
  FROM sys.objects o
  INNER JOIN Meta_Table t ON o.name = t.ID
  INNER JOIN sys.columns c ON o.object_id = c.object_id
  INNER JOIN sys.types ty ON c.user_type_id = ty.user_type_id
  LEFT OUTER JOIN Meta_Column mc 
    ON mc.Tbl_OID = t.OID AND mc.ID = c.name
  WHERE mc.OID IS NULL;

  UPDATE Meta_Column
  SET Seq = c.column_id, 
    DataType = ty.name, 
    Length = 
      CASE WHEN ty.name IN ('varchar', 'nvarchar') 
          AND c.max_length > -1 
        THEN c.max_length 
        ELSE NULL 
      END,
    IsNullable = c.is_nullable, 
    IsIdentity = c.is_identity,
    LookupTable = lt.name,
    IsActive = 1
  FROM Meta_Column mc
  INNER JOIN meta_table t ON mc.Tbl_OID = t.OID 
  INNER JOIN sys.objects o ON o.name = t.ID
  INNER JOIN sys.columns c 
    ON o.object_id = c.object_id AND mc.ID = c.name
  INNER JOIN sys.types ty ON c.user_type_id = ty.user_type_id
  LEFT OUTER JOIN sys.foreign_key_columns fkc 
    ON fkc.parent_object_id = o.object_id 
    AND fkc.parent_column_id = c.column_id
  LEFT OUTER JOIN sys.objects lt 
    ON fkc.referenced_object_id = lt.object_id

The first INSERT statement retrieves all tables and views which are not already stored in the Meta_Table table.

Next, the new column names are retrieved from the sys.columns catalog view, and the columns’ IsActive field is set to false.

Finally, the UPDATE Meta_Column retrieves the column attributes from the catalog view, and sets the IsActive field to true for all columns still present in the catalog view (this is necessary to deal with changes in the data model).


Generating NHibernate ClassMap in PL/SQL

April 26, 2010

In the previous post I showed how to generate C# classes from Oracle table definitions using a PL/SQL stored procedure. This post deals with the ClassMap generation.

I reuse the original two cursors for tables and columns, and replace the field/getter/setting declaration by Fluent NHibernate calls to Id(), Map(), and Reference():

begin
  dbms_output.enable(1000000);

  for obj in cObjects loop
    csclass := replace(initcap(obj.object_name), '_', '');

    dbms_output.put_line('public partial class ' || csclass
      || 'Map : ClassMap<' || csclass || '>');
    dbms_output.put_line('{');
    dbms_output.put_line('    public ' || csclass || 'Map()');
    dbms_output.put_line('    {');
    dbms_output.put_line('        Table("' || obj.object_name || '");');

    for col in cColumns(obj.object_name) loop
      csname := replace(initcap(col.column_name), '_', '');

      case col.data_type
        when 'CHAR' then
          dbms_output.put_line('        Map(x => x.' || csname || ', "' 
            || col.column_name || '");');
        when 'NUMBER' then
          if col.column_name = 'OID' then
            dbms_output.put_line('        Id(x => x.' || csname || ', "' 
              || col.column_name 
              || '").GeneratedBy.Custom("trigger-identity");');
          elsif col.column_name like '%\_OID' escape '\' 
              and col.table_name is not null then
            dbms_output.put_line('        References(x => x.'
              || replace(initcap(replace(col.column_name, '_OID', '')), '_', '')
              || ', "' || col.column_name || '");');
          else
            dbms_output.put_line('        Map(x => x.' || csname || ', "' 
              || col.column_name || '");');
          end if;
        else
          dbms_output.put_line('        Map(x => x.' || csname || ', "' 
            || col.column_name || '");');
      end case;
    end loop;

    dbms_output.put_line('    }');
    dbms_output.put_line('}');
  end loop;
end;

Oracle-specific issues in NHibernate

April 15, 2010

After managing to get NHibernate connect to an Oracle database, the brave developer needs to cope with two specialties of Oracle databases, namely handling Boolean values, and Identity columns generated by INSERT triggers.

Boolean values

Oracle databases do not implement a Boolean data type (while PL/SQL does), as opposed to MS SQL Server’s data type Bit. Instead, the standard way is to have a CHAR(1) with allowed values “Y” and “N”:

CREATE TABLE "FOO"
(
  ...
  "IS_ACTIVE" CHAR(1 BYTE) DEFAULT 'Y' NOT NULL ENABLE,
  ...
  CONSTRAINT "CHK_FOO_IS_ACTIVE" CHECK (IS_ACTIVE IN ('Y', 'N')) ENABLE,
  ...
)

To access such a column in NHibernate in a type-safe way (i.e. C# bool), you need to map it to the YesNoType. In Fluent, this is done using .Map().CustomType<>() in the ClassMap constructor:

  Map(x => x.IsActive, "IS_ACTIVE").CustomType<YesNoType>();

Trigger-generated Identities

Oracle databases do not implement an Auto-Increment or Identity mechanism. Instead, sequences are used to provide incremental values for primary key columns.

The sequence values are either provided in the INSERT statement reading the sequence’s NEXTVAL property, or generated by a BEFORE INSERT FOR EACH ROW trigger.

If you declare a primary key column in Fluent using the Id() method, NHibernate will generate an INSERT statement like this:

INSERT INTO cpa.FOO (OID, ID, IS_ACTIVE)
VALUES (cpa.hibernate_sequence.nextval, ?, ?)
returning OID into :nhIdOutParam

implementing the first variant. The trigger-generated method is supported by specifying GeneratedBy.Custom(“trigger-identity”) (works in Fluent build 636):

Id(x => x.Oid).GeneratedBy.Custom("trigger-identity");

The trigger-identity generator will generate an INSERT statement like this:

INSERT INTO cpa.FOO (ID, DESCRIPTION) 
VALUES (?, ?) 
returning OID into :nhIdOutParam

See this blog for other generators in NHibernate.

Not Oracle-specific, but probably interesting:

Table name aliases

Table name aliases are specified using the Table(“tablename”) method in the ClassMap constructor.

Column name aliases

Column name aliases can be specified by passing a second argument to the Id(), Map(), and References() methods:

Table("FOO");
Id(x => x.Oid, "OID").GeneratedBy.Custom("trigger-identity");
Map(x => x.Id, "ID");
References(x => x.Bar, "BAR_OID");

First Steps with NHibernate, Fluent, Oracle, C#, and Visual Studio 2010 RC

April 13, 2010

A few days into a new project: C#, Oracle, ASP.Net, .Net 4, VS 2010. After creating a (very!) basic data model with a couple of tables, the first task was to check whether the intended tools (C# 4, NHibernate+Fluent, ODP.Net, Oracle 11g) can be persuaded to co-operate.

Versions of tools and libraries used:

After I had defined my classes and maps as described in Fluent’s Getting Started wiki page, the first major hurdle was to get the session factory builder to execute without exception:

public static ISessionFactory Configure()
{
  var c = Fluently.Configure();
  c.Database(OracleDataClientConfiguration.Oracle10
    .ConnectionString(x => x.FromConnectionStringWithKey("default"))
    .DefaultSchema("cpa"));
  c.Mappings(m => m.FluentMappings.AddFromAssemblyOf<ConfigureDAL>());
  return c.BuildSessionFactory();
}

Creating an NHibernate session factory with Fluent, connecting to an Oracle database via ODP.Net.

Fluent provides 3 classes for Oracle access in the FluentNHibernate.Cfg.Db namespace:

  • OracleClientConfiguration for the MS Oracle Client (System.Data.OracleClient, deprecated)
  • OracleConfiguration, obsolete and replaced by:
  • OracleDataClientConfiguration for use with ODP

The connection string is stored in app.config under the name “default”:

<connectionStrings>
  <add name="default"
    connectionString="Data Source=(DESCRIPTION=(ADDRESS_LIST=(ADDRESS=(PROTOCOL=TCP)
      (HOST=orasrv)(PORT=1521)))(CONNECT_DATA=(SERVER=DEDICATED)
      (SERVICE_NAME=orcl)));User Id=cpa;Password=the_password;" />
</connectionStrings>

Trying to execute the BuildSessionFactory() method raised the first exception:

The element ‘class’ in namespace ‘urn:nhibernate-mapping-2.2′ has invalid child element ‘property’ in namespace ‘urn:nhibernate-mapping-2.2′. List of possible elements expected: ‘meta, subselect, cache, synchronize, comment, tuplizer, id, composite-id’

I tried to step through the source code, and noticed that the downloaded source code version did not match the installed 1.0 RTM of Fluent. So I updated to build 636, which solved this problem, but raised a new one:

The IDbCommand and IDbConnection implementation in the assembly Oracle.DataAccess could not be found.
Ensure that the assembly Oracle.DataAccess is located in the application directory or in the Global Assembly Cache.
If the assembly is in the GAC, use <qualifyAssembly/> element in the application configuration file to specify the full name of the assembly.

Thanks to the interweb I found this blog which provided the solution: Force .Net to load a specific version of ODP (2.111.7.20 in my case), instead of looking for any version:

<runtime>
  <assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
    <qualifyAssembly partialName="Oracle.DataAccess"
      fullName="Oracle.DataAccess, Version=2.111.7.20, Culture=neutral,
                PublicKeyToken=89b483f429c47342"/>
  </assemblyBinding>
</runtime>

Start the Visual Studio prompt and type

gacutil /l | find "Oracle"

to find out which version of ODP you have installed. The assemblies come in a version for .Net 1 and for .Net 2.

This finally caused the BuildSessionFactory to execute successfully, let me create a session and query a table in the database.

I had feared that ODP would not work using the .Net 4 profile, and had thus switched to .Net 2.0 and .Net 3.5 to debug. However, you don’t have Linq in .Net 2, and the Oracle assembly cannot be referenced by projects targeting .Net 3.5 Client Profile and .Net 4.0 Client Profile. Adding the qualified assembly worked in .Net 4, after all.


Follow

Get every new post delivered to your Inbox.