NHibernate Tutorial 3: Using the Auto Mapping Feature

in #utopian-io7 years ago (edited)

What Will I Learn?

In this part of the series about NHibernate and its sister project Fluent NHibernate I will explain the auto mapping feature of Fluent NHibernate. Using this feature the task of mapping a domain model to an underlying data model becomes a snap.

Requirements

  • NHibernate
  • Fluent NHibernate
  • SQLite

Difficulty

  • Intermediate

Tutorial Contents

  • Auto mapping the domain
  • Configuring auto mapping
  • Using user defined conventions
  • Building the database schema
  • Using a base class for the entities

Curriculum

NHibernate Tutorial 3: Using the Auto Mapping Feature

In this part of the series about NHibernate and its sister project Fluent NHibernate I will explain the auto mapping feature of Fluent NHibernate. Using this feature the task of mapping a domain model to an underlying data model becomes a snap. The framework will do most of the work for us and we only have to add our own mapping related code if we want to fine tune the mapping process.  

The domain model

In the previous parts of this series I have introduced a simple but still useful domain model describing an order entry system. I have shown how this model can be mapped to a database model with the aid of Fluent NHibernate. In the following I will use FNH as an acronym for Fluent NHibernate. The mapping has been done for each class of the domain. Although the mapping was very simple and straight forward as well as type safe there is still one disadvantage; if we have a domain with many classes the manual mapping of them can become quite a burden.

Fortunately there exists a solution: We can let FNH do the work for us and just let it automatically map the domain model to a corresponding data model. When doing so FNH will analyze the classes defining the domain and use conventions to map them to underlying tables (and relations) in the database. Although the conventions used by FNH are quite reasonable we can still apply our own conventions. These conventions are fine grained thus we have extensive tuning possibilities.Let’s again implement our (simple) domain model for an order entry system from scratch as simple as possible. Here is the class diagram

Class diagram of the domain

And here the code

Domain model 

public class Product
{
   public virtual int Id { get; set; }
   public virtual string Name { get; set; }
   public virtual decimal UnitPrice { get; set; }
   public virtual bool Discontinued { get; set; }
}
public class Customer
{
   public virtual int Id { get; set; }
   public virtual string FirstName { get; set; }
   public virtual string LastName { get; set; }
}
public class Order
{
   public virtual int Id { get; set; }
   public virtual DateTime OrderDate { get; set; }
   public virtual Customer Customer { get; set; }
   public virtual IList<OrderItem> OrderItems { get; set; }
}
public class OrderItem
{
   public virtual int Id { get; set; }
   public virtual int Quantity { get; set; }
   public virtual Product Product { get; set; }
}

The above code probably reflects the simplest way to implement our domain model. I will later on extend improve the code as needed. But for the moment this is enough.

Configuring auto mapping

Let’s now configure our system to automatically map our domain to a data model. We do this by using the fluent configuration API offered by FNH. In my sample I'll use two different databases to explicitly show how (Fluent) NHibernate produces database specific schemas. The first database is SQLite  and the second one is SQL Server Compact Edition. Both are easy to set up and use and are file based.Let’s start with the configuration for SQLite. What I want to do is define a method that creates a session factory configured to use a SQLite database

Configuring NHibernate 

private static ISessionFactory CreateSessionFactory()
{
   return Fluently.Configure()
      .Database(SQLiteConfiguration.Standard.UsingFile(DbFile))
      .Mappings(...)
      .BuildSessionFactory();
}

I can use the class Fluently defined by FNH as well as one of the various predefined configuration classes. In our case it is the SQLiteConfiguration class. I tell the system that I want to use SQLite in file mode using a file with the name provided by the constant DbFile.The Mappings(…) part of the configuration defines where or how the mapping between the domain model and the data model is defined. In our class we want to let FNH do the hard work and use its auto mapping feature. FNH offers the class AutoPersitenceModel for this purpose. 

.Mappings(m => m.AutoMappings.Add(
                   AutoPersistenceModel
                       .MapEntitiesFromAssemblyOf<Customer>()
                       .Where(t => t.Namespace.EndsWith("Domain"))
   )
  )

The mappings method expects a lambda expression. Our expression auto-creates mappings for all types found in the assembly where the class Customer is defined but only those types whose namespace ends with “Domain”. If we would not have the where clause to filter the set of types to map then FNH would try to just map every class in the assembly which is certainly not what we want.If I want to use SQL Server Compact Edition instead the only change I have to make in the above code is replacing the .Database(…) line with the following code snippet 

.Database(MsSqlCeConfiguration.Standard.ConnectionString(
                 C => c.Is("Data Source=AutomappingSample.sdf")))

In this case I use the pre defined class MsSqlCeConfiguration and explicitly define a connection string to use. The above connection string just instructs the database to use a file called AutomappingSample.sdf.

When using SQLite as database the schema generation script will be the following

Schema generation script for SQLite 

