博客园  :: 首页  :: 联系 :: 管理

NHibernet (Hibernet in .NET)

Posted on 2007-07-27 15:11  sunrack  阅读(1637)  评论(2编辑  收藏  举报

Introduction

Hibernate and Log4J are a de-facto standard of ORM (Object-relational mapping) and logging (respectively) in Java world. That's why both were ported to .NET runtime environment.

NHibernate is ORM solution and is intended for transparent binding .NET classes to database tables. NHibernate was designed to reduce time efforts to switch application to another database provider. It is achieved by means of replacing SQL with special NHibernate query language (HQL), providing dialects for several databases and providing special classes for retrieving/updating data in database.

Log4Net is logging framework which is useful for tracking errors in application and can write logs either to database table or flat files. You can download sample Visual Studio .NET project here

NHibernate in n-tier application

Speaking about modern approach to web-site development with ASP.NET you should be acquainted with n-tier approach. Generally n-tier application consists of data access tier, business logic tier, presentation tier and domain objects which spans across all tiers (this is 3-tier approach which is shown below)


Figure 1: A Typical 3-tier Model

In the figure above NHibernate manages Data Access tier which is responsible of loading/updating data into/from domain objects. Business Logic tier uses Data access objects to access data and perform computations upon data, while Presentation tier (in our case ASP.NET 2.0 pages) displays data to end-user and process user input passing it back to Business logic tier and so on.

Getting started with NHibernate

Now we can try to create a simple ASP.NET 2.0 application using NHibernate objects as Data Access tier. First of all we should start new ASP.NET project and add NHibernate binaries to project references (this is done via Add reference option from project menu. NHibernate binaries can be found at http://www.hibernate.org/6.html. We need several libraries from NHibernate package. The best way is to add all .dll files from NHibernate distribution located at bin\net-2.0 folder (in version 1.2). Among them you will notice log4net library which we will use later.

The next thing to do - is to configure our NHibernate to use certain database. This is done in web.config file for your web site. Here is example of typical content of web.config:

< ?xml version="1.0"?>
< configuration xmlns="http://schemas.microsoft.com/.NetConfiguration/v2.0">
  < configSections>
      < section name="nhibernate" type="System.Configuration.NameValueSectionHandler, System, Version=1.0.5000.0,Culture=neutral, PublicKeyToken=b77a5c561934e089"/>
  < /configSections>
  < system.web>
      ...
  < /system.web>
  < appSettings>
      ...
  < /appSettings>
  < nhibernate>
      < add key="hibernate.show_sql" value="true"/>
      < add key="hibernate.connection.provider"
      value="NHibernate.Connection.DriverConnectionProvider"/>
      < add key="hibernate.dialect" value="NHibernate.Dialect.MsSql2000Dialect"/>
      < add key="hibernate.connection.driver_class" value="NHibernate.Driver.SqlClientDriver"/>
      < add key="hibernate.connection.connection_string" value="Password= Persist Security Info=True User ID=sa Initial Catalog=test Data Source=127.0.0.1 "/>
      < add key="hibernate.cache.use_query_cache" value="true"/>
  < /nhibernate>
< /configuration>

The most important part for us in this fragment is connection string which should be set as a value of "hibernate.connection.connection_string" key. It should point to database intended for the test application.

Going on with session

As in ADO.NET we use database connection class to create queries or data adapter, in NHibernate we use session object which is responsible for maintaining connection to database, loading data into domain objects, detecting changes to domain object and syncing domain state with database. NHibernate requires at least one session to work. As NHibernate caches domain object instances to ensure that single row is loaded only once from database multi-user work with one session object is questionable. This issue differ usage of NHibernate in WinForms and ASP.NET application. The general idea is to have only one session per request and this can be achieved either by using singleton pattern on using HttpModule. We will use the last one. To do this we need to create a custom class, make it inherit IHttpModule interface and add following lines to web.config system.web section:

  < httpModules>
      < add type="Dummy.DAL.NHibernateHttpModule" name="NHibernateHttpModule"/>
  < /httpModules>

NHibernateHttpModule - the class you’ve created - is fair simple. The main idea is to open session upon beginning of the request and flush and close session when the request has ended. This class should contain following code:

public static readonly string KEY = "NHibernateSession"
private static ISession _session
 
private void context_BeginRequest(object sender, EventArgs e)
{
          HttpApplication application = (HttpApplication)sender
          HttpContext context = application.Context  
          context.Items[KEY] = OpenSession()
}
 
private void context_EndRequest(object sender, EventArgs e)
{
HttpApplication application = (HttpApplication)sender
          HttpContext context = application.Context
 
          ISession session = context.Items[KEY] as ISession
          if (session != null)
          {
                      try
                    {
                                session.Flush()
                                session.Close()
                      }
                    catch {}
          }
context.Items[KEY] = null
}
 
public static ISession CurrentSession
{
get
          {
                    if (HttpContext.Current==null)
                      {
                                if (_session!=null)
                                  {
                                            return _session
                                  }
                                  else
                                  {
                                            _session = OpenSession()
                                              return _session
                                }
                      }
                      else
                      {
                                HttpContext currentContext = HttpContext.Current
                                  ISession session = currentContext.Items[KEY] as ISession
                                  if (session == null) {
                                            session = OpenSession()
                                            currentContext.Items[KEY] = session
                                  } 
                                  return session
                      }
          }
}

The code above is rather simple. We open session once the request started (context_BeginRequest) and put it to the Context. When session ends we simply get session from Context and close it. OpenSession is the function responsible for creating NHibernate session instance from session factory object.

private static ISessionFactory factory = null
private static ISessionFactory getFactory()
{
if(factory==null)
          {
                    Configuration config
                      config = new Configuration()
                      if (config==null)
                      {
throw new InvalidOperationException("NHibernate configuration is null.")
                      }
                      config.AddAssembly("Dummy.Assembly")
                      factory = config.BuildSessionFactory()
                      if (factory==null)
                      {
throw new InvalidOperationException("Call to Configuration.BuildSessionFactory() returned null.")
                      }
          }
          return factory
                     
}
                     
public static ISession OpenSession()
{
ISession session
session = getFactory().OpenSession()
if (session==null)
          {
throw new InvalidOperationException("Call to factory.OpenSession() returned null.")
          }
return session
}

In the code above we start by creating new Configuration object, which holds database connection settings, which in turn we specified in web.config file (do you remember hibernate.connection.connection_string key from web.config?). Once configuration is initialized we should tell NHibernate where to find domain objects for our application. It's good practice to have them in separate assembly along with Data Access layer and Business Logic layer. After SessionFactory is initialized we can use SessionFactory.OpenSession() method to retrieve new NHibernate session object.

Building domains

Our ASP.NET application is NHibernate ready, so our next step is to add some domains bound to database tables. First of all we need to create tables:

CREATE TABLE users (
  UserID nvarchar(20) NOT NULL default '0',
  Name nvarchar(40) default NULL,
  Password nvarchar(20) default NULL,
  PRIMARY KEY  (UserID)
)
 
CREATE TABLE user_transactions (
  TransID nvarchar(20) NOT NULL default '0',
  UserId nvarchar(40) default NULL,
  TransDate datetime default NULL,
  Amount decimal default NULL,
  PRIMARY KEY  (TransID)
)

These are two simple tables where each user can have several roles. Next step is to create classes for domain objects. This process is pretty straightforward - we just need to duplicate table structure in the class and provide a way for linking two tables.

namespace Dummy.Assembly
{
      public class User
      {
              private string userId
              private string name
              private string password
              private IList transactions
 
              public User()
              {
              }
 
              public string Id
              {
                      get { return userId }
                      set { userId = value }
              }
 
              public string Name
              {
                      get { return name }
                      set { name = value }
              }
 
              public string Password
              {
                      get { return password }
                      set { password = value }
              }
 
              public IList Transactions
              {
                      get { return transactions }
                      set { transactions = value }
              }
      }
}

As transactions and users have many-to-one relationships we've created a property to hold a list of transactions belonging to this user. Transaction class is similar to User class but instead of IList it holds an instance of User object.

namespace Dummy.Assembly
{
      public class Transaction
      {
              private string transId
              private User user
              private DateTime transDate
              private decimal amount
 
              public Transaction()
              {
              }
 
              public string Id
              {
                      get { return transId }
                      set { transId = value }
              }
 
              public User User
              {
                      get { return user }
                      set { user = value }
              }
 
              public DateTime TransDate
              {
                      get { return transDate }
                      set { transDate = value }
              }
 
              public decimal Amount
              {
                      get { return amount }
                      set { amount = value }
              }
      }
}

Once domain classes are ready we should bind classes to tables and tell NHibernate which column matches which property from the domain class. This is done view hibernate mapping files - which must be placed in the namespace we've specified as a parameter for SessionFactory class constructor.

Important note: User.hbm.xml and Transaction.hbm.xml files should have "Embeded resource" type.

Mapping files shown above should have name in following format: < ClassName> .hbm.xml (.hbm - means Hibernate mapping). They are pretty simple: class tag tells hibernate to map Dummy.Assembly. User class form assembly named Dummy.Assembly to table users id denotes primary key field (there must be primary key field in every domain). Property tags does all the magic with mapping properties (name attribute) of the class to respective column of table (column attribute). And more complex bag and many-to-one tags are used to specify relationship between two domains. Bag is used to map one-to-many relations by means of specifying foreign key (< key column="UserId" /> ) in table user_transactions and specifying target relation (one-to-many tag which tells NHibernate that foreign key is found in Transaction class). NHibernate while loading user from table will also take user primary key value and load data from Transaction table, making sure that property UserId is the same as user primary key value. Once all transactions are loaded they will be placed in respective collection - in our case it's Transaction in User class. Another important part there is lazy attribute of bag tag. Let's imagine we have 1 million users and each of them have 1 million transactions. When we want to load all users and reset their passwords at once we will force a big performance problem. That's because NHibernate will load us 1,000,000*1,000,000 records from the database, but we don't need transactions at the moment. Lazy loading is what will help us. Lazy loaded collection is not loaded once its parent object initialized, it only loads when our classes access this collection. Simply setting most of the relations to be lazy will boost performance a lot.

Once our mapping files are ready and placed in proper assembly along with domain classes we can go further. Now we need to use NHibernate session to manage our domain objects. The following code creates new user:

ITransaction transaction = session.BeginTransaction()
User u = new User()
u.Id = "admin"
u.Name = "Administrator"
u.Password = "secret"
 
session.Save(u)
transaction.Commit()
 
transaction = session.BeginTransaction()
u.Name = "Admin"
session.Update(u)
transaction.Commit()

Session variable is the NHibernate session we've instantiated in our HTTP module class. Example is fair simple. We simply create new domain object, fill its fields and tell the session to save changes. After that domain object will be persisted to database. In the next step we change user name and update the record in database. Now we need to load this user into our domain object:

User admin = (User) session.Load(typeof(User), "admin")

Load method loads user record identified by primary key value "admin" from database into our domain object. Isn't it simple?

Queries and Criteria

Loading objects by id good, but what to do when we need to load all objects or filter them by some criteria? Here comes HQL - a special query language used to retrieve data in NHibernate. It's as simple as SQL and moreover - it's SQL adapted for NHibernate. HQL instead of table columns uses properties from domain objects, instead of tables - domains. HQL is case sensitive, which means property names should be written in HQL just like in domain object. Those are the main differences. Examples of HQL below:

FROM User WHERE Name='Admin'
SELECT COUNT(*) FROM User u

In HQL you're allowed to drop SELECT statement. In this case all Users will be selected. To run query you should use simple query class provided with NHibernate:

IQuery query = session.CreateQuery("FROM User WHERE Name=:name")
query.SetString("name", "Admin")
IList users = query.List()

We simply create a query, then pass required parameters and execute List method which will return a collection of Users (even if there only one user). To retrieve only one object from query (as in example with count) you can use UniqueResult method of IQuery.

Another way to load your objects is to use ICriteria interface. Using criteria can be considered more OOP-oriented rather than using queries. Both have their advantages and drawbacks. Using criteria is as simple as using query:

ICriteria criteria = session.CreateCriteria(typeof(User))
criteria.Add(Expression.Eq("Name", "Admin"))
IList users = criteria.List()

We create criteria for user class, and then, using Expression class we tell the criteria to load users that have their "Name" field equal (Eq) "Admin".

It's seems easy to use NHibernate - but really it's not that easy. The main problem is that you can't test your query unless you run your code (SQL query you can run in special SQL editors/analyzers and NHibernate does not have one). Another problem is multiple updates of single instance of domain object is common for the beginners.

Adding Log4Net to ASP.NET application

To track possible problems Log4Net can be used. Its setup is easy. To make it work with ASP.NET you should add following lines to web.config file:

  < configSections>
      . . .
      < section name="log4net"
      type="log4net.Config.Log4NetConfigurationSectionHandler,log4net"/>
  < /configSections>
  < log4net>
      < appender name="GeneralLog" type="log4net.Appender.RollingFileAppender">
          < file value="Logs/general.txt"/>
          < appendToFile value="true"/>
          < maximumFileSize value="100KB"/>
          < rollingStyle value="Size"/>
          < layout type="log4net.Layout.PatternLayout">
              < conversionPattern value="%d{HH:mm:ss} [%t] %-5p %c - %m%n"/>
          < /layout>
      < /appender>
      < root>
          < level value="DEBUG"/>
          < appender-ref ref="GeneralLog"/>
      < /root>
      < logger name="NHibernate" additivity="false">
          < level value="DEBUG"/>
          < appender-ref ref=" GeneralLog"/>
      < /logger>
  < /log4net>
  . . .

Log4Net is based on appenders and loggers. Appenders are used to write logs to some destination (in our example - flat file), while loggers determine which appender should certain namespace use. There is always one appender and root logger (it logs from all classes). Additional loggers have a mandatory attribute name which specifies the namespace to which this logger is applied. For example - you can have a separate log files for Data Access layer and Business Layer. Logger level determines which of the log messages should be printed to log. There are several levels - DEBUG, INFO, WARN, ERROR, FATAL. Debug - include all levels, while Warn include Warn, Error and Fatal and so on.

The next thing is to add to our NHibernate HTTP module's context_BeginRequest procedure following lines to initialize log4net logging system:

log4net.Config.XmlConfigurator.Configure()

or

log4net.Config.BasicConfigurator.Configure()

Now we can use Log4Net:

private static ILog log = LogManager.GetLogger(typeof(User))
 
public void SomeMethod() {
          log.Info("SomeMethod called")
          try {
                      session.Load(typeof(User), "admin")
} catch (Exception ex) {
                    log.Error("Failed to load amin user")
                    log.Debug("Stack trace follows", ex)
} finally {
                    log.Info("SomeMethod end")
}
}

In example we create logger, giving it the typeof(User). This will be used by Log4Net to determine what logger and appender from web.config file to use. Next we generate some log data of different levels (Info, Error, Debug). As our root logger is set to Debug level we will see all the messages. When we don't want a mess in our logs, we can simply change level of appropriate logger to Error. Then no messages from log.Debug and log.Info methods will appear in our log. That's good while using NHibernate because it likes logging much and this decrease performance and log files will grow fast. So it's a good idea to lower log level while setting up live server. Also it's a good practice to have log output in all try-catch blocks.