Chapter 1. NHibernateEg.Tutorial1A

Table of Contents

Presentation of Order and Shop
Quick review of ADO.NET methods
Using Object / Relational Mapping and NHibernate
Implementation of Order and introduction to NHibernate.Mapping.Attributes
The class header
The identifier and its generator
The properties
The methods
Few remarks
Configuration of NHibernate (database and mapping)
Persistence API
CRUD operations
CREATE: Save
RETRIEVE: Query and Load/Get
UPDATE: SaveOrUpdate / Save / Update
DELETE: Delete(HQL) and Delete(Entity)
Conclusion

Overview

Here you will discover how to set up NHibernate and use it for basic operations.

You should have all the requirements and the source code (or at least the binary files).

Presentation of Order and Shop

The application of this tutorial mimics a (very simple) shop. The main (and only) managed class is the Order.

Order is a class which has an identifier that makes it unique. Its properties are:

  • A Date telling when the order was created

  • The name of the product ordered (for simplicity sake, an order can only reference one product and it is its name that is stored)

  • The quantity of items (of the product) ordered

  • The total price of this order (that is quantity * unit price)

Shop is a class that can be used to do some operations related to Order. You can ask the shop to:

  • Generate some random orders; it is useful to fill the database so that you can directly operate on it

  • Print out some field of all orders in the database

  • Load an order: Get the row and convert it to an instance of the class Order

  • Save/Update/Delete an order (in the database)

Now, run the executable file NHibernateEg.Tutorial1A.exe to see all these operations in action.

Before running the application

You need to configure your database. Open the file NHibernateEg.Tutorial1A.exe.config. By default, this application uses a database named "nhibernate" in Microsoft SQL Server 2000 (or MSDE).

If you want to use a MySQL database, change the line: <add key="Database" value="MSSQL" /> to <add key="Database" value="MySQL" />. And make sure that the files MySql.Data.dll and ICSharpCode.SharpZipLib.dll are in your path (they should already be in the current directory).

You can also change the related Connection String if required. Set the value of: MSSQL.ConnectionString (for SQL Server) or MySQL.ConnectionString (for MySQL).

Note that you just have to create the database and configure its access; the application will fill it (after dropping the conflicting tables).

When running the application, you should get something like:

Application is starting...

Configuration of NHibernate...

Use database: <MSSQL>

NHibernate.Mapping.Attributes.HbmSerializer.Default.Serialize()...

new NHibernate.Tool.hbm2ddl.SchemaExport(cfg).Create()...
drop table SimpleOrder
create table SimpleOrder (
  Id INT IDENTITY NOT NULL,
   Date DATETIME null,
   Product NVARCHAR(255) not null,
   Quantity INT null,
   TotalPrice INT null,
   primary key (Id)
)

sessionFact = cfg.BuildSessionFactory();


Saving 3 aleatory orders...

3 orders found!
  Order N°1,  Date=2005-11-09 21:54:11Z,  Product=P1
  Order N°2,  Date=2005-11-09 21:54:11Z,  Product=P2
  Order N°3,  Date=2005-11-09 21:54:11Z,  Product=P3

Loading order N° 1...

Order N°1
 Date = wednesday 9 november 2005 20:54:11
 Product=P1 (updated),  Quantity=3,  TotalPrice=9

Update the order N° 1...

Save the order N° 0...

Change the time zone of all orders: n=25...
4 updated orders!

Deleting the order N° 2...

3 orders found!
  Order N°1,  Date=2005-11-10 22:54:11Z,  Product=P1 (updated)
  Order N°3,  Date=2005-11-10 22:54:11Z,  Product=P3
  Order N°4,  Date=2005-11-10 22:54:12Z,  Product=New

Application is closed!

If you get an exception, read it to understand what the exact problem is (it is probably related to your database installation or your connection string).

Study this output to understand what we will achieve in the next sections.

Here is the code of the method Program.Main() which generate this output:

	string database = System.Configuration.ConfigurationSettings.AppSettings["Database"];
	string connectionString = System.Configuration.ConfigurationSettings.AppSettings[database + ".ConnectionString"];

	Shop shop = new Shop(database, connectionString);

	shop.GenerateRandomOrders(3);

	shop.WriteAllOrders();

	Order o = shop.LoadOrder(1);

	o.Product += " (updated)";
	shop.Write(o);
	shop.Save(o);

	shop.Save( new Order("New", 4, 2) );

	shop.ChangeTimeZone(25);

	shop.Delete(2);

	shop.WriteAllOrders();