create table "Product" (
  Id  integer,
  Name TEXT,
  UnitPrice NUMERIC,
  Discontinued INTEGER,
  primary key (Id)
)
create table "Customer" (
  Id  integer,
  FirstName TEXT,
  LastName TEXT,
  primary key (Id)
)
create table "Order" (
  Id  integer,
  OrderDate DATETIME,
  Customer_id INTEGER,
  primary key (Id)
)
create table "OrderItem" (
  Id  integer,
  Quantity INTEGER,
  Product_id INTEGER,
  Order_id INTEGER,
  primary key (Id)
)

And for SQL Server compact edition we get this schema generation script (which is the same for SQL server)

Schema generation script for SQL Server 

create table "Product" (
  Id INT IDENTITY NOT NULL,
  Name NVARCHAR(255) null,
  UnitPrice NUMERIC(19,5) null,
  Discontinued BIT null,
  primary key (Id)
)
create table "Customer" (
  Id INT IDENTITY NOT NULL,
  FirstName NVARCHAR(255) null,
  LastName NVARCHAR(255) null,
  primary key (Id)
)
create table "Order" (
  Id INT IDENTITY NOT NULL,
  OrderDate DATETIME null,
  Customer_id INT null,
  primary key (Id)
)
create table "OrderItem" (
  Id INT IDENTITY NOT NULL,
  Quantity INT null,
  Product_id INT null,
  Order_id INT null,
  primary key (Id)
)
alter table "Order"
   add constraint FK3117099B1322E4
   foreign key (Customer_id)
   references "Customer"
alter table "OrderItem"
   add constraint FK3EF88858772670B0
   foreign key (Product_id)
   references "Product"
alter table "OrderItem"
   add constraint FK3EF88858DC9ABAD3
   foreign key (Order_id)
   references "Order"

Please especially note the following

  • To keep the script short the drop commands have been omitted
  • The tables are named according to the respective classes
  • The column names used correspond to the mapped property name
  • The property Id of each class is recognized as primary key of the respective entity/table
  • On SQL Server identity fields are used as primary key.
  • Properties of type string are mapped to NVARCHAR(255) in the case of SQL Server
  • Foreign key columns are named according to the name of the referenced table combined with the postfix _id.
  • The names of the foreign key constraints (only SQL Server) are cryptic

This is the result of the default conventions that were applied during the generation of the database schema script. We can now start to fine tune this by defining our own conventions.

Using user defined conventions

A convention is defined in a class that has to implement an interface pre defined by FNH. As I already told previously the conventions are really fine grained thus loads of different interfaces are defined which we can implement if needed. As a simple sample let’s say that we want our properties of type string to always be mapped to have a maximal length of 100 characters. Any of the possible convention interfaces to implement ultimately inherits from the base interface IConvention<T> which defines the two methods Accepts(T target) and Apply(T target). Thus we will have to implement exactly those two methods in our class.

User defined convention for properties of type string 

public class MyStringLengthConvention : IPropertyConvention
{
   public bool Accept(IProperty target)
   {
       return target.PropertyType.Equals(typeof (string));
   }
   public void Apply(IProperty target)
   {
       target.WithLengthOf(100);
   }
}

The Accepts method defines when a convention can be applied and the Apply method contains the logic which applies the convention to the given element. In the above case we accept any target that is of type string and in the Apply method we define the maximal string length to be used in this case.In a previous post I have mentioned that it is an anti-pattern to use identity fields as primary keys for SQL Server in conjunction with NHibernate. It is preferable to e.g. use the HiLo generator if using numbers as primary key; another alternative would be the usage of Guids as primary keys. For performance reasons use the GuidComb generator when using Guids as primary keys. Having said this let’s define our own id convention which uses the HiLo generator instead of the identity generator. In this case we write a class that implements the interface IIdConvention.

User defined convention for id 

public class MyIdConvention : IIdConvention
{
   public bool Accept(IIdentityPart target)
   {
       return true;
   }
   public void Apply(IIdentityPart target)
   {
       target.GeneratedBy.HiLo("1000");
   }
}

We want to apply this convention for any entity in our domain so we just return true in the Accept method. In the Apply method we define which (id) generator we want to use. In this case it is the HiLo generator with an interval of 1000. Why we have to define the “maxLo” value as string and not as a number is not clear; probably it’s a bug.

Now we have to tell the system to use these conventions instead of the default conventions. We can do this either by explicitly add each convention to the configuration of NHibernate or by letting FNH scan an assembly for implementations of conventions. Let’s start with the former. We can add a convention by using the ConventionDiscovery method of the AutoPersistenceModel class.

Configuration adding user defined conventions 

create table "Customer" (
  Id INT not null,
  FirstName NVARCHAR(100) null,
  LastName NVARCHAR(100) null,
  primary key (Id)
)
// code omitted for brevity
create table hibernate_unique_key (
    next_hi INT
)
insert into hibernate_unique_key values ( 1 )

