NHibernate OneToMany surprises

March 19, 2013

Up to now, I did not care about NHibernate’s Sets and Bags, as my queries would target the details tables directly, and filter to the parent’s PK.

Now I tried to extend my Classes and ClassMaps generation code for Mapping.By.Code to Bags, and started out with something like this:

public partial class Foo
{
  public virtual int Id { get; protected set; }
  public virtual ICollection<Bar> Bars { get; set; }
}
public partial class Bar
{
  public virtual long Id { get; protected set; }
  public virtual Foo Foo { get; set; }
}

The mapping for Bar is not affected by the new Bars property:

public partial class Bar_Map: ClassMapping<Bar>
{
  public Bar_Map()
  {
    Table("Bar");
    Id(x => x.Id, map => ...);
    ManyToOne(x => x.Foo, map => ...);
  }
}

The master table gets a new collection property, depending on which NH mapping you use (SO):

Bag(x => x.Bars, bag =>
  {
    bag.Table("Bar");
    bag.Inverse(true);
    bag.Lazy(CollectionLazy.Lazy);
    bag.Cascade(Cascade.DeleteOrphans);
    bag.Key(k => {
      k.Column(col => col.Name("FooId"));  
          // this is the SQL column name
    });
  }, map => map.OneToMany());

So I try this code, retrieve a Foo, and count its Bars:

var foo = session.Get<Foo>(fooId);
Console.WriteLine(foo.Id);
Console.WriteLine(foo.Bars.Count());

Surprisingly, NH selects ALL Bar records into the collection, and counts the elements in the collection. That’s not what I expected.

On the NHibernate Pitfalls blog I found the hint to change the Lazy() setting to Lazy(CollectionLazy.Extra). And indeed, only a SELECT COUNT(*) was executed.

Somehow I was expecting the collection properties to be an alias for SELECT WHERE statements, so I tried things like foo.Bars.FirstOrDefault(), foo.Bars[0] (for IList) or foo.Bars.Take(1), but each of them always first populated the Bars collection in .Net, and only then retrieved the requested object from the collection, rather than issuing a separate SELECT.

For a complete list of documented surprises, see the NHibernate Pitfalls Index.


Invoking Stored Procedures generating C# Code using T4 Templates

February 5, 2013

When developing database applications, I usually start out with the database schema and have the necessary C# code (data classes, procedure calls, application-specific constants) generated by a couple of stored procedures. For my own projects, I create a batch file to build the various parts of the solution, all the way from code generation to publishing the application.

Maybe not everybody shares my preference for building-by-batch, so I was looking for an alternative way to invoke code generation in stored procedures, and I remembered working on a project a couple of years ago that used T4 Text Templates to generate C# code.

The first component of the T4 solution is MultipleOutputHelper.ttinclude which implements features such saving only changed files, handling check-out if connected to TFS, and much more.

Since I want to access the connection strings stored in web.config or app.config, I searched for sample code and found ConfigurationAccessor.tt.

The final T4 Template looks like this. The header includes both libraries and declares .Net namespaces:

<#@ template debug="True" hostspecific="True" language="C#" 
#><#@ output extension=".cs"
#><#@ include file="MultipleOutputHelper.ttinclude"
#><#@ include file="ConfigurationAccessor.ttinclude"
#><#@ assembly name="System.Data"
#><#@ import namespace="System.Data"
#><#@ import namespace="System.Data.SqlClient"
#><#

Next, we instantiate the objects declared in both libraries

  var manager = Manager.Create(Host, GenerationEnvironment); 
  manager.StartNewFile("Classes.generated.cs"); 

  var config = new ConfigurationAccessor((IServiceProvider)this.Host);
  var connStrEntry = config.ConnectionStrings["default"];

Depending on your build process, you may want to cause database errors (connection string, connectivity, execution) to raise either warnings or compiler errors:

  var warnings = true;
  var errors = new List<string>();

  if (connStrEntry == null)
  {
    errors.Add("Connection string named 'default' not found");
  }
  else
  {
    var connStr = connStrEntry.ConnectionString;

After checking the connection string, let’s connect to the database and execute the stored procedure.

The output of PRINT commands is retrieved using the InfoMessage event:

    try
    { 
      using (var conn = new SqlConnection(connStr))
      {
        conn.Open();
        conn.InfoMessage += delegate(object sender, SqlInfoMessageEventArgs e)
        {                                    
            this.WriteLine(e.Message);
        };

        SqlCommand cmd = new SqlCommand("dev_Generate_DAL_Classes", conn);
        cmd.CommandType = CommandType.StoredProcedure;
        cmd.ExecuteNonQuery();
      }
    }
    catch(Exception ex)
    {
      errors.Add(ex.Message);
    }
  }

  manager.EndBlock();