The strings database and connectionString are extracted from the file NHibernateEg.Tutorial1A.exe.config and sent to the Shop for its configuration.

Then we do some CRUD operations on Order: Create, Retrieve, Update and Delete. Their implementations will exhibit the usage of NHibernate for basic operations.

Quick review of ADO.NET methods

If you are familiar with ADO.NET, you can quickly create the application we described following these steps:

  • Creation of tables in DB + Typed DataSet (can be generated - optional)

  • Creation of DbConnection + DataAdapters + Commands (can be generated)

  • Application lifecycle (=> implementation of CRUD operations)

This method has some drawbacks (which increase with the complexity of the software):

  • Lack of flexibility / hard to maintain

  • Hard to do Object-Oriented Programming (OOP) on top of that

  • Hard to separate the Domain Model from the DB

Using Object / Relational Mapping and NHibernate

After the theoric presentation made in the section called “Object / Relational Mapping”, we will focus on the practical aspect.

An Object / Relational Mapping solution allows focusing on the object model when designing an application. The database is hidden by a persistence framework.

While implementing the Shop, you will see that we almost never speak about the database (except to configure its access) and we will never manipulate tables/rows or SQL queries.

You will discover the benefits of this technology when using it.

The step zero is to create a Console Application and add the libraries: NHibernate.dll, log4net.dll and NHibernate.Mapping.Attributes.dll as references.

Implementation of Order and introduction to NHibernate.Mapping.Attributes

Now, we are going to implement the class Order. That is: Create the class, add the fields / properties and the methods.

To manipulate the classes, NHibernate needs a mapping between these classes and the database tables. Here, we use the NHibernate.Mapping.Attributes to provide this mapping information.

NHibernate.Mapping.Attributes uses .NET Attributes to define the mapping.  Basically, for each element in your classes, you put the right attribute on it so that NHibernate will know how this element is mapped to its equivalent in the database. Each attribute has many properties used to specify how the mapping should work. Read NHibernate mapping reference documentation to understand their meaning.

Here is the implementation of the class Order:

The class header

	[NHibernate.Mapping.Attributes.Class(Table="SimpleOrder")]
	public class Order
	{
	}

The .NET attribute [Class] is used to tell to NHibernate that this is a mapped class. With Table="SimpleOrder", we give the name of the table in which orders are stored.

The identifier and its generator

	private int _id = 0;
	[NHibernate.Mapping.Attributes.Id(Name="Id")]
		[NHibernate.Mapping.Attributes.Generator(1, Class="native")]
	public virtual int Id
	{
		get { return _id; }
	}

The identifier is defined with the attribute [Id]. Don't forget to set its name (it can not be guessed...).

The identifier can be assigned by the database, the application (you) or NHibernate. Read the documentation for more details on these options. The attribute [Generator] is used to select one of these strategies. We let the database choose the identifier, that's why we write Class="native".

There is no set { ... } because the Id should never be changed (only NHibernate changes it when it is loaded/persisted).

As you can see, we have two attributes on this property (and there can be even more). [Generator] is indented to stress that it belongs to [Id]; and "1" (same as Position=1) tells that it comes after [Id] (which position is "0"); .NET attributes are not automatically ordered.

The properties

	private System.DateTime _date = System.DateTime.Now;
	private string _product;
	private int _quantity;
	private int _totalPrice;

	[NHibernate.Mapping.Attributes.Property]
	public virtual System.DateTime Date
	{
		get { return _date; }
	}

	[NHibernate.Mapping.Attributes.Property(NotNull=true)]
	public virtual string Product
	{
		get { return _product; }
		set { _product = value; }
	}

	[NHibernate.Mapping.Attributes.Property]
	public virtual int Quantity
	{
		get { return _quantity; }
		set { _quantity = value; }
	}

	[NHibernate.Mapping.Attributes.Property]
	public virtual int TotalPrice
	{
		get { return _totalPrice; }
		set { _totalPrice = value; }
	}

[Property] is used for fields that directly map to database's columns. The type of the property and the column should be compatible.

NotNull=true is written because the product's name should not be null. If you want to have nullable properties (for bool/int/float/DateTime/...), you can use the Nullables library (it is distributed in the NHibernateContrib package).

If you wonder how NHibernate can set read-only properties, you will see that NHibernate is configured to use the private fields. The advantage of this setting is that you can use the properties names in Queries and as NHibernate accesses directly to fields, it doesn't execute eventual business logic contained in the properties.