After modifying the configuration with our own conventions the schema generation script for SQL Server looks like this

Schema generation script for SQL Server after applying custom conventions 

create table "Customer" (
  Id INT not null,
  FirstName NVARCHAR(100) null,
  LastName NVARCHAR(100) null,
  primary key (Id)
)
// code omitted for brevity
create table hibernate_unique_key (
    next_hi INT
)
insert into hibernate_unique_key values ( 1 )

An additional table called hibernate_unique_key is created which is used for the management of the ids by NHibernate. Ids are no longer columns of type identity but just of type int. Not SQL Server manages the ids now but NHibernate does it. Furthermore we can see that the properties of type string are mapped to columns of type NVARCHAR(100).

Building the database schema

In the above configuration we use the ExposeConfiguration method which expects a delegate to a method that will be called during the configuration process. In our case the delegate points to a method called BuildSchema. This method has one parameter of type Configuration. We use the configuration object to create the database schema with the aid of the SchemaExport utility class of NHibernate.

Code needed to create the database schema 

private static void BuildSchema(Configuration config)
{
   new SchemaExport(config).Create(true, true);
}

Note that in a productive environment you would omit this part since the database schema is generated only once; probably by a DBA.

Using a base class for the entities

Our domain model has some weaknesses so far. One weakness is that entities do not implement the functionality do determine whether two instances represent the same entity. Equality is based on the id (primary key) of an entity. If two instances have the same id then they are the same. Since every entity has an id and every entity needs to be comparable and to keep our code DRY it is best to define a base class for all entities which implements this common logic. Let’s call our base class EntityBase. All entities will now inherit from this class. For further details please consult the code provided with this tutorial.

If we were to run this now, we wouldn't get the mapping we desire. FNH would see EntityBase as an actual entity and map it with all other entities (e.g. Customer or Order) as subclasses; this is not what we desire, so we need to modify our auto mapping configuration to reflect that.

After MapEntitiesFromAssemblyOf<EntityBase>() we need to alter the conventions that the auto mapper is using so it can identify our base-class. 

return Fluently.Configure()
   .Database(MsSqlCeConfiguration.Standard.ConnectionString(
                 C => c.Is("Data Source=AutomappingSample.sdf")))
   .Mappings(m => m.AutoMappings.Add(
       AutoPersistenceModel
           .MapEntitiesFromAssemblyOf<EntityBase>()
           .WithSetup(s =>  
           {  
               s.IsBaseType = type => type == typeof(EntityBase);  
           })
           .Where(t => t.Namespace.EndsWith("Domain"))
           .ConventionDiscovery.Add<MyStringLengthConvention>()
           .ConventionDiscovery.Add<MyIdConvention>()
               ))
   .ExposeConfiguration(BuildSchema)
   .BuildSessionFactory();

We've added the WithSetup call in which we replace the IsBaseType convention with our own. This convention is used to identify whether a type is simply a base-type for abstraction purposes, or a legitimate storage requirement. In our case we've set it to return true if the type is an EntityBase.With this change, we now get our desired mapping. EntityBase is ignored as far as FNH is concerned, and all the properties (Id in our case) are treated as if they were on the specific subclasses.

Summary

In part 3 of this tutorial series about NHibernate and Fluent NHibernate I have discussed how to let Fluent NHibernate automatically map a domain model to a data model. We have realized that FNH provides a reasonable mapping out of the box by using default conventions. I have shown how one can implement user defined conventions which will influence how the mapping is defined on a very fine grained level. I have also shown that if we use a base class in our domain which implements common functionality we can instruct FNH to ignore this class and just map the "real" entities.

FNH with its auto mapping feature reduces the task of mapping a complex domain to an underlying data model to just a few keystrokes. And as is always true: less code results in less bugs and less maintenance overhead. 



Posted on Utopian.io - Rewarding Open Source Contributors

Sort:  

Thank you for the contribution. It has been approved.

You can contact us on Discord.
[utopian-moderator]

Hey @haig I am @utopian-io. I have just upvoted you!

Achievements

  • You have less than 500 followers. Just gave you a gift to help you succeed!
  • Seems like you contribute quite often. AMAZING!

Suggestions

  • Contribute more often to get higher and higher rewards. I wish to see you often!
  • Work on your followers to increase the votes/rewards. I follow what humans do and my vote is mainly based on that. Good luck!

Get Noticed!

  • Did you know project owners can manually vote with their own voting power or by voting power delegated to their projects? Ask the project owner to review your contributions!

Community-Driven Witness!

I am the first and only Steem Community-Driven Witness. Participate on Discord. Lets GROW TOGETHER!

mooncryption-utopian-witness-gif

Up-vote this comment to grow my power and help Open Source contributions like this one. Want to chat? Join me on Discord https://discord.gg/Pc8HG9x

Coin Marketplace

STEEM 0.17
TRX 0.15
JST 0.028
BTC 58044.48
ETH 2352.63
USDT 1.00
SBD 2.36