If no errors occurred, save the generated file. Otherwise, either raise warnings or errors.

  if (errors.Count == 0)
  {
    manager.Process(true); 
  }
  else 
  {
    foreach(var error in errors)
    {
      if (warnings)
        this.Warning(Host.TemplateFile + ": " + error);
      else
        this.Error(Host.TemplateFile + ": " + error);
    }
  }
#>

I originally tried to store warnings and errors using the Host.LogErrors method. However, no matter which Error Code I set in CompilerError, the result would always be errors rather than warnings. Using the TextTransformation‘s Warning() and Error() methods did the trick.


NHibernate String Comparison

August 20, 2012

If you try to compare strings in NHibernate Linq using string.Compare() or [string property].CompareTo(), the query evaluation will throw a NotSupportedException.

After a bit of searching I found various NHibernate extensions that add TSQL functionality to NH:

For the 4 string comparison operations (GreaterThan, GreaterThanOrEqual, LessThan, LessThanOrEqual) we define a C# extension method:

public static class StringComparison
{
  public static bool GreaterThan(this string s, string other)
  {
    return string.Compare(s, other) > 0;
  }
}

Next, we define a BaseHqlGeneratorForMethod class for each of these extension methods which generates the HqlTree for the new operation:

public class StringGreaterThanGenerator 
  : BaseHqlGeneratorForMethod
{
  public StringGreaterThanGenerator()
  {
    SupportedMethods = new[]
    {
       ReflectionHelper.GetMethodDefinition<string>(x => x.GreaterThan(null))
    };
  }

  public override HqlTreeNode BuildHql(MethodInfo method, 
    System.Linq.Expressions.Expression targetObject, 
    ReadOnlyCollection<System.Linq.Expressions.Expression> arguments, 
    HqlTreeBuilder treeBuilder, IHqlExpressionVisitor visitor)
  {
    return treeBuilder.GreaterThan(
      visitor.Visit(targetObject).AsExpression(),
      visitor.Visit(arguments[0]).AsExpression());
  }
}

Finally, the extensions have to be registered by a registry helper class which is added to the NH configuration:

public class StringComparisonLinqtoHqlGeneratorsRegistry 
  : DefaultLinqToHqlGeneratorsRegistry
{
  public StringComparisonLinqtoHqlGeneratorsRegistry()
  {
    this.Merge(new StringGreaterThanGenerator());
    this.Merge(new StringGreaterThanOrEqualGenerator());
    this.Merge(new StringLessThanGenerator());
    this.Merge(new StringLessThanOrEqualGenerator());
  }
}

In the code building configuration and session factory, add:

configuration.LinqToHqlGeneratorsRegistry
  <StringComparisonLinqtoHqlGeneratorsRegistry>();

The full code for all string comparison operations is available for download here.

 

 


Fixing the Blob Truncation problem with NHibernate 3.2.0 GA

August 10, 2012

In one of my NH ClassMaps, I map a VARBINARY(MAX) column like this

Property(x => x.OriginalImage, map =>
  {
    map.Column("OriginalImage");
    map.NotNullable(true);
    map.Lazy(true);
  });

but when I save an uploaded image, the binary data in the database is truncated to 8000 bytes.

Adding a

    map.Length(int.MaxValue);

causes NHibernate to expect an IMAGE column rather than VARBINARY(MAX) (using the MsSql2005Dialect dialect).

After a short search I found two solutions for this problem:

  • Explicitly define a SqlType():

Property(x => x.OriginalImage, map =>
{
map.Column(c => { c.Name(“OriginalImage”); c.SqlType(“VARBINARY(MAX)”); });
map.NotNullable(true);
map.Lazy(true);
map.Length(int.MaxValue);
});

based on this SO answer referring to Fluent rather than Loquacious.

  • Subclass the dialect

As illustrated in this SO answer

public class MyMsSql2008Dialect : MsSql2008Dialect
{
    protected override void RegisterLargeObjectTypeMappings()
    {
        base.RegisterLargeObjectTypeMappings();
        base.RegisterColumnType(DbType.Binary, 2147483647, "VARBINARY(MAX)");
    }
}