All your members (properties, methods and events) which access these fields directly should be virtual; it is required when lazy loading is enable on the entity (it is used in Chapter 2, NHibernateEg.Tutorial1B).

Therefore, if you have a member which is not virtual (or doesn't belong the entity) and which need to access to a field, it should use a virtual member of the entity (directly or not); most of the time, the virtual property of this field.

The methods

	public Order()
	{
	}

	public Order(string product, int unitPrice, int quantity)
	{
		this.Product = product;
		this.Quantity = quantity;
		this.ComputeTotalPrice(unitPrice);
	}

	public void ComputeTotalPrice(int unitPrice)
	{
		this.TotalPrice = unitPrice * this.Quantity;
	}

	public virtual void ChangeTimeZone(int n)
	{
		this._date = this.Date.AddHours(n);
	}

As you can see, fields are never used directly; the only exception is ChangeTimeZone() which is virtual.

The class Order has a constructor which takes no parameter (public Order()). This is required as NHibernate needs it to create instances internally. But this constructor can be protected and even private if lazy loading is not enable for your entity.

Few remarks

If you don’t set the name of the table/column (in the attributes) where the class/property is loaded/saved, NHibernate will guess that they have the same name.

To fully understand NHibernate.Mapping.Attributes, you should read its documentation. And NHibernate documentation contains explanations for all mapping.

As you read other articles/samples, you will discover that the mapping information is stored in .hbm.xml files. In fact, using attributes is a simpler and nicer way to generate this information (it is also far less verbose). Anyway, someday, as you progress in your usage of NHibernate, you will probably have to hand-write these files.

Configuration of NHibernate (database and mapping)

This configuration is done in the constructor of Shop: public Shop(string database, string connectionString).

Before configuring NHibernate, there is a library you need to know: log4net.

log4net is a logging system.  Basically, it receives log messages, filters them as you want and sends them wherever you want (in a file, on the console, in a database, as e-mail, etc.)

Its configuration is in the file NHibernateEg.Tutorial1A.exe.config, in the section called <log4net>. You have to call one of the log4net.Config.XmlConfigurator.Configure() methods before using it; these methods read the configuration.

Finally, you put somewhere in your code:

[assembly: log4net.Config.XmlConfigurator(Watch=true)]

Watch=true tells to log4net to detect changes made on the configuration file at run-time (very useful to not have to restart your application...). For more information, go to log4net website.

NHibernate needs some information to know how to communicate with the database:

  • ConnectionProvider: NHibernate.Connection.DriverConnectionProvider

  • Dialect and ConnectionDriver: they depend of your database.

  • ConnectionString: the string used to create a database connection.

The <appSettings /> section (in NHibernateEg.Tutorial1A.exe.config) makes it possible to switch from one database to another without changing a single line of code. Doing this with plain ADO.NET is not straightforward.

Here is the code used to set this information:

	// Create the object that will hold the configuration settings
	// and fill it with the information to access to the Database
	NHibernate.Cfg.Configuration cfg = new NHibernate.Cfg.Configuration();
	cfg.SetProperty( NHibernate.Cfg.Environment.ConnectionProvider,
		"NHibernate.Connection.DriverConnectionProvider" );

	if("MSSQL" == database)
	{
		cfg.SetProperty(NHibernate.Cfg.Environment.Dialect, "NHibernate.Dialect.MsSql2000Dialect");
		cfg.SetProperty(NHibernate.Cfg.Environment.ConnectionDriver, "NHibernate.Driver.SqlClientDriver");
		cfg.SetProperty(NHibernate.Cfg.Environment.ConnectionString, connectionString);
	}
	else [...]

Here, you can read the value of Dialect and ConnectionDriver for a Microsoft SQL Server 2000 database. These values are set with the method SetProperty(). They can also be set with a XML file.

We extract the mapping information using NHibernate.Mapping.Attributes:

	System.IO.MemoryStream stream = new System.IO.MemoryStream(); // Where the information will be written in
	// Ask to NHibernate to use fields instead of properties
	NHibernate.Mapping.Attributes.HbmSerializer.Default.HbmDefaultAccess = "field.camelcase-underscore";(1)
	// Gather information from this assembly (can also be done class by class)
	NHibernate.Mapping.Attributes.HbmSerializer.Default.Serialize( stream,
		System.Reflection.Assembly.GetExecutingAssembly() );(2)
	stream.Position = 0;
	cfg.AddInputStream(stream);(3) // Send the Mapping information to NHibernate Configuration
	stream.Close();

(1)

With this setting, NHibernate will convert the name of the property in the camel case and will add an underscore before to get the name of the field that hold this data. Read the documentation to discover other naming strategies. If the field is private, it will use the reflection (so make sure that the application's security level allows this).

(2)

Here, NHibernate.Mapping.Attributes uses the .NET reflection to find all classes (in the ExecutingAssembly) with the attribute [Class] and fill the stream with information about these classes.

(3)

Once the stream is filled, we rewind it and send it to the NHibernate Configuration instance. It will parse its XML content to gather all the information NHibernate will need to manipulate your classes.

Now, we use a wonderful tool: SchemaExport:

new NHibernate.Tool.hbm2ddl.SchemaExport(cfg).Create(true, true);

Here, we create an instance of SchemaExport and we directly call the method Create(). It uses all the provided information (in the configuration instance) to generate and run a script that will create the tables and relationships in the database to hold the mapped classes. Note that it will fail to drop a table if this table has a reference that doesn’t exist in the mapping; in this case, you must drop this table manually.

Finally, we create the SessionFactory: (explanation in the next section)

_sessionFactory = cfg.BuildSessionFactory();

Note thate the _sessionFactory is an instance of the class Shop that will be destroyed only when the Shop instance is destroyed.

Persistence API

Before digging into NHibernate operations, there are some concepts that need to be explained:

The notion of "Entity".  In these articles, an entity is simply a mapped class. It implies that NHibernate is aware of this class and can manipulate it.

The main interface used to manipulate entities is ISession (full name: NHibernate.ISession).

You need one ISessionFactory for each database (which is generally created at the initialization of the application as it is the case here). Its implementation is thread-safe and exception-proof, so that it can live as long as your application is running.

A session is created by an ISessionFactory by calling the method ISessionFactory.OpenSession().

You should use the session-per-request pattern; that is create one session for each user request. This is recommended because a session is not exception-proof (nor thread-safe). If an exception is thrown, you should rollback the transaction and close the session (don’t try to recover it).

The classic usage of a session (and its transaction) is:

	NHibernate.ISession session = null;
	NHibernate.ITransaction transaction = null;
	try
	{
		session = _sessionFactory.OpenSession();
		transaction = session.BeginTransaction();

		// CRUD operations here (with the session)

		transaction.Commit();
	}
	catch
	{
		if(transaction != null)
			transaction.Rollback();
		throw;
	}
	finally
	{
		if(session != null)
			session.Close();
	}

As you can read in this code, we create the session, then its transaction. We use the session to do some CRUD operations; finally, we commit the changes and close the session (rollback if any exception is thrown).

NHibernate transactions behave like ADO.NET transactions. The transaction is tightly coupled with the session. SQL commands are generated and ran only when calling transaction.Commit(). But it is possible to force that by calling session.Flush().

Note that creating the transaction is optional (mainly when you just want to run SELECT operations); in this case, you can write:

	using(NHibernate.ISession session = _sessionFactory.OpenSession())
	{
		// Retrieve data here (with the session)
	}

using() will automatically close the session.

An entity has a state which can change: The state you implicitly know is transient. When you create an instance of a class, this instance is transient.

Once you send this instance to a session (to save/update it...), this instance becomes persisted. At this level, the session has this instance in its cache.

If you delete this instance, it is obviously not destroyed from the memory; the session will delete its row in the database and will remove it from its cache (which means that it becomes transient again). Note that an entity can not be attached to two opened sessions at the same time.

Finally, when an instance is persisted and the session which persisted it is closed, this instance became detached. The difference with "transient" is that there might be some useful information that are hidden in this instance and can be used by another session.

CRUD operations

Now, we will use the session to do some basic CRUD operations (that is Create, Retrieve, Update and Delete).

For each operation, a method will be shown and explained.

CREATE: Save

Implementation of the method Shop.GenerateRandomOrders(): Create "n" random orders and save them.

	public void GenerateRandomOrders(int n)
	{
		NHibernate.ISession session = null;
		NHibernate.ITransaction transaction = null;
	
		System.Console.Out.WriteLine("\nSaving " + n + " aleatory orders...");
		try
		{
			session = _sessionFactory.OpenSession();
			transaction = session.BeginTransaction();
	
			for(int i=0; i<n; i++)
			{
				Order o = new Order();
	
				o.Product = "P" + (i+1).ToString();
				o.Quantity = n - i;
				o.ComputeTotalPrice(i * 10 + n);
	
				session.Save(o);
			}
	
			// Commit modifications (Build and execute queries)
			transaction.Commit();
		}
		catch
		{
			if(transaction != null)
				transaction.Rollback(); // Error: we MUST roll back modifications
			throw; // Here, we throw the same exception so that it is handled (printed)
		}
		finally
		{
			if(session != null)
				session.Close();
		}
	}

In this method, orders are randomly created and filled. Then, the method session.Save() is used to save them.

RETRIEVE: Query and Load/Get

To retrieve entities, NHibernate has two query APIs:

  • The Hibernate Query Language (HQL) is a language similar to SQL but "object-oriented".

  • The Criteria API: Use classes to express your query.

These APIs return as result an IList which contains all the items matching the criteria.

When you need to load an entity (knowing its identifier), you can either use session.Load() or session.Get(); send the type and the identifier of the entity to load it.

session.Load() throws an exception if there is no entity in the database with this identifier and session.Get() simply returns null in this case. The session runs a query to retrieve the row, builds an instance, fills and returns it.

Implementation of the method Shop.WriteAllOrders(): For each order (in the database), write the identifier, the date and the product name.

	public void WriteAllOrders()
	{
		using(NHibernate.ISession session = _sessionFactory.OpenSession())
		{
			System.Collections.IList result = session.Find("select o.Id, o.Date, o.Product from Order o");
	
			System.Console.Out.WriteLine("\n" + result.Count + " orders found!");
			foreach(System.Collections.IList l in result)
				System.Console.Out.WriteLine("  Order N°"
					+ l[0] + ",  Date=" + ((System.DateTime)l[1]).ToString("u")
					+ ",  Product=" + l[2]);
		} // finally { session.Close(); }	is done by using()
	}

As you can see, there is no transaction here (needless) and using() will close the session at the end. The HQL query ran here is "select o.Id, o.Date, o.Product from Order o". This query is identical to what can be written in SQL: We ask the list of some items (the Id, Date and Product) in Order's table. These items are stored in an IList which means that we get an IList containing ILists.

Implementation of the method Shop.LoadOrder(): Return the order with the specified identifier.

	public Order LoadOrder(int id)
	{
		System.Console.Out.WriteLine("\nLoading order N° " + id + "...");
		using(NHibernate.ISession session = _sessionFactory.OpenSession())
			return session.Load(typeof(Order), id) as Order;
		// finally { session.Close(); }	is done by using()
	}

This method uses session.Load() because we know that it would be exceptional that the entity to load doesn’t exist.

UPDATE: SaveOrUpdate / Save / Update

Implementation of the method Shop.Save(): Save or Update the order (in the database).

	public void Save(Order o)
	{
		NHibernate.ISession session = null;
		NHibernate.ITransaction transaction = null;

		// That's how the Session decide to save or to update; set NHMA.Id(UnsavedValue=x) to replace 0
		System.Console.Out.Write("\n"  +  (o.Id == 0  ?  "Save"  :  "Update"));
		System.Console.Out.WriteLine(" the order N° " + o.Id + "...");
		try
		{
			session = _sessionFactory.OpenSession();
			transaction = session.BeginTransaction();

			// NHibernate Session will automatically find out if it has to build an INSERT or an UPDATE
			session.SaveOrUpdate(o);

			// Commit modifications (=> Build and execute queries)
			transaction.Commit();
		}
		catch
		{
			if(transaction != null)
				transaction.Rollback(); // Error => we MUST roll back modifications
			throw; // Here, we throw the same exception so that it is handled (printed)
		}
		finally
		{
			if(session != null)
				session.Close();
		}
	}

This method uses a particular technique to issue INSERT or UPDATE commands: An identifier has an unsaved value which is by default, the value that it takes when it is created (an integer takes the value 0 at its creation). You can set [NHibernate.Mapping.Attributes.Id(UnsavedValue=?)] to replace the unsaved value. So a new entity will have the unsaved value as identifier and the session will know that it needs to insert this entity. Refer to the documentation to see how you can customize this behavior even more. There are still the methods session.Save() and session.Update() if you need them.

Implementation of the method Shop.ChangeTimeZone(): Add 'n' hours to all orders.

	public void ChangeTimeZone(int n)
	{
		NHibernate.ISession session = null;
		NHibernate.ITransaction transaction = null;

		System.Console.Out.WriteLine("\nChange the time zone of all orders: n=" + n + "...");
		try
		{
			session = _sessionFactory.OpenSession();
			transaction = session.BeginTransaction();

			System.Collections.IList commandes = session.CreateCriteria(typeof(Order)).List();
			// same as: session.Find("from Order");
			foreach(Order o in commandes)
				o.ChangeTimeZone(n);
			// It is useless to call Update(), the Session will automatically
			// detect modified entities (as long as it loaded them)
			System.Console.Out.WriteLine(commandes.Count + " updated orders!");

			// Commit modifications (=> Build and execute queries)
			transaction.Commit();
		}
		catch
		{
			if(transaction != null)
				transaction.Rollback(); // Error => we MUST roll back modifications
			throw; // Here, we throw the same exception so that it is handled (printed)
		}
		finally
		{
			if(session != null)
				session.Close();
		}
	}

Here, we load all orders using:

session.CreateCriteria(typeof(Order)).List()

Equivalent to:

session.Find("from Order")

"select" is not needed when you want entities (in simple queries), but you can write it like this:

session.Find("select o from Order o")

Once loaded, these entities are kept in the session cache. This means that you don’t need to save them again. When doing transaction.Commit(); the session will browse its cache to detect changed entities and it will persist them. This feature is called transparent persistence.

DELETE: Delete(HQL) and Delete(Entity)

There are two ways to delete entities: You can either run a HQL query or send an entity for deletion.

With HQL, NHibernate will first load all matching entities (to update its caches...) and then, it will delete them (and delete linked entities by cascade if needed). If you already have an entity, you can call session.Delete(entity).

Implementation of the method Shop.Delete(): Delete the order (in the database).

	public void Delete(int id)
	{
		NHibernate.ISession session = null;
		NHibernate.ITransaction transaction = null;

		System.Console.Out.WriteLine("\nDeleting the order N° " + id + "...");
		try
		{
			session = _sessionFactory.OpenSession();
			transaction = session.BeginTransaction();

			session.Delete("from Order o where o.Id = :Id", id, NHibernate.NHibernateUtil.Int32);

			// Commit modifications (=> Build and execute queries)
			transaction.Commit();
		}
		catch
		{
			if(transaction != null)
				transaction.Rollback(); // Error => we MUST roll back modifications
			throw; // Here, we throw the same exception so that it is handled (printed)
		}
		finally
		{
			if(session != null)
				session.Close();
		}
	}

The new element here is the usage of parameter: an object (the id) and the descriptor of its type (NHibernate.NHibernateUtil.Int32).

You can also write:

session.Delete("from Order o where o.Id = " + id);

But it is not recommended to take this habit. Using parameters prevents some SQL injection attacks and escapes invalid characters. And you can also get slightly better performances.

Conclusion

In this tutorial, we designed a simple shop, we implemented its order class, we configured NHibernate and implemented the Shop's methods (doing CRUD operations).

The most obvious advantage of using an ORM (for beginners) is the simplicity when doing CRUD operations. When you are familiar with the API, you can load, save, update and delete entities without any knowledge of the underlining database; you don’t even need to know SQL. But this knowledge is still important if you want get the best performance.

Another more important advantage is that your entities don’t even know that they are ever persisted; which can be useful in complex applications. Persistence becomes an external service which is not intrusive here (almost).

Finally, you can design your application with OOP in mind and then create a database that can keep your entities.

But NHibernate is also impressive when dealing with legacy database. You can take a classic relational database and map it to your object-oriented entities.

You have to understand that if DataSet is not the perfect answer to every problem, ORM is neither perfect in every situation. Reporting and batch processing are situations where DataSet and DataReader might perform better. And it is probably less brainstorming to use them in straightforward applications.

Now, I suggest you to try to do a Console Application similar to this one without looking at the tutorial/code too often. You may also try new things. After that, you will have a better understanding and memorize more information.

A tool that you should really try is NHibernate Query Analyzer. With it, you can edit NHibernate XML configuration files and mapping (.hbm.xml) files. You can also easily run HQL queries after setting up the configuration. To see it in action, download the flash videos available on its web site.

You can find more sample and articles in the NHibernate Documentation.

And if you want to discuss about this tutorial, use this NHibernate topic. Note that you should only use it for topics directly linked to this tutorial; if you have some problems with your own tests, you should create a new topic (read Chapter 4, More information and support).