NHibernate 3 Data Type Mapping for SQL Server

August 8, 2012

When I recently tried to add a DOUBLE column in a project using NHibernate 3.2.0 GA to connect to a SQL Server database, the schema validation failed since the T-SQL DOUBLE/C# double mapping did not work out. So I wondered how the NHibernate schema validator decides which column data types are supported for each of the C# data types.

So I checked the following sources:

  • the MsSql****Dialect.cs files in the \src\NHibernate\Dialect\ directory of 3.2.0GA

The result, sorted by the DbType enumeration, looks like this:

DbType .Net version C# NH Mapping 7/2000 2005 2008
AnsiString 1.1 VARCHAR(255)
VARCHAR(n)
TEXT
VARCHAR(MAX)
AnsiString-
FixedLength
1.1 CHAR(255)
CHAR(n)
Binary 1.1 byte[] VARBINARY(8000)
VARBINARY(n)
IMAGE
VARBINARY(MAX)
Boolean 1.1 bool BIT
Byte 1.1 byte TINYINT
Currency 1.1 MONEY
Date 1.1 DATETIME
DATE
DateTime 1.1 DateTime DATETIME
DateTime2 3.5 DATETIME2
DateTime-
Offset
3.5 DateTime-
Offset
DATETIME-
OFFSET
Decimal 1.1 decimal DECIMAL(19,5)
DECIMAL(n,n)
Double 1.1 double DOUPLE PRECISION (synonym for FLOAT(53))
Guid 1.1 Guid UNIQUEIDENTIFIER
Int16 1.1 short SMALLINT
Int32 1.1 int INT
Int64 1.1 long BIGINT
Single 1.1 float REAL (synonym for FLOAT(24))
String 1.1 string NVARCHAR(255)
NVARCHAR(n)
NTEXT
NVARCHAR(MAX)
String-
FixedLength
1.1 NCHAR(255)
NCHAR(n)
Time 1.1 TimeSpan DATETIME
TIME
Xml 2.0 XML

Note that some data types are not supported:

  • DbType: Object, Sbyte, UInt16, UInt32, UInt64, VarNumeric
  • C# data types: char, object
  • T-SQL data types: sql_variant, rowversion, smalldatetime, timestamp, and CLR types

Getting Started with SQLite and Loquacious NHibernate 3.2

July 5, 2012

After I figured out how to create and access a SQLite database in C#, the obvious (for me, at least) next step was to use NHibernate for database access.

In NH 2.1.2 GA I preferred the Fluent configuration rather than the XML-based HBM files, which has been replaced by Loquacious. (You can read more on Loquacious here and here.)

You start with classes which represent a table (or view), and class maps that define the mapping of table fields to class properties:

public partial class DirectorySelection
{
  public virtual int ID { get; protected set; }
  public virtual string Path { get; set; }
}
public partial class DirectorySelection_Map 
  : ClassMapping<DirectorySelection>
{
  public DirectorySelection_Map()
  {
    Table("DirectorySelection");
    Id(x => x.ID, map =>
    {
      map.Generator(Generators.Identity);
    });
    Property(x => x.Path, map =>
    {
      map.Column("Path");
      map.NotNullable(true);
      map.Length(2000);
      map.Unique(true);
    });
  }
}

Class maps require the inclusion of NH namespaces NHibernate.Mapping.ByCode and NHibernate.Mapping.ByCode.Conformist.

Next, we need to define a Session Factory which will create us a NH Session every time we access the database:

  public class ConfigureNH
  {
    static ISessionFactory sessionFactory = NHSessionFactory();

    static ISessionFactory NHSessionFactory()
    {
      var mapper = new ModelMapper();
      mapper.AddMappings(Assembly.GetExecutingAssembly().GetExportedTypes());
      HbmMapping domainMapping = 
        mapper.CompileMappingForAllExplicitlyAddedEntities();

      var f = Path.Combine(
        System.Environment.GetFolderPath(
          System.Environment.SpecialFolder.LocalApplicationData), 
        "mydatabase.db");
      var cn = 
        (new System.Data.SQLite.SQLiteConnectionStringBuilder { DataSource = f })
          .ConnectionString;

      var configuration = new NHibernate.Cfg.Configuration();
      configuration.DataBaseIntegration(c =>
      {
        c.Driver<NHibernate.Driver.SQLite20Driver>();
        c.Dialect<SQLiteDialect>();
        c.ConnectionString = cn;
        c.KeywordsAutoImport = Hbm2DDLKeyWords.AutoQuote;
        c.SchemaAction = SchemaAutoAction.Update;    

        c.LogFormattedSql = true;
        c.LogSqlInConsole = true;
      });
      configuration.AddMapping(domainMapping);
      sessionFactory = configuration.BuildSessionFactory();
      return sessionFactory;
    }

    public static ISessionFactory SessionFactory
    {
      get
      {
        return sessionFactory;
      }
    }
  }

Note that this configuration class needs to be contained in the same assembly as the NH data classes and maps, since we use Assembly.GetExecutingAssembly().GetExportedTypes() to retrieve all DAL classes.

To create an NH session, we simply open one via the session factory:

var session = ConfigureNH.SessionFactory.OpenSession();

MSSQL Legacy Data Types in NHibernate.Mapping.ByCode.Conformist

February 24, 2012

Trying to migrate the code generation of NHibernate data classes and mappings from 2.1.2 to 3.2 and from Fluent to Mapping ByCode Conformist, I noticed a couple of problems relating mostly to non-Unicode string fields and legacy data types such as image and ntext.

Typically, column mapping uses the Property() method for columns like to NVARCHAR column:

public partial class Application_Map: ClassMapping<Application>
{
  public Application_Map()
  {
    Property(x => x.ID, map =>
      {
        map.Column("ID");
        map.NotNullable(true);
        map.Length(100);
      });
  }
}

This kind of declaration raised a HibernateException for VARCHAR columns  in configuration.BuildSessionFactory()

Wrong column type in MyDatabase.dbo.Foo for column Bar. Found: varchar, Expected NVARCHAR(256)

To fix this exception, the varchar column needs to be declared like this:

  Property(x => x.VarcharFoo, map =>
    {
      map.Column(c =>
        {
          c.Name("VarcharFoo");
          c.SqlType("varchar");
          c.Length(50);
        });
    });

Similarly, we can declare the native SQL data type for image and ntext columns:

  Property(x => x.FooImage, map =>
    {
      map.Column(c =>
        {
          c.Name("FooImage");
          c.SqlType("image");
        });
      map.Lazy(true);
    });
  Property(x => x.FooNText, map =>
    {
      map.Column(c =>
        {
          c.Name("FooNText");
          c.SqlType("ntext");
        });
    });


Generating NHibernate Access to MSSQL Stored Procedures

February 23, 2012

This series describes the generation of C# classes and database access for NHibernate 2.1.2GA.

To store the information on which Stored Procedures to script out, we need a table Meta_Procedure, similar to Meta_Table presented earlier.

CREATE TABLE [dbo].[Meta_Procedure](
  [OID] [int] IDENTITY(1,1) NOT NULL,
  [ID] [nvarchar](50) NOT NULL,
  [UseDAL] [bit] NOT NULL,
  CONSTRAINT [PK_Meta_Procedure] PRIMARY KEY CLUSTERED
  ( [OID] ASC )
)

We fill this table from the list of stored procedures stored in sys.objects:

INSERT INTO Meta_Procedure (ID)
SELECT o.name
FROM sys.objects o
LEFT OUTER JOIN Meta_Procedure t ON o.name = t.id
WHERE o.type IN ('P')
AND t.OID IS NULL;

The developer now only needs to set the UseDAL field to true (1) for each procedure to be scripted.

Declaration of stored procedures for NHibernate consists of two parts: a .hbm.xml containing named queries with a TSQL statement to invoke each stored procedure, which is compiled as Embedded Resource:

CREATE PROCEDURE [dbo].[dev_Generate_DAL_HBM_XML] AS

  PRINT '<?xml version="1.0" encoding="utf-8"?>
<hibernate-mapping xmlns="urn:nhibernate-mapping-2.2"
    assembly="My.Project" namespace="My.Project.DAL">'
  
  DECLARE @oid INT, @id NVARCHAR(50)

  DECLARE cT CURSOR FOR
  SELECT object_id, name
  FROM sys.procedures p
  INNER JOIN Meta_Procedure mp ON p.name = mp.ID
  WHERE mp.UseDAL = 1
  ORDER BY name

  OPEN cT
  FETCH cT INTO @oid, @id

  WHILE @@FETCH_STATUS = 0 BEGIN
    DECLARE @cid NVARCHAR(50), @datatype NVARCHAR(50), 
      @isoutput BIT, @parameterid INT
    
    DECLARE cP cursor FOR
    SELECT  REPLACE(p.name, '@', ''), t.name, p.is_output, p.parameter_id
    FROM  sys.parameters p
    INNER JOIN sys.types t ON p.user_type_id = t.user_type_id
    WHERE   p.object_id = @oid
    ORDER BY p.parameter_id

    PRINT '    <sql-query name="' + @id + '" callable="true">'

    OPEN cP
    FETCH cP INTO @cid, @datatype, @isoutput, @parameterid

    WHILE @@FETCH_STATUS = 0 BEGIN
      IF @datatype = 'nvarchar' 
        SET @datatype = 'string'
      ELSE IF @datatype = 'date'
        SET @datatype = 'DateTime?'
      ELSE
        SET @datatype = @datatype + '?'

      PRINT '        <query-param name="' + @cid + '" type="' + @datatype + '" />'
      
      FETCH cP INTO @cid, @datatype, @isoutput, @parameterid
    END

    CLOSE cP

    PRINT 'begin'
    PRINT '    execute ' + @id 
    
    OPEN cP
    FETCH cP INTO @cid, @datatype, @isoutput, @parameterid

    WHILE @@FETCH_STATUS = 0 BEGIN
      IF @datatype = 'nvarchar' 
        SET @datatype = 'string'
      ELSE IF @datatype = 'date'
        SET @datatype = 'DateTime?'
      ELSE
        SET @datatype = @datatype + '?'

      IF @parameterid = 1
        PRINT '        :' + @cid
      ELSE BEGIN
        PRINT '        , :' + @cid
      END
      
      FETCH cP INTO @cid, @datatype, @isoutput, @parameterid
    END
    CLOSE cP

    DEALLOCATE cP
    
    PRINT 'end'
    PRINT '    </sql-query>'
    
    FETCH cT into @oid, @id
  END

  CLOSE cT
  DEALLOCATE cT

  PRINT '</hibernate-mapping>'

For each stored procedure, we need a C# method which calls the corresponding named query. The SP’s parameters are retrieved from the sys.parameters catalog view, and their data types translated just as we handled table columns.

CREATE PROCEDURE [dbo].[dev_Generate_DAL_Procedures] AS

  PRINT 'using System;'
  PRINT 'using System.Collections;'
  PRINT 'using NHibernate;'
  PRINT ''
  PRINT 'namespace My.Project.DAL'
  PRINT '{'
  PRINT '    public static class Procedures'
  PRINT '    {'
  
  DECLARE @oid INT, @id NVARCHAR(50)

  DECLARE cT CURSOR FOR
  SELECT object_id, name
  FROM sys.procedures p
  INNER JOIN Meta_Procedure mp ON p.name = mp.ID
  WHERE mp.UseDAL = 1
  ORDER BY name

  OPEN cT
  FETCH cT INTO @oid, @id

  WHILE @@FETCH_STATUS = 0 BEGIN
    DECLARE @cid NVARCHAR(50), @datatype NVARCHAR(50), @isoutput BIT
    
    DECLARE cP cursor FOR
    SELECT  REPLACE(p.name, '@', ''), t.name, p.is_output
    FROM  sys.parameters p
    INNER JOIN sys.types t ON p.user_type_id = t.user_type_id
    WHERE   p.object_id = @oid
    ORDER BY p.parameter_id

    PRINT '        public static IList ' + @id + '(ISession session'

    OPEN cP
    FETCH cP INTO @cid, @datatype, @isoutput

    WHILE @@FETCH_STATUS = 0 BEGIN
      IF @datatype = 'nvarchar' 
        SET @datatype = 'string'
      ELSE IF @datatype = 'date'
        SET @datatype = 'DateTime?'
      ELSE
        SET @datatype = @datatype + '?'

      PRINT '            , ' + 
        CASE WHEN @isoutput = 1 THEN 'out ' ELSE '' END + 
        @datatype + ' ' + @cid
      
      FETCH cP INTO @cid, @datatype, @isoutput
    END

    PRINT '            )'
    PRINT '        {'
    PRINT '            IQuery sp = session.GetNamedQuery("' + @id + '");'

    CLOSE cP

    OPEN cP
    FETCH cP INTO @cid, @datatype, @isoutput

    WHILE @@FETCH_STATUS = 0 BEGIN
      IF @datatype = 'nvarchar' 
        SET @datatype = 'string'
      ELSE IF @datatype = 'date'
        SET @datatype = 'DateTime?'
      ELSE
        SET @datatype = @datatype + '?'

      IF @datatype = 'string'
        PRINT '            sp.SetParameter<' + @datatype + 
          '>("' + @cid + '", ' + @cid + ');'
      ELSE BEGIN
        PRINT '            sp.SetParameter<' + @datatype + 
          '>("' + @cid + '", ' + @cid + ');'
      END
      
      FETCH cP INTO @cid, @datatype, @isoutput
    END
    CLOSE cP

    DEALLOCATE cP
    
    PRINT '            return sp.List();'
    PRINT '        }'
    
    FETCH cT INTO @oid, @id
  END

  CLOSE cT
  DEALLOCATE cT

  PRINT '    }'
  PRINT '}'

Generating NHibernate Classes for MSSQL in TSQL (2)

February 20, 2012

After retrieving the necessary meta data on tables and columns, we can now generate the C# classes for NHibernate and the necessary mapping information.

CREATE PROCEDURE [dbo].[dev_Generate_DAL_Classes] AS

  SET NOCOUNT ON

  PRINT 'using System;'
  PRINT 'using System.Collections.Generic;'
  PRINT 'using System.Linq;'
  PRINT 'using System.Text;'
  PRINT 'using System.ComponentModel;'
  PRINT 'using System.Runtime.Serialization;'
  PRINT 'using NHibernate.Validator.Constraints;'
  PRINT ''
  PRINT 'namespace My.Project.DAL'
  PRINT '{'

  DECLARE @oid INT, @id NVARCHAR(50), @tabletype NVARCHAR(1)

  DECLARE cT CURSOR FOR
  SELECT OID, ID, TableType
  FROM Meta_Table
  WHERE UseDAL = 1
  ORDER BY ID

  OPEN cT
  FETCH cT INTO @oid, @id, @tabletype

  WHILE @@FETCH_STATUS = 0 BEGIN
    PRINT '    public partial class ' + @id
    PRINT '    {'

    DECLARE @cid NVARCHAR(50), @datatype NVARCHAR(50), @length INT,
      @isnullable BIT, @isidentity BIT, @lookup NVARCHAR(50)

    DECLARE cC CURSOR FOR
    SELECT ID, DataType, Length, IsNullable, IsIdentity, LookupTable
    FROM   Meta_Column
    WHERE  Tbl_OID = @oid
    AND    IsActive = 1
    ORDER BY Seq

    OPEN cC
    FETCH cC INTO @cid, @datatype, @length, @isnullable,
      @isidentity, @lookup

    WHILE @@FETCH_STATUS = 0 BEGIN
      IF @datatype = 'nvarchar'
        SET @datatype = 'string'
      ELSE IF @datatype = 'varchar'
        SET @datatype = 'string'
      ELSE IF @datatype = 'date'
        SET @datatype = 'DateTime'
      ELSE IF @datatype = 'datetime'
        SET @datatype = 'DateTime'
      ELSE IF @datatype = 'bit'
        SET @datatype = 'bool'
      ELSE IF @datatype = 'varbinary' BEGIN
        SET @datatype = 'byte[]'
        SET @isnullable = 0
      END

      IF @isidentity = 1 OR @cid = 'OID'
        PRINT '        public virtual ' + @datatype + ' '
          + @cid + ' { get; private set; }'
      ELSE IF @lookup IS NOT NULL BEGIN
        IF @isnullable = 0
          PRINT '        [NotNull]'
        PRINT '        public virtual ' + @lookup + ' '
          + REPLACE(@cid, '_OID', '') + ' { get; set; }'
      END ELSE BEGIN
        IF @datatype = 'string' BEGIN
          IF @isnullable = 0
            PRINT '        [NotNullNotEmpty]'
          IF @length IS NOT NULL
            PRINT '        [Length('
              + CONVERT(NVARCHAR, @length) + ')]'
        END ELSE
          IF @isnullable = 1
            SET @datatype = @datatype + '?'

        PRINT '        public virtual ' + @datatype + ' '
          + @cid + ' { get; set; }'
      END
      FETCH cC INTO @cid, @datatype, @length, @isnullable,
        @isidentity, @lookup
    END

    CLOSE cC
    DEALLOCATE cC

    PRINT '    }'

    FETCH cT INTO @oid, @id, @tabletype
  END

  CLOSE cT
  DEALLOCATE cT

  PRINT '}'

This stored procedure generates a C# class for each of the tables with the UseDAL flag set to true by the developer. For the columns, the SQL Server data types need to be translated into C# (i.e. .Net data types)

And now for the Class Maps

CREATE PROCEDURE [dbo].[dev_Generate_DAL_ClassMaps] AS

  SET NOCOUNT ON

  PRINT 'using System;'
  PRINT 'using System.Collections.Generic;'
  PRINT 'using System.Linq;'
  PRINT 'using System.Text;'
  PRINT 'using FluentNHibernate.Mapping;'
  PRINT 'using NHibernate.Validator.Constraints;'
  PRINT ''
  PRINT 'namespace My.Project.DAL'
  PRINT '{'

  DECLARE @oid INT, @id NVARCHAR(50), @tabletype NVARCHAR(1)

  DECLARE cT CURSOR FOR
  SELECT OID, ID, TableType
  FROM Meta_Table
  WHERE UseDAL = 1
  ORDER BY ID

  OPEN cT
  FETCH cT INTO @oid, @id, @tabletype

  WHILE @@FETCH_STATUS = 0 BEGIN
    PRINT '    public partial class ' + @id + '_Map: ClassMap<' + @id + '>'
    PRINT '    {'
    PRINT '        public ' + @id + '_Map()'
    PRINT '        {'
    PRINT '            Table("' + @id + '");'

    DECLARE @cid NVARCHAR(50), @datatype NVARCHAR(50), @length INT,
      @isnullable BIT, @isidentity BIT, @lookup NVARCHAR(50)

    DECLARE cC CURSOR FOR
    SELECT ID, DataType, Length, IsNullable, IsIdentity, LookupTable
    FROM  Meta_Column
    WHERE  Tbl_OID = @oid
    AND    IsActive = 1
    ORDER BY Seq

    OPEN cC
    FETCH cC INTO @cid, @datatype, @length, @isnullable,
      @isidentity, @lookup

    WHILE @@FETCH_STATUS = 0 BEGIN
      IF @datatype = 'nvarchar'
        SET @datatype = 'string'
      ELSE IF @datatype = 'varchar'
        SET @datatype = 'string'
      ELSE IF @datatype = 'date'
        SET @datatype = 'DateTime'
      ELSE IF @datatype = 'datetime'
        SET @datatype = 'DateTime'
      ELSE IF @datatype = 'bit'
        SET @datatype = 'bool'
      ELSE IF @datatype = 'varbinary'
        SET @datatype = 'byte[]'

      IF @isidentity = 1
        PRINT '            Id(x => x.' + @cid + ').GeneratedBy.Identity();'
      ELSE IF @cid = 'OID'
        PRINT '            Id(x => x.' + @cid + ');'
      ELSE IF @cid = 'DateCreated'
        PRINT '            Map(x => x.' + @cid + ').Generated.Insert();'
      ELSE BEGIN
        IF @lookup IS NOT NULL BEGIN
          PRINT '            References(x => x.' +
              REPLACE(@cid, '_OID', '') + ', "' + @cid + '")' +
            CASE WHEN @isnullable = 0 THEN '.Not.Nullable()' ELSE '' END +
            '.LazyLoad();'
        END ELSE BEGIN
          PRINT '            Map(x => x.' + @cid + ')' +
            CASE WHEN @isnullable = 0 THEN '.Not.Nullable()' ELSE '' END +
            ';'
        END
      END
      FETCH cC INTO @cid, @datatype, @length, @isnullable,
        @isidentity, @lookup
    END

    CLOSE cC
    DEALLOCATE cC

    PRINT '        }'
    PRINT '    }'

    FETCH cT INTO @oid, @id, @tabletype
  END

  CLOSE cT
  DEALLOCATE cT

  PRINT '}'

These stored procedures can be invoked from the command line using the SQL Server osql tool

set osql=osql -S localhost -U [user] -P [pwd] -d [database] -n -w 1000
%osql% -Q "exec dev_Generate_DAL_Classes" -o "d:\path\to\Classes.cs"
%osql% -Q "exec dev_Generate_DAL_ClassMaps" -o "d:\path\to\ClassMaps.cs"

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).


Follow

Get every new post delivered to your Inbox.