The official Fatica Labs Blog! RSS 2.0
# Wednesday, 25 July 2012

I just did some new project at work with heavy and extensive usage of data access over legacy databases, and I tried the DapperDotNet micro or/m instead of NHibernate. I just point before the fact that I've already all the infrastructure to map such a legacy DB in NH by using mapping by code and leveraging a lot of conventios in the DB table/field naming, so the mapping work does not make any difference for me, a part the fact that it is not needed with dapper ( or at least, not needed in the Entity based form ) since you just map the data transfer structures. So what's missing from using NH? Lets see:

  • Inheritance I was a little worried about Dapper leak of support for any kind of inheritance concept, but really I managed to do all the requirement without it, having the best dsl for querying the database did the work.
  • Identity Map We have to keep an eye to the fact the identity map does not exist anymore using a micro/orm. This not just in subsequnt queries in the same section, but when we load associations, expecially when the associated class has a lot of data. For example I had an association with an entity containing a big bounch of xml, if I load that association in a dto, I need to manage myself to load it just when the associated id changes.
  • Lazy Collections using Dapper we have to forget such automatic features, basically there is not such a concept, but I really can live without it.
  • Db Schema Create/Update I really miss that just in unit testing. You have to craft the schema by hand in your unit test. In production in my case I have no control for the schema generation *at all* so it is not a problem anyway, but I guess the NH update / generation is not enough for a real DB deployment. You probably need a DB migration in any case.
  • Linq/Hql In fact I miss LinqToNh. Not absolutely Hql. But we have to consider that a big portion of the impedence an OR/M introduces is caused to the creation of a DSL on top of plain SQL.

Let's consider the pure benefit we have from Dapper:

  • Any kind of optimized SQL is easy to submit.
  • Calling an SP handling In/out parametrs is simple as calling a query
  • Multiple resultset are easy to handle ( The Future<> in NH )
  • Bulk operations are easy too ( you still need real bulk if you realaly want to insert big amount of data )
  • Really noticeable increase in performance, due to smart ADO.NET underlayng access and to the fact we control the SQL roundtrip ourself )

So in my opinion: we probably code a little more in the data access phase, but we have more control, there is no a separate "mapping" part, that can be not so easy to mantain, but it really worth the effort to move definitely in the Micro Orm direction.

Wednesday, 25 July 2012 11:32:20 (GMT Daylight Time, UTC+01:00)  #    Comments [1] - Trackback
C# | NHibernate | ORM

# Thursday, 12 April 2012

There are scenarios in which NHibernate performance decrease even if we do all the effort to correctly use it. This could happen if we need in some circumstances to load a lot of record ( I explicitly use record instead of ‘Entity’ ) from some relational structures, and doing this the OR/M way means overload the session with a lot of entities, that is painful in term of speed. Other cases happens when we need to  write or update something that is not properly represented in the entity model we have, maybe because the model is more “read” oriented. Other cases? I’m not able to grasp all  of course, but I’m sure that you face some if you use an OR/M ( not necessarily NH ) in your daily basis. Using NHibernate an alternative could be using FlushMode=Never in session, but you still have all the OR/M plumbing in the hydrating entity code that negatively impacts the performances. I obtained impressive results in solving such a situation, by using Dapper, a so called single file OR/M. It is a single file that provider some IDbConnection extension methods, those methods works on an already opened connection, so we can use the connection sticked to the NHibernate open session, as here below:

// don't get confused by LinqToNh Query<> this one is the Dapper query
// acting on the CONNECTION :)

session.Connection.Query<MyDto>("select Name=t.Name,Mail=t.Mail from mytable t where t.Valid=@Valid",new{Valid=true});




you obtain back a big recordset of MyDto instances in almost the same time if you wire by hand a DateReader vertical on the dto, with all the error checking.

So why don’t use it always?

Because despite the name Dapper is not an OR/M, it does not keep track of modified entities, it does not help you in paginating results or lazy load the entity graph, neither helps in porting from one SQL dialect to another.

Is this strategy used somewhere else?

You probably find interesting to read this post by Sam Saffron, this solution is used in Stackoverflow.com combined with the LinqToSql OR/M to help when the OR/M performance are not enough.

By my test I experienced a performance increase of 10x in a very hacking situation, but I can’t show the case since it is not public code. Something more scientific about performance is here.

Thursday, 12 April 2012 09:41:12 (GMT Daylight Time, UTC+01:00)  #    Comments [0] - Trackback
CodeProject | Dapper | NHibernate | ORM

# Sunday, 27 November 2011

In this post Ayende talk about when we should use NHibernate and he point that in almost read only scenario other approach can be preferred. I think he forget to mention the fact that even in such a scenario we can leverage a very reliable multi DB abstraction offered by NH that can help us if we think to target different data platforms. In order to me we should say that the point of decision to choose fro NH to another approach is the ability to create an entity model, and an entity model helpful to our objectives. This can also depends on how much we are confortable with the technology. Another interesting extension of the argument is, if we should not  use NH what can we use instead ? Well not for sure EF, since the reason of renounce to NH in a project should be the same to avoid EF. The NOSql solutions works only if we can completely avoid a relational database, and the pure crude ADO.NET is just ugly. An option could be Dapper,  a lightweight OR/M ( not exactly an OR/M, but almost ) that remove all the ugliness of ADO.NET and does not change the performance in comparison on using the manual data access approach. I did not tried it myself, but one of its users is stackoverlow, so this should be by itself a guarantee.

Sunday, 27 November 2011 08:56:37 (GMT Standard Time, UTC+00:00)  #    Comments [0] - Trackback
CodeProject | NHibernate | ORM

# Thursday, 17 November 2011

Even if Linq To NHibernate provider allow us to write query in a strongly type manner, it is sometimes needed to works with property names literally. For example in a RIA application a service can receive a column as a string containing the name of the property to order by. Since Linq to NHibernate is a standard Linq provider, we can leverage a standard dynamic linq parser. This is achieved by using an old code by MS, known as System.Linq.Dynamic. By following the link you will find a download location that point to an almost just a sample project that eventually contains the file Dynamic.cs that contains some extension method allowing to merge literal parts in a type safe linq query.

Let’see an example:

var elist = session.Query<MyEntity>()
              .OrderBy(“Name descending”)
              .Skip(first)
              .Take(count)
              .ToList();

I supposed we have a property called Name on the class MyEntity. The OrderBy taking a string as a parameter is an extension method provided by Dynamic.cs, and in order to have it working you just need to merge the file dynamic.cs in your project and import System.Linq.Dynamic. Of course you will have extension for Where and for other linq operators too.

Thursday, 17 November 2011 13:15:19 (GMT Standard Time, UTC+00:00)  #    Comments [5] - Trackback
CodeProject | NHibernate

# Wednesday, 02 November 2011

Sometimes when we use mapping by code we want to see the generated HBM, just to understand if all is working as expected. I did this in some my old posts by dumping the hbm with an helper method, but I simply miss that the function exists as an extension method. Here below an example:

 var compiled = mapper.CompileMappingForAllExplicitlyAddedEntities();
#if DEBUG
            Trace.WriteLine(compiled.AsString());
#endif
            cfg.AddMapping(compiled);

 

We simply need to include the following using:

using NHibernate.Mapping.ByCode;

Here I use the AsString method that serialize in a string the compiled mapping.
Wednesday, 02 November 2011 14:39:29 (GMT Standard Time, UTC+00:00)  #    Comments [0] - Trackback
NH Mapping By Code | NHibernate

# Thursday, 20 October 2011

Since it is boring having to specify every time the Schema a table belongs to, it would be nice having a conventional strategy to automatically wire up this information. With NHibernate mapping by code functionality this is simple. The strategy I propose is having the last part of the entity namespace being equal with the schema. So for example we have an entity Product in the schema named Production we define the entity as:

MyEntities.Production.Product

Then by using a ConventionModelMapper we can write:

public class MyModelMapper:ConventionModelMapper
    {
        public MyModelMapper()
        {
            this.BeforeMapClass += (modelInspector, type, customizer) =>
                {
                    customizer.Schema(type.Namespace.Split('.').Last());
                };
        }
        
    }

And we achieve to write less line again :)

Thursday, 20 October 2011 12:05:18 (GMT Daylight Time, UTC+01:00)  #    Comments [1] - Trackback
NH Mapping By Code | NHibernate

# Wednesday, 28 September 2011

This is just an example helper class to create a generic IResultTransformer that allow us to convert an NHibernate result into a string[] suitable for generating a CSV. Nothing advanced, just some basic stuff and a fluent interface syntax. Let’see the class:

class CsvTransformer<T>:IResultTransformer
    {
        List<Func<T, string>> mappers = new List<Func<T,string>>();
        Func<object,T> elemSelector = k=>(T)((object[])k)[0];
        public CsvTransformer()
        {
            
        }
        #region IResultTransformer Members

        public System.Collections.IList TransformList(System.Collections.IList collection)
        {
            List<string[]> csv = new List<string[]>();
            foreach (object elem in collection)
            {
                List<string> row = new List<string>();
                foreach (var map in mappers)
                    row.Add(map(elemSelector(elem)));
                csv.Add(row.ToArray());
            }
            return csv;
        }

        public object TransformTuple(object[] tuple, string[] aliases)
        {
            return tuple;
        }

        #endregion
        public CsvTransformer<T> Extract(Func<object, T> elemSelector)
        {
            this.elemSelector = elemSelector;
            return this;
        }
        public CsvTransformer<T> Map(Func<T, string> map)
        {
            mappers.Add(map);
            return this;
        }
    }

 

And an example usage can be:

session.QueryOver<MyEntity>()
                        .TransformUsing(newTransformers.CsvTransformer<MyEntity>()
                        .Map(k=>k.Field1)
                        .Map(k=>k.Field2)
                        .Map(k=>k.Date.HasValue?k.Date.Value.ToShortDateString():"")
                        )
                    .List<string[]>();

We can also customize how to extract data from the NHibernate returned tuple by using the .Extract method.

Easy to use and to embed as a stand alone file in our projects. What about having something similar integrating AutoMapper ?

Wednesday, 28 September 2011 15:47:56 (GMT Daylight Time, UTC+01:00)  #    Comments [0] - Trackback
NHibernate

# Tuesday, 27 September 2011

NHibernate QueryOver is a new API ( from version 3.0 )  that allow to create queries the ICriteria way, but with lambda expressions instead of raw strings. With QueryOver we have the benefit of intellisense and the code is refactoring friendly. At the moment one reference can be found on this blog post. Something not so obvious to do is a query over a portion of a date property, for instance all the entities in a certain month, and so on. Let’s have an example considering this simple entity:

<?xml version="1.0" encoding="utf-8" ?>
<hibernate-mapping xmlns="urn:nhibernate-mapping-2.2"  assembly="MyAssembly" namespace="MyAssembly.MyNamespace">
  <class name="Item" table="Items">
    
    <id name="Id"  type="Int64">
      <generator class="native"/>
    </id>
        
    <property type="AnsiString" name="Code" length="32"></property>
    <property type="DateTime" name="StartDate"></property>
    
  </class>
</hibernate-mapping>

And suppose we want to fetch all the entities having start date in the year 2011, with QueryOver we write:

var result = session.QueryOver<Item>()
                    .Where(
                    Restrictions.Eq(
                    Projections.SqlFunction("year"
                    , NHibernateUtil.Int32
                    , Projections.Property<Item>(item => item.StartDate)
                    ) 
                    ,2011)).List();

 

that yield the following query ( with MSSQL 2005 dialect ):

SELECT
        this_.Id as Id21_0_,
        this_.Code as Code21_0_,
        this_.StartDate as StartDate21_0_
    FROM
        Items this_
    WHERE
        datepart(year, this_.StartDate) = @p0;
    @p0 = 2011 [Type: Int32 (0)]

 

obviously we can select a projection with a portion of the date to, suppose we want to fetch the day of all item in a certain date range, we can write:

var session = NHHelper.Instance.CurrentSession;
               var result = session.QueryOver<Item>()
              .WhereRestrictionOn(k => k.StartDate).IsBetween(dlow)
              .And(dup)
              .Select(
                       Projections.SqlFunction("day"
                                   , NHibernateUtil.Int32
                                   , Projections.Property<Item>(p => p.StartDate)
                                   ))
              .List<int>();

 

obtaining the following query:

SELECT
        datepart(day,
        this_.StartDate) as y0_
    FROM
        Items this_
    WHERE
        this_.StartDate between @p0 and @p1;
    @p0 = 01/09/2011 00:00:00 [Type: DateTime (0)], @p1 = 29/09/2011 00:00:00 [Type: DateTime (0)]

 

These are really common situation when we have to fill-up gui elements, and with these tricks we can write optimal and fast queries.

Addition:

As pointed by Vahid ( Thanks a lot ! ) in the comment, with the latest version ( NH 3.2 ) things are easier since there is a shortcut for dateparts as projection extension, so the first sample became:

session.QueryOver<Item>()
                 .Where(
                    p=>p.StartDate.YearPart()==2011
                   
                    ).List();

 

and the second one:

 

session.QueryOver<Item>()
                .Where(
                   p=>p.StartDate.IsBetween(dlow).And(dup)
                   )
                   .Select(k=>k.StartDate.DayPart() )
                   .List();
surely better, and this is the way to go with newer NH versions, and producing of course the same DB queries.
Tuesday, 27 September 2011 11:48:57 (GMT Daylight Time, UTC+01:00)  #    Comments [2] - Trackback
NHibernate | QueryOver

# Friday, 09 September 2011

This post is an exercise, similar to this and this previous posts about using NHibernate  mapping by code new features present form version 3.2. The source inspiring it is an old post form Ayende, showing a non trivial requirement to map.

Here the DB model:

And the wanted object model:

So there is a lot of comments about DB refactoring needing, or on needing to have the linking entity as a visible entity in the model, but:

  • I like the idea of collapsing the linking entity.
  • I suppose that the DB is untouchable, as frequently happens.

Ayende solves the trouble by the <join/> mapping having an entity spawning two tables, so Address will be represented by joining the Table Address and PeopleAddress.

This can be done very easily in Mapping by code too, lets see how:

 

ModelMapper mapper = new ModelMapper();
            mapper.Class<Person>(m =>
                {
                    m.Id(k => k.Id,g=>g.Generator(Generators.Native));
                    m.Table("People");
                    m.Property(k => k.Name);
                    m.Bag(k => k.Addresses, t => 
                            { 
                                t.Table("PeopleAddresses");
                                t.Key(c=>c.Column("PersonId"));
                                t.Inverse(true);
                                
                            }
                         ,rel=>rel.ManyToMany(many=>many.Column("AddressId"))
                        );
                }

                );

            mapper.Class<Address>(m =>
                {
                    m.Id(k => k.Id, g => g.Generator(Generators.Native));
                    m.Table("Addresses");
                    m.Property(p => p.City);

                    m.Join("PeopleAddresses", z => 
{
z.Property(p => p.IsDefault);
z.Property(p => p.ValidFrom);
z.Property(p => p.ValidTo);
z.Key(k => k.Column("PersonId"));
});

That yield  the following mapping:

<?xml version="1.0" encoding="utf-16"?>
<hibernate-mapping xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" namespace="TestMappingByCode" assembly="TestMappingByCode" xmlns="urn:nhibernate-mapping-2.2">
  <class name="Person" table="People">
    <id name="Id" type="Int32">
      <generator class="native" />
    </id>
    <property name="Name" />
    <bag name="Addresses" table="PeopleAddresses" inverse="true">
      <key column="PersonId" />
      <many-to-many class="Address" column="AddressId" />
    </bag>
  </class>
  <class name="Address" table="Addresses">
    <id name="Id" type="Int32">
      <generator class="native" />
    </id>
    <property name="City" />
    <join table="PeopleAddresses">
      <key column="PersonId" />
      <property name="IsDefault" />
      <property name="ValidFrom" />
      <property name="ValidTo" />
    </join>
  </class>
</hibernate-mapping>

 

Exactly the ones that Ayende proposed. As you can see is pretty straightforward map even a not so common situation.

Friday, 09 September 2011 11:59:53 (GMT Daylight Time, UTC+01:00)  #    Comments [1] - Trackback
NH Mapping By Code | NHibernate | ORM

# Friday, 02 September 2011

In this post we done some effort in automatically generate the mapping based on convention, but we miss a very common one: table names is usually the pluralized entity name. This is usually done by using an inflector. Thanks to Stack Overflow, I found this question about it, and choose that one, that is a single easily embeddable file. So we modify a little our AutoMapper class as below:

void AutoMapper_BeforeMapClass(IModelInspector modelInspector, Type type, IClassAttributesMapper classCustomizer)
       {
           //
           // Create the column name as "c"+EntityName+"Id"
           //
           classCustomizer.Id(k => 
                               { 
                                   k.Generator(Generators.Native); k.Column("c" + type.Name + "Id"); 
                               }
                               );
           classCustomizer.Table(Inflector.Pluralize(type.Name));
        }

 

And this is all, the generated mapping will change as:

<hibernate-mapping xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:x=""
sd="http://www.w3.org/2001/XMLSchema" namespace="MappingByCode" assembly="Mappin
gByCode" xmlns="urn:nhibernate-mapping-2.2">
  <class name="SimpleEntity" table="SimpleEntities">
    <id name="Id" column="cSimpleEntityId" type="Int32">
      <generator class="native" />
    </id>
    <property name="Description">
      <column name="txtSimpleEntityDescr" sql-type="AnsiString" />
    </property>
    <many-to-one name="Referred" column="cReferredId" />
  </class>

Just for better sharing, I published this “laboratory” project here.

Friday, 02 September 2011 14:42:10 (GMT Daylight Time, UTC+01:00)  #    Comments [0] - Trackback
NH Mapping By Code | NHibernate | ORM

# Thursday, 01 September 2011

Since version 3.2.0 NHibernate  has an embedded strategy for mapping by code, that basically comes from Fabio Maulo’s ConfORM. With some reading at this Fabio post,  this other one, and this one too, I wrote my own sample just to see what we can do.

Even if we can use mapping by code to map class by class the entire model, something more interesting can be done by writing some convention-based automatic mapper, that can help us even when we face legacy ( non code first ) databases with some (perverted) naming convention.

We have to consider first the ModelMapper class, this class in the NH mapping by code is the one responsible for driving the mapping generator. It provides a suite of events to intercept the actual generation of each elements in the mapping. By listening these event we can decorate the detail of the single element, for example the Id generator class, the SqlType, the column name, and so on. ModelMapper uses a ModelInspector to get the way we want to map each portion of the entity ( properties, many-to-one, collections ), or if we have a component, or a subclass and so on. We realize our AutoMapper class by deriving a ModelMapper and internally subscribing some events, and passing to it a custom ModelInspector ( we named it AutoModelInspector ).

Let’s start with a very basic model:

ultra simple model

Basically an entity that unidirectionally associates with a referred one. Let’s say we have these example database conventions:

  • Identifier column are named “c”+EntityName+”Id” and are autoincrement
  • Column description are named “txt”+EntityName+”Descr”
  • Column of type string have to be prefixed with “txt”
  • Column of type string have to be AnsiString ( for DDL generation of CHAR instead of NChar )
  • Foreign key column have to be called “c”+ForeignEntityName+”Id”

So let’s see how we wrote the custom model mapper:

class AutoMapper:ModelMapper
    {
        public AutoMapper()
            : base(new AutoModelInspector())
        {
            //subscribe required ebvents for this simple strategy ...
            this.BeforeMapClass += new RootClassMappingHandler(AutoMapper_BeforeMapClass);
            this.BeforeMapProperty += new PropertyMappingHandler(AutoMapper_BeforeMapProperty);
            this.BeforeMapManyToOne += new ManyToOneMappingHandler(AutoMapper_BeforeMapManyToOne);
            //...
            //other events....
        }
        
        void AutoMapper_BeforeMapManyToOne(IModelInspector modelInspector, PropertyPath member, IManyToOneMapper propertyCustomizer)
        {
            //
            // name the column for many to one as
            // "c"+foreignEntityName+"id"
            //
            var pi = member.LocalMember as PropertyInfo;
            if (null != pi)
            {
                propertyCustomizer.Column(k => k.Name("c"+pi.PropertyType.Name+"Id"));
            }
        }

        void AutoMapper_BeforeMapProperty(IModelInspector modelInspector, PropertyPath member, IPropertyMapper propertyCustomizer)
        {
            //
            // Treat description as a special case: "txt"+EntityName+"Descr"
            // but for all property of type string prefix with "txt"
            //
            if (member.LocalMember.Name == "Description")
            {
                propertyCustomizer.Column(k =>
                    {
                        k.Name("txt" + member.GetContainerEntity(modelInspector).Name + "Descr");
                        k.SqlType("AnsiString");
                    }
                    );
            }
            else
            {
                var pi = member.LocalMember as PropertyInfo;
                
                if (null != pi && pi.PropertyType == typeof(string))
                {
                    propertyCustomizer.Column(k =>
                    {
                        k.Name("txt" + member.LocalMember.Name);
                        k.SqlType("AnsiString");
                    }
                   );
                }
            }
        }
       
        void AutoMapper_BeforeMapClass(IModelInspector modelInspector, Type type, IClassAttributesMapper classCustomizer)
        {
            //
            // Create the column name as "c"+EntityName+"Id"
            //
            classCustomizer.Id(k => { k.Generator(Generators.Native); k.Column("c" + type.Name + "Id"); });
        }

        
    }

 

The event handlers apply the convention we said before. As we see we pass a special model inspector in the constructor, that is implemented as below:

class AutoModelInspector:IModelInspector
    {
        #region IModelInspector Members

       
        public IEnumerable<string> GetPropertiesSplits(Type type)
        {
            return new string[0];
        }

        public bool IsAny(System.Reflection.MemberInfo member)
        {
            return false;
        }

        
        public bool IsComponent(Type type)
        {
            return false;
        }

       
        public bool IsEntity(Type type)
        {
            return true;
        }

       
        
        public bool IsManyToOne(System.Reflection.MemberInfo member)
        {
            //property referring other entity is considered many-to-ones...
            var pi = member as PropertyInfo;
            if (null != pi)
            {
                return pi.PropertyType.FullName.IndexOf("MappingByCode") != -1;
            }
            return false;
        }

        public bool IsMemberOfComposedId(System.Reflection.MemberInfo member)
        {
            return false;
        }

        public bool IsMemberOfNaturalId(System.Reflection.MemberInfo member)
        {
            return false;
        }

      
        public bool IsPersistentId(System.Reflection.MemberInfo member)
        {
            return member.Name == "Id";
        }

        public bool IsPersistentProperty(System.Reflection.MemberInfo member)
        {
            return member.Name != "Id";
        }

        public bool IsProperty(System.Reflection.MemberInfo member)
        {
            if (member.Name != "Id") // property named id have to be mapped as keys...
            {
                var pi = member as PropertyInfo;
                if (null != pi)
                {
                    // just simple stading that if a property is an entity we have 
                    // a many-to-one relation type, so property is false
                    if (pi.PropertyType.FullName.IndexOf("MappingByCode") == -1)
                        return true;
                }

            }
            return false;
                
        }

        public bool IsRootEntity(Type type)
        {
            return type.BaseType == typeof(object);
        }

       
        
        public bool IsTablePerClassSplit(Type type, object splitGroupId, System.Reflection.MemberInfo member)
        {
            return false;
        }

       
        public bool IsVersion(System.Reflection.MemberInfo member)
        {
            return false;
        }

        #endregion
    }

 

As we say there is a bounch of IsXXXXX function, that are called for each portion of the class in order to know what to do with it. Our implementation is absolutely incomplete ( not implemented function omitted ), but it feet the simple requirement we stated. Then we can see how we actually realize the mapping:

static void Main(string[] args)
       {
           AutoMapper mapper = new AutoMapper();
          
           //this line simple rely on the fact
           //all and just the entities are exported...
           var map = mapper.CompileMappingFor(Assembly.GetExecutingAssembly().GetExportedTypes());

           //dump the mapping on the console
           XmlSerializer ser = new XmlSerializer(map.GetType());
           ser.Serialize(Console.Out, map);
       }

Simple, isn’t ?

The resulting map, as dumped on the console is:

image

That fulfill the actually simple requirements. So is just a matter of recognize the convention and the exceptions, and let’s go auto-mapping!

Thursday, 01 September 2011 14:58:04 (GMT Daylight Time, UTC+01:00)  #    Comments [2] - Trackback
NHibernate | ORM | NH Mapping By Code

# Friday, 01 April 2011

Just a little post about this argument, since as the author of (t4) Hbm2net and Db2hbm I think I can say my opinion from a critic point of view. As an user I can say that, as an OR/M NHibernate is a great platform, almost any circumstances are treated in deep, and even the infamous “legacy db from hell” are gently handled. So this some of my thought:

ONE -  Mapping is not always monkey coding.

This comes from the experience from Db2hbm. The tool can be great if you ( as me, as everybody at the start/half part of the learning curve ) have to start with a big database, and you don’t want to do all the job by hand. Db2hbm has a strategy called mapping exception, you can use it to customize the mapping, but the product is actually monkey code, and you need to abandon the automatic lifecycle in favor of custom mapping. This is bad in term of code generation, since the difference between a code generator that works almost 90% the time and one that works 100% of the time is a lot more than 10% Smile.

TWO – Writing class is monkey coding. 

This comes from hbm2net. I worked personally on the old version in the NHContrib and produced a version with a T4 template. I never modified the template and always had a working class for my mapping, even with exotic associations. This is really pleasant, since we already did a boring job in writing the hbm, creating the class from it would be just painful. So, in my experience, there is nothing more in the class that is not already said in the hbm.

THREE – Xml is not the devil

There is nothing painful in writing XML if we enable the intellisense for it. It is easier to grab all the mapping details by writing your own mapping, and at soon you have a a good practice, you can leverage some nh mapping sugar, to deal with really powerful mapping constructions. With the meta tag hbm can collect even more information to feed more code generators.

FOUR – Fluent NHibernate

Fluent NHibernate is the ( probably most famous ) way to avoid writing XML with NHibernate. Even of there is an AutoMapping feature, that does avoid wrtite any code, you eventually ends writing such things:

public class CatMap : ClassMap<Cat>
{
  public CatMap()
  {
    Id(x => x.Id);
    Map(x => x.Name)
      .Length(16)
      .Not.Nullable();
    Map(x => x.Sex);
    References(x => x.Mate);
    HasMany(x => x.Kittens);
  }
}
So if it is monking code writing the class for HBM, here you monkey twice by writing two class for say the same thing. 

FIVE – ConfOrm

CodeConform is the newest, and maybe the most innovative no xml mapping strategy. Fabio Maulo inspired himself probably in this post. The main idea in Conform in order to me is the pattern strategy that can help write complex mapping without really having to touch any xml file. The xml layer is avoided even internally, so loading a session factory is sensibly faster.  Since Fabio is the lead developer of NHibernate, some of these concept will probably appear on the next NH version.

Conclusion – What I would like to see

I like code first OR/M. But in real life this happened to me 0 times. So having legacy DB ( well, DB first approach, that is the same ) is quite common for me, and I think for a lot of people. Write a mapping in code is not an option, or does not make sense in order to replace hbm, we eventually fall in duplicate the concepts. ConfOrm by design avoid composite key, that are really common in legacy schemas. We need something to easily grab the pattern the DB designer would mean and easily port it to our NH solution.

Friday, 01 April 2011 22:25:13 (GMT Daylight Time, UTC+01:00)  #    Comments [2] - Trackback
NHibernate

# Wednesday, 22 December 2010

The hbm2net and db2hbm tool are now updated to the latest NH version. The fastest location for download are:

The solution on sourceforge now is updated to VS 2010.

Wednesday, 22 December 2010 10:39:01 (GMT Standard Time, UTC+00:00)  #    Comments [0] - Trackback
db2hbm | hbm2net | NHibernate

# Saturday, 23 October 2010

Note: For visitors of your site, this entry is only displayed for users with the preselected language Romanian/română (ro)

Continuare la partea a 5-a

În această parte ne vom opri puţin să analizăm mai bine cum şi când se activează sesiunea (ISession) şi care sunt modalităţile cele mai corecte pentru a o face. Să vedem în mare ce face Session:

  1. Permite recuperarea, salvarea şi eliminarea entităţilor.
  2. Menţine un mapp al entităţilor organizat ca reference/ID Database
  3. Administrează un cache ( first level cache ) care, prin mapp-ul de mai sus, evită să meargă la baza de date pentru a recupera o entitate pe care o avem deja in memorie
  4. Ţine evidenţa entităţilor modificate
  5. Administrează interacţiunile cu driver-ul ADO.NET , inclusiv deschiderea închiderea conexiunilor.
  6. Ne permite să închidem şi să deschidem tranzacţiile.

Cel de-al doilea punct merită un comentariu: între diferenţele conceptuale dintre baza de date şi obiecte .Net există identitatea obiectului. În baza de date relaţia de identitate este după o ultimă analiză garantată de ID-ul rândului, în timp ce în .NET identitatea a două obiecte este după o ultimă analiză raportată la confruntarea dintre două reference. NHibernate vrea să evite existenţa acestor ambiguităţi şi susţine faptul că trebuie să existe o singură reference care să mizeze la un determinat obiect persistent: chiar dacă creăm un obiect clonă al unui obiect persistent şi incercăm, de exemplu să-l eliminăm sau să-l modificăm, această clonă nu va fi acceptată de NHibernate.

Practic când avem nevoie să interacţionăm cu obiectele persistent, avem necesitatea de a lucra în sfera unei sesiuni. Acest lucru introduce adesea nişte erori când căutăm momentul oportun pentru a deschide sesiunea:

 

Open Session: De evitat

1. De evitat deschiderea unei sesiuni pentru fiecare interacţiune pe care vrem să o avem cu stratul persistenţei ( Session-per-operation Antipattern )

2. De evitat utilizarea unei singure sesiuni pe întreaga durată a aplicaţiei. ( Session-per-application Antipattern ) Am mai spus acest lucru, dar e bine de amintit având în vedere că vorbim despre lucruri de evitat: dacă sesiunea are o anumită agilitate de uz, Session Factory trebuie să fie deschisă o singură dată în interiorul unui app domain. Nu pentru că ar exista ceva ce nu funcționează, dar pur şi simplu sesiunea factory se încarcă lent.

Dacă folosim prima strategie, ne lovim puternic de faptul că NH nu vrea să "mixeze" entităţi ce aparţin mai multor sesiuni, se pierde orice fel de beneficiu al cach-ului de prim nivel, iar obiectele lazy tind să producă cunoscuta eroare “NHibernate.LazyInitializationException: illegal access to loading collection”, datorită faptului că sesiunea care a creat obiectul a fost deja închisă, iar NH nu poate să meargă să recupereze obiectele colecţiei.

Cea de-a doua strategie este mai complicată: lucrăm cu date în "repaus", adică date ce probabil nu sunt aduse la zi cu respectivele obiecte salvate, sesiunea se îngreunează (NH devine din ce în ce mai lent dacă obiectele tratate de o sesiune sunt prea multe), nu se mai asigură o politică corectă de administrare a conexiunii la BD, care trebuie să fie deschisă doar pe timpul necesar, şi deci închisă.

Soluţia recomandată este Session per Business Transaction. O busuness transaction este, pentru a face un exemplu: „citesc prima pagină a clienţilor", "Introduc o nouă comandă", "Caut toate produsele ce încep cu XXX în depozit", etc. Deci operaţiuni autoincluse, dar nu triviale. În aplicaţii pot exista uşoare diferenţe între rich client şi Web application, în aceste din urmă realizarea acestui pattern poate converge în Session Per Request, ce constă în deschiderea unei sesiuni când se verifică evenimentul Begin request, şi închiderea acesteia la următorul End. Acest gen de contact, în general este aproape echivalent cu Session Per bussines transaction, pentru că de fapt, într-o aplicaţie web un ciclu request/response coincide cu o bussines transaction.

Putem folosi în acest scop patterm-ul Unit of Work. În sursele de exemplu am pus o versiune simplă, care utilizează ContextSession ale NH. Practic aceste Context session ne permit să împărtăşim o sesiune între mai multe funcţii ale aplicaţiei noastre, în realaţie cu un context ce poate fi, de exemplu Thread-ul curent, sau HttpContext-ul apelului la web server, sau oricare alt lucru, din punctul de vedere al configurării. Ca să ne fie mai clar, să vedem sursele:

 

 

   1: public class NHUow:IDisposable
   2: {
   3:     private NHUow()
   4:     {
   5:         if (CurrentSessionContext.HasBind(sessionFactory))
   6:             throw new InvalidOperationException("Nested unit of work not allowed.");
   7:         CurrentSessionContext.Bind(sessionFactory.OpenSession());
   8:     }
   9:     
  10:     static object lockObject = new object();
  11:     public static NHUow Open()
  12:     {
  13:         lock (lockObject)
  14:         {
  15:             if (sessionFactory == null)
  16:                 CreateSessionFactory();
  17:         }
  18:         return new NHUow();
  19:     }
  20:     public static ISession CurrentSession
  21:     {
  22:         get { return sessionFactory.GetCurrentSession(); }
  23:     }
  24:  
  25:     private static ISessionFactory sessionFactory;
  26:     private static void CreateSessionFactory()
  27:     {
  28:         Configuration cfg = CreateConfiguration();
  29:         sessionFactory = cfg.BuildSessionFactory();
  30:     }
  31:  
  32:     private static Configuration CreateConfiguration()
  33:     {
  34:         Configuration cfg = new Configuration();
  35:         cfg.Configure();
  36:         // implicitamente carichiamo tutti i mapping che si trovano nell'assembly che
  37:         // contiene customer
  38:         cfg.AddAssembly(typeof(Customer).Assembly);
  39:         return cfg;
  40:     }
  41:  
  42:  
  43:     #region IDisposable Members
  44:  
  45:     public void Dispose()
  46:     {
  47:         if (!CurrentSessionContext.HasBind(sessionFactory) )
  48:         {
  49:             throw new InvalidOperationException("Invalid current session");
  50:         }
  51:         var session = sessionFactory.GetCurrentSession();
  52:         CurrentSessionContext.Unbind(sessionFactory);
  53:         if (!session.IsOpen)
  54:         {
  55:             throw new InvalidOperationException("Session closed before UOW end");
  56:         }
  57:        
  58:         if ( null != session.Transaction && session.Transaction.IsActive )
  59:         {
  60:             if (session.Transaction.WasCommitted == false && session.Transaction.WasRolledBack == false)
  61:                 session.Transaction.Rollback();
  62:         }
  63:         session.Close();
  64:     }
  65:  
  66:     #endregion
  67: }

 

Obiectul implementează IDisposable, şi ne permite să-l utilizăm în acest fel:

 

   1: using (var uow = NHUow.Open())
   2: {
   3:     var transaction = NHUow.CurrentSession.BeginTransaction();
   4:     Customer c = new Customer();
   5:     c.Name = "FELIX";
   6:     c.AddressLine1 = c.AddressLine2 = "";
   7:     c.City = "XX";
   8:     c.ZipCode = "12060";
   9:     NHUow.CurrentSession.Save(c);
  10:     transaction.Commit();
  11: }

Practic deschid o unitate de lucru cu using new… şi, odată ieşiţi din scope, uow se închide. În interiorul acestui scope, întregul nostru cod va putea accesa sesiunea curentă folosind funcţia statică NHUow.CurrentSession. Cei mai atenţi vor observa faptul că configurarea sesiunii factory este fixă:acest lucru se întâmplă pentru a simplifica clasa UOW, pentru a o face să devină generală este de ajuns să intervenim adăugând o Session FactoryFactory, pe care UOW o va utiliza pentru a recupera Session factory - ul extern.

Dacă în acest moment am lansa unit testul de probă, am obţine următoarea excepţie:

No CurrentSessionContext configured (set the property current_session_context_class)!

Asta se întâmplă deoarece mecanismul de întreţinere al sesiunilor de context al NH este configurabil, iar noi nu am specificat nimic. După cum spune mesajul de eroare, trebuie să adăugăm în fişierul de config rândul current_session_context_class ( rândul 11 ):

   1: <hibernate-configuration xmlns="urn:nhibernate-configuration-2.2">
   2:   <session-factory name="NHibernate.Test">
   3:     <property name="connection.driver_class">NHibernate.Driver.SqlClientDriver</property>
   4:     <property name="connection.connection_string">Server=.\SQLEXPRESS;initial catalog=NHFROMSCRATCH;Integrated Security=SSPI</property>
   5:     <property name="adonet.batch_size">10</property>
   6:     <property name="show_sql">true</property>
   7:     <property name="dialect">NHibernate.Dialect.MsSql2005Dialect</property>
   8:     <property name="use_outer_join">true</property>
   9:     <property name="command_timeout">60</property>
  10:     <property name="query.substitutions">true 1, false 0, yes 'Y', no 'N'</property>
  11:     <property name="current_session_context_class">thread_static</property>
  12:     <property name="proxyfactory.factory_class">NHibernate.ByteCode.LinFu.ProxyFactoryFactory, NHibernate.ByteCode.LinFu</property>
  13:   </session-factory>
  14: </hibernate-configuration>

Observaţi că nu am introdus un nume al clasei, asta pentru că există anumite shortcut pentru cei mai folosiţi context manager. Enumerăm posibili context manager:

· NHibernate.Context.WebSessionContext – Sesiunea curentă este inclusă în HttpContext. Alias web.

· NHibernate.Context.CallSessionContext - Sesiunea curentă este inclusă în CallContext. Alias call.

· NHibernate.Context.ThreadStaticSessionContext – Sesiunea curentă este în context –ul thread-ului. Alias thread_static.

Desigur, tot cu filosofia maximei extensibilităţi, putem scrie context manager-ul nostru.

In sursele de exemplu, pe lângă clasa UOW, există o clasă a Unit Test-ului NHFromScratch.Tests.Uow , care arată cum se foloseşte.

 

Mergi la partea  5
Descarca Sursa
Saturday, 23 October 2010 18:47:18 (GMT Daylight Time, UTC+01:00)  #    Comments [0] - Trackback
hbm2net | NHibernate | NHibernate Tutorial

# Saturday, 16 October 2010
Continua da parte 5

In questa parte ci fermiamo un attimo ad analizzare meglio come e quando attivare la sessione (ISession) e quali siano le modalità più corrette per farlo. Vediamo per grandi linee cosa fa Session:

  1. Consente di recuperare salvare ed eliminare le entità.
  2. Mantiene una mappa delle entità organizzata come reference/ID Database
  3. Gestisce una cache ( first level cache ) che, tramite la mappa di cui sopra, ci evita di andare sul database per recuperare un entità che abbiamo già in memoria
  4. Tiene traccia delle entità modificate
  5. Gestisce le interazioni con il driver ADO.NET sottostante, comprese l’apertura chiusura delle connessioni.
  6. Ci consente di aprire e chiudere le transazioni.

Il punto 2 merita un commento: tra le difformità concettuali tra database e oggetti .NET vi è l’identità dell’oggetto. Nel database la relazione di identità è in ultima analisi garantita dall’ID della riga, mentre in .NET l’identità di due oggetti è in ultima analisi riconducibile al confronto fra due reference. NHibernate vuole evitare che ci siano queste ambiguità, e asserisce che ci debba essere una sola reference che punti ad un determinato oggetto persistent: anche se creiamo un oggetto clone di un oggetto persistent e cerchiamo, per esempio, di cancellarlo o di modificarlo, questo clone non sarà accettato da NHIbernate.

In pratica, quando ci serve di interagire con gli oggetti persistent, abbiamo necessita di lavorare nell’ambito di una sessione. Questo introduce spesso degli errori quando cerchiamo il momento opportuno per aprire la sessione:

Open Session: Pratiche da evitare
  1. Evitare di aprire la session per ogni singola interazione che si vuole avere con lo strato di persistenza. ( Session-per-operation Antipattern )
  2. Evitare di utilizzare una sola session per l’intera durata dell’ applicazione. ( Session-per-application Antipattern )

Già lo abbiamo detto, ma è meglio ricordarlo visto che siamo in zona di cose da evitare: se la session ha comunque una certa agilità di uso, la SessionFacory deve essere aperta una volta sola all’interno di un app domain. Non che ci sia qualcosa che non funziona, ma semplicemente la session factory è lenta a caricarsi.

Se si usa la prima strategia, ci si scontra violentemente con il fatto che NH non vuole “mixare” entità appartenenti a più sessioni,  si perde qualsiasi beneficio della cache di primo livello, e gli oggetti lazy tendono a produrre il famigerato errore “NHibernate.LazyInitializationException: illegal access to loading collection”, dovuto al fatto che la sessione che ha creato l’ogegtto ormai l’abbiamo chiusa, e NH non può andare a fare il recupero degli oggetti della collection.

La seconda strategia se possibile è anche peggiore: lavoriamo con dati in “stallo”, cioè possibilmente non aggiornati con i rispettivi oggetti persistiti, la session si appesantisce ( NH diventa sempre più lento se gli oggetti trattati da una session sono troppi ), non si assicura più una corretta politica di gestione della connessione al DB,che deve essere aperta  per il tempo strettamente necessario e quindi chiusa.

La soluzione consigliata è la Session per Business Transaction. Una business transaction è, tanto per fare un esempio: “leggo  la prima pagina dei clienti”, “Inserisco un nuovo ordine”, “cerco tutti i prodotti in magazzino che iniziano per XXX”, ecc. Quindi operazioni autocontenute, ma non triviali. Nelle applicazioni ci possono essere delle leggere differenze tra rich client e Web application, in queste ultime la realizzazione di questo pattern può convergere nella Session Per Request, che consiste nell’aprire una session quando si verifica l’evento Begin request, e di chiuderla al succesivo End. Questo approccio in generale è pressoche equivalente al Session Per business transaction, perchè di fatto in un applicativo web un ciclo request/response coincide con una business transaction.

Possiamo utilizzare per questo scopo il pattern Unit of Work. Nei sorgenti di esempio ne ho messo una versione semplice, che utilizza le ContextSession di NH. In pratica le context session ci consentono di condividere una sessione tra più funzioni del nostro applicativo, in relazione ad un contesto che può essere, per esempio il Thread corrente, oppure l’HttpContext della chiamata al web server, o qualunque altra cosa, configurabilmente. Vediamo i sorgenti che così è più chiaro:

   1: public class NHUow:IDisposable
   2: {
   3:     private NHUow()
   4:     {
   5:         if (CurrentSessionContext.HasBind(sessionFactory))
   6:             throw new InvalidOperationException("Nested unit of work not allowed.");
   7:         CurrentSessionContext.Bind(sessionFactory.OpenSession());
   8:     }
   9:     
  10:     static object lockObject = new object();
  11:     public static NHUow Open()
  12:     {
  13:         lock (lockObject)
  14:         {
  15:             if (sessionFactory == null)
  16:                 CreateSessionFactory();
  17:         }
  18:         return new NHUow();
  19:     }
  20:     public static ISession CurrentSession
  21:     {
  22:         get { return sessionFactory.GetCurrentSession(); }
  23:     }
  24:  
  25:     private static ISessionFactory sessionFactory;
  26:     private static void CreateSessionFactory()
  27:     {
  28:         Configuration cfg = CreateConfiguration();
  29:         sessionFactory = cfg.BuildSessionFactory();
  30:     }
  31:  
  32:     private static Configuration CreateConfiguration()
  33:     {
  34:         Configuration cfg = new Configuration();
  35:         cfg.Configure();
  36:         // implicitamente carichiamo tutti i mapping che si trovano nell'assembly che
  37:         // contiene customer
  38:         cfg.AddAssembly(typeof(Customer).Assembly);
  39:         return cfg;
  40:     }
  41:  
  42:  
  43:     #region IDisposable Members
  44:  
  45:     public void Dispose()
  46:     {
  47:         if (!CurrentSessionContext.HasBind(sessionFactory) )
  48:         {
  49:             throw new InvalidOperationException("Invalid current session");
  50:         }
  51:         var session = sessionFactory.GetCurrentSession();
  52:         CurrentSessionContext.Unbind(sessionFactory);
  53:         if (!session.IsOpen)
  54:         {
  55:             throw new InvalidOperationException("Session closed before UOW end");
  56:         }
  57:        
  58:         if ( null != session.Transaction && session.Transaction.IsActive )
  59:         {
  60:             if (session.Transaction.WasCommitted == false && session.Transaction.WasRolledBack == false)
  61:                 session.Transaction.Rollback();
  62:         }
  63:         session.Close();
  64:     }
  65:  
  66:     #endregion
  67: }

L’oggetto implementa IDisposable, e ci permette di utilizzaro in questo modo:

   1: using (var uow = NHUow.Open())
   2: {
   3:     var transaction = NHUow.CurrentSession.BeginTransaction();
   4:     Customer c = new Customer();
   5:     c.Name = "FELIX";
   6:     c.AddressLine1 = c.AddressLine2 = "";
   7:     c.City = "XX";
   8:     c.ZipCode = "12060";
   9:     NHUow.CurrentSession.Save(c);
  10:     transaction.Commit();
  11: }

In pratica apro una unità di lavoro con using new… e, usciti dallo scope la uow si chiude. All’interno di questo scope, tutto il nostro codice potrà accedere alla session corrente usango la funzione statica NHUow.CurrentSession. I più attenti noteranno che la configurazione del session factory è fissa: questo è per semplificare la clase UOW, per renderla generale basta intervenire aggiungendo un SessionFactoryFactory, che UOW utilizzera per recuperare il session factory esterno.

Bene, se a questo punto lanciassimo lo unit test di prova, otterremmo la seguente eccezione:

No CurrentSessionContext configured (set the property current_session_context_class)!

Questo perchè il meccanismo di mantenimento delle sessioni di contesto di NH è configurabile, e noi non abbiamo specificato nulla. Come dice il messaggio di errore, bisogna aggiungere nel file di config la linea current_session_context_class ( linea 11 ):

   1: <hibernate-configuration xmlns="urn:nhibernate-configuration-2.2">
   2:   <session-factory name="NHibernate.Test">
   3:     <property name="connection.driver_class">NHibernate.Driver.SqlClientDriver</property>
   4:     <property name="connection.connection_string">Server=.\SQLEXPRESS;initial catalog=NHFROMSCRATCH;Integrated Security=SSPI</property>
   5:     <property name="adonet.batch_size">10</property>
   6:     <property name="show_sql">true</property>
   7:     <property name="dialect">NHibernate.Dialect.MsSql2005Dialect</property>
   8:     <property name="use_outer_join">true</property>
   9:     <property name="command_timeout">60</property>
  10:     <property name="query.substitutions">true 1, false 0, yes 'Y', no 'N'</property>
  11:     <property name="current_session_context_class">thread_static</property>
  12:     <property name="proxyfactory.factory_class">NHibernate.ByteCode.LinFu.ProxyFactoryFactory, NHibernate.ByteCode.LinFu</property>
  13:   </session-factory>
  14: </hibernate-configuration>

Notate che non ho inserito un nome di classe, questo perchè ci sono degli shortcut per i context manager più usati. Tanto per elecare i vari possibili context manager:

  • NHibernate.Context.WebSessionContext – la sessione corrente è contenuta in HttpContext. L’alias è web.
  • NHibernate.Context.CallSessionContext - la sessione corrente è contenuta CallContext. L’alias è call.
  • NHibernate.Context.ThreadStaticSessionContext – la sessione corrente è nel context del thread. L’alias è thread_static.

Ovviamente, sempre nella filosofia della massima estendibilità, possiamo scrivere il nostro context manager.

Nei sorgenti di esempio, oltre alla classe Uow, c’è una classe di Unit Test NHFromScratch.Tests.Uow, che ne mostra l’utilizzo.

 

Torna alla parte 5
Download Sorgenti
Saturday, 16 October 2010 17:48:51 (GMT Daylight Time, UTC+01:00)  #    Comments [0] - Trackback
hbm2net | NHibernate | NHibernate Tutorial

# Wednesday, 29 September 2010

Note: For visitors of your site, this entry is only displayed for users with the preselected language Romanian/română (ro)

Continuare la partea a 4-a

În această parte vom vedea un alt tip de colecţie, şi cum, în general putem colecţiona obiecte ce nu sunt entităţi, adică obiecte care nu administrează pe cont propriu o identitate, şi a căror existenţă are sens doar dacă sunt conectate la entitatea cuprinzătoare. Pentru a face asta adăugăm entitatea Produs la modelul nostru, de care vom avea oricum nevoie pentru alte lucruri, şi presupunem că avem un simplu depozit, organizat pe celule, ce conţin stocul produsului în chestiune. Să mai spunem că pentru a identifica celula ne este de ajuns un singur şir. În realitate un depozit va fi mult mai complex, dar această simplificare este un pretext pentru a arăta strategia de colecţie MAP, iar într-o realitate foarte redusă ar putea chiar să aibă un sens aşa cum e făcut.

 

Să adăugăm deci mapping-ul entităţii Produs, cu un map al locaţiilor depozitului şi relativele cantităţi anexat:

   1: <?xml version="1.0" encoding="utf-8" ?>
   2: <hibernate-mapping xmlns="urn:nhibernate-mapping-2.2" namespace="NHFromScratch.Entities" 
   3:                                                       assembly="NHFromScratch.Entities">
   4:   <class name="Product" table="Products">
   5:     <id name="Id" type="Int32">
   6:       <generator class="native"/>
   7:     </id>
   8:     <property name="Code" type="AnsiString" />
   9:     <property name="Description" type="AnsiString"/>
  10:     <map name="Warehouse">
  11:       <key column="Product"/>
  12:       <index column="Cell" type="AnsiString"/>
  13:       <element column="Quantity" type="Int32"></element>  
  14:     </map>
  15:     <!-- details comes here -->
  16:   </class>
  17:   
  18: </hibernate-mapping>
  19:  

Să vedem cum este declarat <map/>, sau mai bine spus, un mod pentru a declara un map, în acest caz un map indexat cu un şir ce identifică celula depozitului, şi cu un număr care indică cantitatea produsului prezent în această celulă. În tabela ce conţine map-ul, Tag Key este referinţa la entitatea mamă (în acest caz Product), apoi avem nevoie de un indice, în acest caz este „Cell”-ul depozitului, iar valoarea, în acest caz este Quantity a produsului. Lansând unit test-ul de creare a bazei de date obţinem, pentru partea care ne interesează, următoarea schemă:

 

 

image

 

Dacă mergem să examinăm mai bine DDL-ul observăm, în partea de creare a tabelei warehouse, următorul script:

 

clip_image001

Cheia primară a tabelei este deci constituită din FK-ul tabelei parent(Product) combinată cu componenta „Index” - în acest caz Cell -. Nu am putea avea, pentru un produs, două informaţii relative aceleiaşi celule, sau locaţii de depozit.

 

Să presupunem că pentru a localiza poziţia în depozit sunt necesare, însă, două informaţii, de exemplu rând şi coloană, şi că pe lângă cantitate ne interesează să ştim data la care această cantitate a fost stocată; putem modifica mapping-ul după cum urmează:

   1: <?xml version="1.0" encoding="utf-8" ?>
   2: <hibernate-mapping xmlns="urn:nhibernate-mapping-2.2" namespace="NHFromScratch.Entities" 
   3:                                                       assembly="NHFromScratch.Entities">
   4:   <class name="Product" table="Products">
   5:     <id name="Id" type="Int32">
   6:       <generator class="native"/>
   7:     </id>
   8:     <property name="Code" type="AnsiString" />
   9:     <property name="Description" type="AnsiString"/>
  10:     <map name="Warehouse">
  11:       <key column="Product"/>
  12:       <composite-index class ="WarehouseLocation">
  13:         <key-property name="CellRow" type="AnsiString" />
  14:         <key-property name="CellCol" type="AnsiString" />
  15:       </composite-index>
  16:       <composite-element class="StockInformation">
  17:         <property name="StockDate" type="DateTime"/>
  18:         <property name="Quantity" type="Int32"/>
  19:       </composite-element>
  20:     </map>
  21:     <!-- details comes here -->
  22:   </class>
  23:   
  24: </hibernate-mapping>

Practic am înlocuit “<index>” cu “<composite-index>” şi “<element>” cu “<composite-element>”. Desigur putem avea şi scenarii în care doar indicele este compozit, sau doar elementul. Imediat ce lansăm compilarea hbm2net generează câteva fişiere în plus:

 

 

clip_image001[5]

 

Practic avem o clasă pentru a reprezenta „cheia” ( WarehouseLocation ) în map şi una pentru a reprezenta „elementul” ( StockInformation ). Clasa utilizată pentru cheie trebuie să poată acţiona ca atare, şi deci trebuie să efectueze oportune ovveride -uri ale metodelor Equals si  GetHashCode; suferă comportamente imprevizibile ale codului. Utilizând hbm2net acesta generează codul oportun:

   1: public override bool Equals(object other)
   2: {
   3:     if (this == other)
   4:         return true;
   5:     WarehouseLocation rhs = other as WarehouseLocation;
   6:     if (rhs == null) 
   7:         return false; // null or not a WarehouseLocation
   8:     return _cellRow.Equals(rhs._cellRow) && _cellCol.Equals(rhs._cellCol);
   9: }
  10:  
  11: public override int GetHashCode()
  12: {
  13:     return _cellRow.GetHashCode() ^ _cellCol.GetHashCode();
  14: }

Adăugând aceste două clase la proiect putem regenera baza de date, care rezultă modificată astfel:

 

clip_image001[7]

Diagrama clase devine, pentru partea product, următoarea:

clip_image001[9] 

 

Este interesant să observăm că din punctul de vedere OR/M StockInformation si WarehouseInformation *nu* sunt entităţi. În această parte a cursului s-a dezminţit deci presupusa corespondenţă 1:1 între entităţi şi tabele.

 

Acum să scriem unit test-ul obişnuit pentru a vedea cum se comportă NH-ul în timpul CRUD-ului în această collection. Nu ne întoarcem asupra subiectului „lazy” întrucât analog partii a 4-a pentru <bag/>.

 

 

Codul unit test-ului pe care-l vom lansa este următorul:

   1: [Test]
   2: public void TestCrud()
   3: {
   4:    Console.WriteLine("Creating a product");
   5:    using (ISession session = sessionFactory.OpenSession())
   6:    using (ITransaction trns = session.BeginTransaction())
   7:    {
   8:        Product p = new Product();
   9:        p.Code = "P00000";
  10:        p.Description = " a sample product";
  11:        Console.WriteLine("Adding some Warehouse information");
  12:        var l1 = new WarehouseLocation() { CellCol = "A", CellRow = "1" };
  13:        p.Warehouse.Add(new KeyValuePair<WarehouseLocation, StockInformation>(l1, new StockInformation()));
  14:        p.Warehouse[l1].Quantity = 12;
  15:        p.Warehouse[l1].StockDate = DateTime.Now;
  16:        var l2 = new WarehouseLocation() { CellCol = "A", CellRow = "2" };
  17:        p.Warehouse.Add(new KeyValuePair<WarehouseLocation, StockInformation>(l2, new StockInformation()));
  18:        p.Warehouse[l2].Quantity = 42;
  19:        p.Warehouse[l2].StockDate = DateTime.Now.AddDays(-7);
  20:        Console.WriteLine("Persisting the Product");
  21:        session.SaveOrUpdate(p);
  22:        trns.Commit();
  23:    }
  24:    Console.WriteLine("Modifying warehouse information of a product");
  25:    using (ISession session = sessionFactory.OpenSession())
  26:    using (ITransaction trns = session.BeginTransaction())
  27:    {
  28:        Console.WriteLine("Retrieving product by code");
  29:        var p = session.CreateCriteria<Product>()
  30:            .Add(Expression.Eq("Code", "P00000"))
  31:            .UniqueResult<Product>();
  32:        
  33:        Console.WriteLine("Modify stored quantity on cell A1");
  34:        var l1 = new WarehouseLocation() { CellCol = "A", CellRow = "1" };
  35:        
  36:        p.Warehouse[l1].Quantity -=3;
  37:        p.Warehouse[l1].StockDate = DateTime.Now;
  38:        
  39:        Console.WriteLine("Persisting the Product");
  40:        session.SaveOrUpdate(p);
  41:        trns.Commit();
  42:    }
  43:    Console.WriteLine("deleting warehouse information of a product");
  44:    using (ISession session = sessionFactory.OpenSession())
  45:    using (ITransaction trns = session.BeginTransaction())
  46:    {
  47:        Console.WriteLine("Retrieving product by code");
  48:        var p = session.CreateCriteria<Product>()
  49:            .Add(Expression.Eq("Code", "P00000"))
  50:            .UniqueResult<Product>();
  51:  
  52:        Console.WriteLine("Modify stored quantity on cell A1");
  53:        var l1 = new WarehouseLocation() { CellCol = "A", CellRow = "1" };
  54:  
  55:        p.Warehouse.Remove(l1);
  56:  
  57:        Console.WriteLine("Persisting the Product");
  58:        session.SaveOrUpdate(p);
  59:        trns.Commit();
  60:    }
  61: }

Care produce următorul rezultat. S-au evidenţiat punctele în care se vede cum NH accesează la elementele colecţiei:

 

image

După cum se vede, map este foarte eficient şi accesează în update/delete elemente individuale ale colecţiei. Contrar acestei afirmaţii, având în vedere faptul că structura este reprezentată în memorie printr-un dictionary, nu este posibilă ordonarea sa (chiar făcând query-ul cu un order-by, funcţionarea internă a dicţionarului ar putea schimba această ordine).

Derscarca sursa
Partea a 4-a
Partea a 6-a
Wednesday, 29 September 2010 21:37:34 (GMT Daylight Time, UTC+01:00)  #    Comments [0] - Trackback
hbm2net | NHibernate | NHibernate Tutorial

# Monday, 27 September 2010
Continua da parte 4

In questa parte vedremo un altro tipo di collezione, e di come in generale possiamo collezionare oggetti che non sono entità, cioè oggetti che non gestiscono per conto proprio una identità, e che ha senso che esistano solo se collegati all’entità contenitrice. Per fare questo aggiungiamo al nostro modello l’entità Prodotto, che ci servirà comunque per altre cose, e supponiamo di avere un semplicissimo magazzino, organizzato a celle, contenenti la giacenza del prodotto in questione. Diciamo anche che per identificare la cella ci basta avere una stringa. Nella realtà un magazzino sarà molto più complesso, ma questa semplificazione è un pretesto per mostrare la strategia di collezione MAP, ed in una realtà molto piccola potrebbe anche avere un senso così come fatto.

Aggiungiamo quindi il mapping della entità Prodotto, con annessa la mappa delle locazioni di magazzino e relative quantità:

   1: <?xml version="1.0" encoding="utf-8" ?>
   2: <hibernate-mapping xmlns="urn:nhibernate-mapping-2.2" namespace="NHFromScratch.Entities" 
   3:                                                       assembly="NHFromScratch.Entities">
   4:   <class name="Product" table="Products">
   5:     <id name="Id" type="Int32">
   6:       <generator class="native"/>
   7:     </id>
   8:     <property name="Code" type="AnsiString" />
   9:     <property name="Description" type="AnsiString"/>
  10:     <map name="Warehouse">
  11:       <key column="Product"/>
  12:       <index column="Cell" type="AnsiString"/>
  13:       <element column="Quantity" type="Int32"></element>  
  14:     </map>
  15:     <!-- details comes here -->
  16:   </class>
  17:   
  18: </hibernate-mapping>
  19:  

Vediamo come è definita <map/>, o meglio, un modo di definire una mappa, in questo caso una mappa indicizzata da una stringa che identifica la cella di magazzino, ed un numerico che indica la quantità del prodotto presente in tale cella. Il tag key è il riferimento, nella tabella che contiene la map, alla entità madre ( in questo caso Product ), poi occorre un indice, in questo caso è la “Cell” del magazzino, e il valore, in questo caso la Quantity del prodotto. Lanciando lo unit test di creazione del database otteniamo, per la parte che ci interessa, il seguente schema:

image

Se andiamo ad indagare meglio la DDL notiamo, nella parte di creazione della tabella warehouse, il seguente script:

clip_image001

La chiave primaria della tabella è quindi costituita dalla FK alla tabella parent ( Product ) combinata con la componente “Index” – in questo caso Cell –. No potremo avere, per un prodotto, due informazioni relative alla medesima cella, o locazione di magazzino.

Supponiamo che per localizzare la posizione in magazzino siano necessarie invece due informazioni, per esempio la riga e la colonna, e che oltre che la quantità ci interessi anche sapere la data a cui questa quantità è stata stoccata, possiamo modificare il mapping come di seguito:

   1: <?xml version="1.0" encoding="utf-8" ?>
   2: <hibernate-mapping xmlns="urn:nhibernate-mapping-2.2" namespace="NHFromScratch.Entities" 
   3:                                                       assembly="NHFromScratch.Entities">
   4:   <class name="Product" table="Products">
   5:     <id name="Id" type="Int32">
   6:       <generator class="native"/>
   7:     </id>
   8:     <property name="Code" type="AnsiString" />
   9:     <property name="Description" type="AnsiString"/>
  10:     <map name="Warehouse">
  11:       <key column="Product"/>
  12:       <composite-index class ="WarehouseLocation">
  13:         <key-property name="CellRow" type="AnsiString" />
  14:         <key-property name="CellCol" type="AnsiString" />
  15:       </composite-index>
  16:       <composite-element class="StockInformation">
  17:         <property name="StockDate" type="DateTime"/>
  18:         <property name="Quantity" type="Int32"/>
  19:       </composite-element>
  20:     </map>
  21:     <!-- details comes here -->
  22:   </class>
  23:   
  24: </hibernate-mapping>

In pratica abbiamo sostituito “<index>” con “<composite-index>” e “<element>” con “<composite-element>”. Ovviamente possiamo avere anche scenari in cui ad essere composito è solo l’indice, o solo l’element. Appena lanciata la compilazione il nostro hbm2net genera qualche file in più:

clip_image001[5]

In pratica abbiamo una classe per rappresentare la “chiave” ( WarehouseLocation ) nella mappa e una per rappresentare l’”element” ( StockInformation ). La classe utilizzata per la chiave deve poter agire come tale, e pertanto deve effettuare opportuni ovveride dei metodi EqualsGetHashCode, pena comportamenti impredicibile del codice. Utilizzando hbm2net questi gener ail codice opportuno:

   1: public override bool Equals(object other)
   2: {
   3:     if (this == other)
   4:         return true;
   5:     WarehouseLocation rhs = other as WarehouseLocation;
   6:     if (rhs == null) 
   7:         return false; // null or not a WarehouseLocation
   8:     return _cellRow.Equals(rhs._cellRow) && _cellCol.Equals(rhs._cellCol);
   9: }
  10:  
  11: public override int GetHashCode()
  12: {
  13:     return _cellRow.GetHashCode() ^ _cellCol.GetHashCode();
  14: }

Aggiungendo queste due classi al progetto possiamo rigenerare il database, che risulta così modificato:

clip_image001[7]

Il diagramma classi diventa, per la parte product, il seguente:

clip_image001[9]

E’ interessante notare che dal punto di vista OR/M StockInformation e WarehouseInformation *non* sono entità. In questa parte del corso si è quindi totalmente smentità la presunta corrispondenza 1:1 tra entità e tabelle.

 

Ora scriviamo il solito unit test per vedere come si comporta NH durante il CRUD su questa collection. Non torneremo a ripetere il discorso “lazy” in quanto analogo a quello visto nella parte 4 per <bag/>.

Il codice dello unit test che lanceremo è il seguente:

   1: [Test]
   2: public void TestCrud()
   3: {
   4:    Console.WriteLine("Creating a product");
   5:    using (ISession session = sessionFactory.OpenSession())
   6:    using (ITransaction trns = session.BeginTransaction())
   7:    {
   8:        Product p = new Product();
   9:        p.Code = "P00000";
  10:        p.Description = " a sample product";
  11:        Console.WriteLine("Adding some Warehouse information");
  12:        var l1 = new WarehouseLocation() { CellCol = "A", CellRow = "1" };
  13:        p.Warehouse.Add(new KeyValuePair<WarehouseLocation, StockInformation>(l1, new StockInformation()));
  14:        p.Warehouse[l1].Quantity = 12;
  15:        p.Warehouse[l1].StockDate = DateTime.Now;
  16:        var l2 = new WarehouseLocation() { CellCol = "A", CellRow = "2" };
  17:        p.Warehouse.Add(new KeyValuePair<WarehouseLocation, StockInformation>(l2, new StockInformation()));
  18:        p.Warehouse[l2].Quantity = 42;
  19:        p.Warehouse[l2].StockDate = DateTime.Now.AddDays(-7);
  20:        Console.WriteLine("Persisting the Product");
  21:        session.SaveOrUpdate(p);
  22:        trns.Commit();
  23:    }
  24:    Console.WriteLine("Modifying warehouse information of a product");
  25:    using (ISession session = sessionFactory.OpenSession())
  26:    using (ITransaction trns = session.BeginTransaction())
  27:    {
  28:        Console.WriteLine("Retrieving product by code");
  29:        var p = session.CreateCriteria<Product>()
  30:            .Add(Expression.Eq("Code", "P00000"))
  31:            .UniqueResult<Product>();
  32:        
  33:        Console.WriteLine("Modify stored quantity on cell A1");
  34:        var l1 = new WarehouseLocation() { CellCol = "A", CellRow = "1" };
  35:        
  36:        p.Warehouse[l1].Quantity -=3;
  37:        p.Warehouse[l1].StockDate = DateTime.Now;
  38:        
  39:        Console.WriteLine("Persisting the Product");
  40:        session.SaveOrUpdate(p);
  41:        trns.Commit();
  42:    }
  43:    Console.WriteLine("deleting warehouse information of a product");
  44:    using (ISession session = sessionFactory.OpenSession())
  45:    using (ITransaction trns = session.BeginTransaction())
  46:    {
  47:        Console.WriteLine("Retrieving product by code");
  48:        var p = session.CreateCriteria<Product>()
  49:            .Add(Expression.Eq("Code", "P00000"))
  50:            .UniqueResult<Product>();
  51:  
  52:        Console.WriteLine("Modify stored quantity on cell A1");
  53:        var l1 = new WarehouseLocation() { CellCol = "A", CellRow = "1" };
  54:  
  55:        p.Warehouse.Remove(l1);
  56:  
  57:        Console.WriteLine("Persisting the Product");
  58:        session.SaveOrUpdate(p);
  59:        trns.Commit();
  60:    }
  61: }

Che produce il seguente risultato. Si sono evidenziati i punti in cui si vede come NH accede agli elementi della collezione:

image

Come si vede, map è molto efficiente ad accedere in update/delete a singoli elementi della collezione. Per contro, visto che la struttura è rappresentata in memoria tramite un dictionary, non è possibile ordinarla ( anche se si facesse la query con un order-by, il funzionamento interno del dictionary potrebbe mutare questo ordine ).

Download Sorgenti
Parte 4
Parte 6
Monday, 27 September 2010 17:09:49 (GMT Daylight Time, UTC+01:00)  #    Comments [0] - Trackback
hbm2net | NHibernate | NHibernate Tutorial

# Saturday, 25 September 2010

Note: For visitors of your site, this entry is only displayed for users with the preselected language Romanian/română (ro)

Continuare la Partea a 3-a : <many-to-one/>

Continuând să lucrăm asupra comenzilor minimale pe care le analizăm acum, ne-ar putea fi folositor să ştim care comenzi sunt asociate unui anumit Customer. Lăsând modelul aşa cum e, o putem face, atât prin HQL cât şi prin ICriteria. Amintesc că acesta este cursul NHibernate de la *ZERO*, deci nu folosim LINQ to NH, şi nici un alt tip de API extern NHibernate –ului, nu pentru că acestea sunt sau nu eficace, ci doar pentru că vrem să analizăm comportamentul de bază. Pur şi simplu creăm cele două unit-teste, primul cu HQL:

   1: [Test]
   2: public void QueryCustomerOrdersWithHQL()
   3: {
   4:    CreateSomeOrdersAndCustomers();
   5:    using (ISession session = sessionFactory.OpenSession())
   6:    {
   7:        var query = session.CreateQuery("from Order o join fetch o.Customer where o.Customer.Name=:customer")
   8:            .SetParameter("customer","Customer A");
   9:        foreach (NHFromScratch.Entities.Order order in query.List<NHFromScratch.Entities.Order>())
  10:        {
  11:            Console.WriteLine("Order:{0} Customer:{1}", order.OrderCode, order.Customer.Name);
  12:        }
  13:    }
  14: }

Care lansat produce următorul output la console:

 

   1: NHibernate: select order0_.Id as Id3_0_, customer1_.Id as Id2_1_, order0_.OrderCode as OrderCode3_0_, order0_.OrderDate as OrderDate3_0_, order0_.DueDate as DueDate3_0_, order0_.CustomerId as CustomerId3_0_, customer1_.Name as Name2_1_, customer1_.AddressLine1 as AddressL3_2_1_, customer1_.AddressLine2 as AddressL4_2_1_, customer1_.City as City2_1_, customer1_.ZipCode as ZipCode2_1_, customer1_.Active as Active2_1_ from Orders order0_ inner join Customers customer1_ on order0_.CustomerId=customer1_.Id, Customers customer2_ where order0_.CustomerId=customer2_.Id and customer2_.Name=@p0;@p0 = 'Customer A'
   2: Order:ORD00000 Customer:Customer A
   3: Order:ORD00001 Customer:Customer A

Observăm că în HQL am specificat oricum join fetch-ul în asociaţia o.Customer:asta tot pentru a evita antipattern-ul select N+1.

Acelaşi rezultat îl putem obţine cu API ICriteria, iată unit-testul:

   1: [Test]
   2: public void QueryCustomerOrdersWithCriteria()
   3: {
   4:     CreateSomeOrdersAndCustomers();
   5:     using (ISession session = sessionFactory.OpenSession())
   6:     {
   7:         var criteria = session.CreateCriteria<NHFromScratch.Entities.Order>()
   8:             .CreateCriteria("Customer")
   9:             .Add(Expression.Eq("Name","Customer A")
  10:             );
  11:  
  12:         foreach (var order in criteria.List<NHFromScratch.Entities.Order>())
  13:         {
  14:             Console.WriteLine("Order:{0} Customer:{1}", order.OrderCode, order.Customer.Name);
  15:         }
  16:     }
  17: }

Acesta produce următorul output de trace:

 

   1: NHibernate: SELECT this_.Id as Id9_1_, this_.OrderCode as OrderCode9_1_, this_.OrderDate as OrderDate9_1_, this_.DueDate as DueDate9_1_, this_.CustomerId as CustomerId9_1_, customer1_.Id as Id8_0_, customer1_.Name as Name8_0_, customer1_.AddressLine1 as AddressL3_8_0_, customer1_.AddressLine2 as AddressL4_8_0_, customer1_.City as City8_0_, customer1_.ZipCode as ZipCode8_0_, customer1_.Active as Active8_0_ FROM Orders this_ inner join Customers customer1_ on this_.CustomerId=customer1_.Id WHERE customer1_.Name = @p0;@p0 = 'Customer A'
   2: Order:ORD00000 Customer:Customer A
   3: Order:ORD00001 Customer:Customer A

Deci am putea spune că problema este rezolvată… dar din nefericire nu într-un mod object oriented. Ar fi mai bine să avem o colecţie de comenzi la care obiectul Customer poate accesa cu uşurinţă, ceva de genul customer.Orders. Pentru a face acest lucru ne vine în ajutor cel mai simplu tag de asociaţie pentru a exprima o colecţie de entităţi legate între ele:

<bag/>

Asta ne redă din punct de vedere object oriented ceva ce există deja în baza de date în formă implicită. Dacă în algebra relaţională o FK exprimă de la sine o relaţie bidirecţională, când vorbim despre obiecte această relaţie trebuie explicitată. Deci este necesar să modificăm mapping-ul prin simpla adăgare în clasa Customer al unui <bag/> al Comenzii:

 

 

   1: <hibernate-mapping xmlns="urn:nhibernate-mapping-2.2" namespace="NHFromScratch.Entities" 
   2:                                                       assembly="NHFromScratch.Entities">
   3:   <class name="Customer" table="Customers">
   4:     <id name="Id" type="Int32">
   5:       <generator class="native"/>
   6:     </id>
   7:     <property name="Name" type="String" not-null="true"/>
   8:     <property name="AddressLine1" type="String" not-null="true"/>
   9:     <property name="AddressLine2" type="String" />
  10:     <property name="City" type="String" not-null="true"/>
  11:     <property name="ZipCode" type="String" not-null="true"/>
  12:     <property name="Active" type="Boolean" not-null="true"/>
  13:     <bag name="Orders">
  14:       <key column="CustomerId"/>
  15:       <one-to-many class="Order"/>
  16:     </bag> 
  17:   </class>
  18:   
  19: </hibernate-mapping>

 

Făcând un rebuild al soluţiei, obţinem o diagramă de clase pentru entităţile noastre:

 

clip_image002

Care ne arată de fapt relaţia bidirecţională dintre client şi comenzile sale. Să vedem acum cu un alt unit test cum putem lucra cu un Customer şi obţine comenzile ce aparţin acestuia:

 

   1: [Test]
   2: public void QueryCustomerOrdersWithBag()
   3: {
   4:     CreateSomeOrdersAndCustomers();
   5:     using (ISession session = sessionFactory.OpenSession())
   6:     {
   7:         var customerA = session.CreateCriteria<Customer>()
   8:             .Add(Expression.Eq("Name", "Customer A")
   9:             ).UniqueResult<Customer>();
  10:         Console.WriteLine("Obtained customer instance");
  11:         foreach (var order in customerA.Orders)
  12:         {
  13:             Console.WriteLine("Order:{0} Customer:{1}", order.OrderCode, order.Customer.Name);
  14:         }
  15:     }
  16: }

Aceasta produce următorul output la console:

 

   1: Obtained customer instance
   2: NHibernate: SELECT orders0_.CustomerId as CustomerId1_, orders0_.Id as Id1_, orders0_.Id as Id3_0_, orders0_.OrderCode as OrderCode3_0_, orders0_.OrderDate as OrderDate3_0_, orders0_.DueDate as DueDate3_0_, orders0_.CustomerId as CustomerId3_0_ FROM Orders orders0_ WHERE orders0_.CustomerId=@p0;@p0 = 1
   3: Order:ORD00000 Customer:Customer A
   4: Order:ORD00001 Customer:Customer A

După cum se observă, colecţia a fost recuperată de la storage doar în momentul necesar. Şi în collection avem un comportament „lazy”, care în multe cazuri este oportun, dar nu întotdeauna. Şi aici putem modifica comportamentul lazy specificându-l în mapping:

 

   1: <bag name="Orders" fetch="join">
   2:   <key column="CustomerId"/>
   3:   <one-to-many class="Order"/>
   4: </bag> 

Recompilând testul de dinainte obţinem un output uşor diferit:

 

   1: Obtained customer instance
   2: Order:ORD00000 Customer:Customer A
   3: Order:ORD00001 Customer:Customer A

 

 

Nici un query: o dată cu încărcarea customer-ului a fost încărcată şi asociaţia cu collection a comenzii, query-ul a fost de fapt următorul:

 

   1: SELECT this_.Id ... orders2_.CustomerId as CustomerId3_... FROM Customers this_ left outer join Orders orders2_ on this_.Id=orders2_.CustomerId WHERE this_.Name = @p0;@p0 = 'Customer A'

 

In toate cazurile, bag-ul care ne soseşte este o listă potenţial dezordonată. Putem cere NH-ului să facă, sau mai bine spus să ceară bazei de date să facă, o ordonare în baza unei anumite proprietăţi a entităţii colecţionate. In exemplul nostru am putea vrea o ordonare în funcţie de data comenzii, adăugând la mapping acest atribut:

 

   1: <bag name="Orders" fetch="join" order-by="OrderDate" >

 

Şi obţinând deci interogaţia următoare:

 

1: SELECTFROM Customers this_ left outer join Orders orders2_ on this_.Id=orders2_.CustomerId WHERE this_.Name = @p0

ORDER BY orders2_.OrderDate;

@p0 = 'Customer A'

 

Cu oportuna clauză id ordonare. Să vedem acum ce se întâmplă făcând nişte teste cu HQL. Primul:

 

   1: [Test]
   2: public void QueryCustomerOrdersWithBagUsingHQL()
   3: {
   4:    CreateSomeOrdersAndCustomers();
   5:    using (ISession session = sessionFactory.OpenSession())
   6:    {
   7:        IQuery query = session.CreateQuery("from Customer c where c.Name=:name")
   8:            .SetParameter("name", "Customer A");
   9:        
  10:        var list = query.List<Customer>();
  11:        Assert.AreEqual(1, list.Count);
  12:        var customerA = list[0];
  13:        foreach (var order in customerA.Orders)
  14:        {
  15:            Console.WriteLine("Order:{0} Customer:{1}", order.OrderCode, order.Customer.Name);
  16:        }
  17:    }
  18: }

 

Observăm că numărul de query revine la un comportament lazy:

 

 

   1: select customer0_.Id as Id2_, customer0_.Name as Name2_, customer0_.AddressLine1 as AddressL3_2_, customer0_.AddressLine2 as AddressL4_2_, customer0_.City as City2_, customer0_.ZipCode as ZipCode2_, customer0_.Active as Active2_ from Customers customer0_ where customer0_.Name=@p0;@p0 = 'Customer A'
   2: SELECT orders0_.CustomerId as CustomerId1_, orders0_.Id as Id1_, orders0_.Id as Id3_0_, orders0_.OrderCode as OrderCode3_0_, orders0_.OrderDate as OrderDate3_0_, orders0_.DueDate as DueDate3_0_, orders0_.CustomerId as CustomerId3_0_ FROM Orders orders0_ WHERE orders0_.CustomerId=@p0 ORDER BY orders0_.OrderDate;@p0 = 1
   3: Order:ORD00000 Customer:Customer A
   4: Order:ORD00001 Customer:Customer A

 

Cu toate acestea, putem specifica şi în HQL politica fetch, modificând HQL-ul in: “from Customer c join fetch c.Orders where c.Name=:name”. Din nefericire testul eşuează în mod jalnic:

 

clip_image002[5]

 

Acesta este un comportament „ciudat” al NH-ului. De fapt există un singur Customer în testele noastre care satisface relaţia cerută, totuşi efectuând join-ul, ne sunt restituite mai multe record-uri (n record-uri, unde n reprezintă numărul comenzilor). Trebuie menţionat că NH nu creează instanţe diferite ale obiectelor, ci pune în lista output-ului nişte references ale aceleiaşi entităţi.

Se poate remedia la această problemă cu un “ResultTransformer”, adică un post-processor al rezultatelor din NH. Putem construi nişte post processori (IResultTransformer ) pentru a manipula în modul pe care-l considerăm oportun rezultatele unei ICriteria sau cele ale unui HQL. NH furnizează anumiţi transformer de utilităţi, unul dintre aceştia ne este util în această situaţie complicată:

 

   1: IQuery query = session.CreateQuery("from Customer c join fetch c.Orders where c.Name=:name")
   2:     .SetParameter("name", "Customer A")
   3:     .SetResultTransformer(Transformers.DistinctRootEntity)
   4:     ;

Această transformare face ca entităţile „identice„ să devină una singură, şi iată că liniile întoarse sunt acelea pe care le aşteptam, iar query-ul devine unul singur, după ce am specificat un comportament non lazy.

Ce se întâmplă dacă încercăm să înlăturăm nişte comenzi de la client? ( atenţie, în realitate acest lucru nu se va întâmpla aproape niciodată: vor exista probabil anumite strategii soft-delete, aici facem doar nişte exemple ) Iată un unit test pentru a testa acest scenariu:

 

   1: [Test]
   2: public void RemoveOrderForCustomers()
   3: {
   4:     CreateSomeOrdersAndCustomers();
   5:     using (ISession session = sessionFactory.OpenSession())
   6:     using(ITransaction trns = session.BeginTransaction() )
   7:     {
   8:         var customerA = session.CreateCriteria<Customer>()
   9:            .Add(Expression.Eq("Name", "Customer A")
  10:            ).UniqueResult<Customer>();
  11:         Console.WriteLine("Obtained customer instance");
  12:         customerA.Orders.RemoveAt(0);
  13:         trns.Commit();
  14:     }
  15: }

Dacă lansăm acest test, obţinem următoarea eroare:

   1: NHFromScratch.Tests.TestOrders.RemoveOrderForCustomers:
   2: NHibernate.Exceptions.GenericADOException : could not delete collection rows: [NHFromScratch.Entities.Customer.Orders#1][SQL: UPDATE Orders SET CustomerId = null WHERE CustomerId = @p0 AND Id = @p1]
   3:   ---->; System.Data.SqlClient.SqlException : Cannot insert the value NULL into column 'CustomerId', table 'NHFROMSCRATCH.dbo.Orders'; column does not allow nulls. UPDATE fails.
   4: The statement has been terminated.

Practic NH încearcă să „desprindă” comanda eliberând-o de Customer, dar noi am specificat faptul că Customer-ul nu nu poate fi null, deci logica e corectă. Singurul mod este să spunem că owner-ul acestei asociaţii este Customer, introducând inverse=”true” cu intenţia de a spune că „partea”asociaţiei nu este responsabilă de colecţie, astfel:

 

 

   1: <bag name="Orders" fetch="join" order-by="OrderDate" inverse="true">
   2:   <key column="CustomerId"/>
   3:   <one-to-many class="Order"/>
   4: </bag> 

Din păcate testul continuă să nu funcţioneze, de această dată ne zice că nimic nu a fost anulat:

clip_image004

Ceea ce putem face este să forţăm anularea cu parametrul „cascade”:

 

   1: <bag name="Orders" fetch="join" order-by="OrderDate" inverse="true" cascade="all-delete-orphan">
   2:   <key column="CustomerId"/>
   3:   <one-to-many class="Order"/>
   4: </bag> 

Şi iată că în sfârşit testul funcţionează:

clip_image006

Parametrul cascade are efecte similare şi pentru operaţiunile de introducere a noilor comenzi: acestea vor fi salvate automat în momentul salvării entităţii Customer.

Înainte de a încheia această parte merită să aruncăm o privire asupra unui comportament pe care este bine să-l cunoaştem date fiind problematicile de optimizare. Să presupunem că vrem să ştim câte comenzi are un Customer, şi că avem asociaţia în modalitatea lazy:

 

   1: [Test]
   2:    public void OrdersCount()
   3:    {
   4:        CreateSomeOrdersAndCustomers();
   5:        using (ISession session = sessionFactory.OpenSession())
   6:        {
   7:            var customerA = session.CreateCriteria<Customer>()
   8:               .Add(Expression.Eq("Name", "Customer A")
   9:               ).UniqueResult<Customer>();
  10:            Console.WriteLine("Obtained customer instance");
  11:            Assert.AreEqual(2, customerA.Orders.Count);
  12:        }
  13:       
  14: }

si uitaţi ce query curat se execută:

 

NHibernate: SELECT count(Id) FROM Orders WHERE CustomerId=@p0;@p0 = 1

 

 

Bun, şi această parte s-a terminat, nu au fost tratate toate combinaţiile posibile de atribute pentru <bag/>, dar s-a pus accentul pe anumite probleme clasice, minimul pentru a putea începe experimentarea pe cont propriu. Două ultime lucruri:

  • Până în acest moment echivalenţa clasă-tabel s-a manifestat întotdeauna, dar nu va fi mereu aşa.
  • Să ne uităm la query-urile pe care le generează NH iniţial pentru a înţelege ce se întâmplă, dar tot mai mult trebuie să învăţăm să dăm ca sigur comportamentul şi să ne uităm la query-uri pentru motive de optimizare.

Descarca Sursa
Parte 3
Saturday, 25 September 2010 11:54:55 (GMT Daylight Time, UTC+01:00)  #    Comments [0] - Trackback
hbm2net | NHibernate | NHibernate Tutorial

Note: For visitors of your site, this entry is only displayed for users with the preselected language Romanian/română (ro)

Continuare la Partea a 2-a: Crearea unui proiect

Când o entitate a modelului nostru are o proprietate de tipul unei alte entităţi a domeniului, adică face referire la o altă entitate, putem clarifica acest concept în fişierul de mapping cu tag-ul <many-to-one/>. Continuând să îmbogăţim modelul început la Partea a 2-a, hotărâm să introducem entitatea comandă, şi de cum o comandă are neapărată nevoie de un client, vom putea vedea cum să explicităm acest concept. In realitate entitatea comandă va fi mult mai complexă, dar pentru moment ne mulţumim să observăm relaţia dintre comandă şi cel care a făcut-o.

Adăugăm un fişier mapping construit astfel:

 

   1: <?xml version="1.0" encoding="utf-8" ?>
   2: <hibernate-mapping xmlns="urn:nhibernate-mapping-2.2" namespace="NHFromScratch.Entities" 
   3:                                                       assembly="NHFromScratch.Entities">
   4:   <class name="Order" table="Orders">
   5:     <id name="Id" type="Int32">
   6:       <generator class="native"/>
   7:     </id>
   8:     <property name="OrderCode" type="AnsiString" />
   9:     <property name="OrderDate" type="DateTime"/>
  10:     <property name="DueDate" type="DateTime"/>
  11:     <many-to-one name="Customer" class="Customer" column="CustomerId"/>
  12:     <!-- details comes here -->
  13:   </class>
  14:   
  15: </hibernate-mapping>

După cum vedeţi, am adăugat câteva proprietăţi de tip date, dar noutatea o constituie tag-ul <many-to-one/> care ne spune în această definiţie minimală:

  • Numele pe care îl are proprietatea cu atributul “name”
  • Care este entitatea externă, adică tipul său (primul tabel nu ne mai interesează)
  • Opţional numele coloanei (practic numele coloanei ce va fi o foreign-key în tabelul order VS tabelul customer) cu atributul “column”.

Dacă nu aş explicita column, numele ar fi acelaşi cu al proprietăţii. Pe scurt: column aplică şi la proprietăţile simple, cu aceeaşi semantică.

Bun, acum să ne amintim să punem “embedded resource” la noul fişier hbm, pe care-l vom numi în mod întâmplător Order.hbm.xml, lansăm compilarea pentru a obţine o primă versiune a fişierului .cs order.generated.cs, pe care-l vom adăuga proiectului şi-l vom compila.

clip_image002

După ce am făcut acest lucru putem lansa unit testele, şi recrea baza de date:

clip_image004

Testul „trece pe verde” şi observăm cum tabelul order apare în script DDL, şi cum foreign key care mizează la Customer este introdusă în mod oportun în tabelul order. În plus, cei mai atenţi vor fi observat cum proprietatea code a fost definită ca VARCHAR, în timp ce celelalte coloane şir din tabelul customer sunt însă NVARCHAR, acest lucru este provocat de alegerea de a folosi ca tip pentru şirul OrderCode AnsiString în loc de string. Acest lucru nu are nici un efect în codul C#, dar schimbă DDL-ul din database, deci alegeţi-l pe cel mai potrivit. Un alt lucru important, FK-ul poate fi anulat, asta nu e prea bine pentru că am spus că un Order trebuie să aibă obligatoriu un Customer asociat: este de ajuns să folosim atributul not-null = “true” adăugat la tag-ul <many-to-one/>.

 

   1: <many-to-one name="Customer" class="Customer" column="CustomerId" not-null="true"/>

Asta va modifica DDL-ul ca mai jos :

 

   1: create table Orders (
   2:        Id INT IDENTITY NOT NULL,
   3:       OrderCode VARCHAR(255) null,
   4:       OrderDate DATETIME null,
   5:       DueDate DATETIME null,
   6:       CustomerId INT not null,
   7:       primary key (Id)
   8:    )

 

În cele din urmă baza noastră de date va avea această schemă:

clip_image002[5]

Acum putem crea câteva teste, pentru a vedea cum funcţionează modelul. Să spunem că vrem să creăm nişte teste repetitive, deci TestFixture va recrea de fiecare dată o bază de date de la zero, astfel fiecare test va fi independent. Primul test interesant ar putea să fie crearea unei comenzi, cu un nou client simultan. De această dată vom vedea mai bine în detaliu variile operaţii pe care le vom face. Iată codul de test pe care vom încerca să-l facem:

   1: public void CreateAnOrderWithANewCustomer()
   2: {
   3:     using(ISession session = sessionFactory.OpenSession() )
   4:     using (ITransaction transaction = session.BeginTransaction())
   5:     {
   6:         Order order = new Order();
   7:         order.Customer = new Customer()
   8:         {
   9:             Active = true
  10:             ,
  11:             AddressLine1 = "xxxxxxx xxx xxxx"
  12:             ,
  13:             AddressLine2 = "xx xxxxxxx xxxxx"
  14:             ,
  15:             City = "Milano"
  16:             ,
  17:             Name = "Customer Name"
  18:             ,
  19:             ZipCode = "00000"
  20:         };
  21:         order.OrderCode="ORD00000";
  22:         order.OrderDate = DateTime.Now;
  23:         order.DueDate = order.OrderDate.Value.AddMonths(2);
  24:         session.SaveOrUpdate(order);
  25:         transaction.Commit();
  26:     }
  27: }

Mai întâi creăm o „Sesiune”, ceva ce se aseamănă unei conexii la baza de date, dar e mai mult de atât, este o unitate de lucru. În interiorul unei unităţi de lucru, entităţile vor deveni demne de acest nume, iar NHibernate urmăreşte modificările şi se asigură să garanteze unicitatea cu acelaşi id deţinut în baza de date, şi multe alte lucruri. Să spunem câteva regului bune despre session:

  • E uşor să creezi o sesiune (nu putem spune asta despre Session Factory )
  • Încercaţi să faceţi sesiunile scurte din punct de vedere temporal
  • Limitaţi numărul entităţilor cu care lucrează o sesiune
  • Sesiunea nu este multithread.

Imediat după, am deschis o tranzacţie, pentru că la sfârşit vom menţine ceva nou în database, şi vrem să facem toate operaţiile în unitatea noastră de lucru (UOW = unit of work) în mod consistent şi excepţional. Într-un singur UOW putem avea mai multe tranzacţii.

Lansăm testul şi vedem ce se întâmplă în text output al NUnit, pentru a vedea ce face NH în baza de date. Din nefericire testul „trece pe roşu”:

clip_image002[7]

Aceasta este faimoasa eroare:

“NHFromScratch.Tests.TestOrders.CreateAnOrderWithANewCustomer:
NHibernate.PropertyValueException : not-null property references a null or transient valueNHFromScratch.Entities.Order.Customer”

Practic, asta înseamnă că încercăm să salvăm o entitate (Order) care are o referire la altă entitate, dar aceasta din urmă nu este persistentă ( adică transitoria=Transient ). Pentru a rezolva această problemă avem la dispoziţie două căi:

Să salvăm entitatea referită (dacă aceasta mai este tranzitorie) înainte de a salva entitatea comandă:

   1: session.SaveOrUpdate(order.Customer);
   2: session.SaveOrUpdate(order);
   3: transaction.Commit();

Acest lucru face testul să devină „verde”, iar acestea sunt query-urile trimise la baza de date:

 

   1: ***** NHFromScratch.Tests.TestOrders.CreateAnOrderWithANewCustomer
   2: NHibernate: INSERT INTO Customers (Name, AddressLine1, AddressLine2, City, ZipCode, Active) VALUES (@p0, @p1, @p2, @p3, @p4, @p5); select SCOPE_IDENTITY();@p0 = 'Customer Name', @p1 = 'xxxxxxx xxx xxxx', @p2 = 'xx xxxxxxx xxxxx', @p3 = 'Milano', @p4 = '00000', @p5 = True
   3: NHibernate: INSERT INTO Orders (OrderCode, OrderDate, DueDate, CustomerId) VALUES (@p0, @p1, @p2, @p3); select SCOPE_IDENTITY();@p0 = 'ORD00000', @p1 = 11/09/2010 20.48.24, @p2 = 11/11/2010 20.48.24, @p3 = 1

Cealaltă modalitate este aceea de a acţiona asupra atributului „cascade” al tag-ului <many-to-one/>, de exemplu modificându-l astfel:

   1: <many-to-one name="Customer" class="Customer" column="CustomerId" not-null="true" cascade="save-update"/>

 

În test ne putem din nou limita la a salva Order, la toate celelalte consideraţii pentru a menţine în mod consistent operaţiunea asupra database-ului se gândeşte NH, mulţumită atributului „cascate”.

Acum trecem la partea de citire, creăm un test cu câţiva clienţi şi câteva comenzi şi încercăm să facem nişte extrageri de entităţi deja salvate. Să presupunem că creăm o bază de entităţi, într-un mod cu totul analog aceluia de mai sus, creând doi clienţi, Customer A şi Customer B, cu respectiv 2 şi 1 comandă fiecare. Primul test prevede recuperarea entităţilor salvate prin crearea unei interfețe ICriteria. Iată codul de test:

   1: [Test]
   2: public void QueryWithICRiteria()
   3: {
   4:    CreateSomeOrdersAndCustomers();
   5:    using (ISession session = sessionFactory.OpenSession())
   6:    {
   7:        var criteria = session.CreateCriteria<Order>();
   8:        foreach (Order order in criteria.List<Order>())
   9:        {
  10:            Console.WriteLine("Order:{0} Customer:{1}", order.OrderCode, order.Customer.Name);
  11:        }
  12:    }
  13: }

După cum vedeţi există un foreach care imprimă detaliile comenzii şi a clientului asociat acestuia. Dar dacă observăm query-urile pe care le face NH descoperim o problemă:

 

 

clip_image002[9]

 

Practic Lista forţează execuţia primului query, dar de fiecare dată când mergem să căutăm proprietatea „Customer” iată că baza de date primeşte un nou query, pentru a recupera entitatea referită. În ciuda faptului că NH este atât de deştept încât nu efectuează un query pentru acelaşi Customer de două ori, ceea ce se întâmplă nu reprezintă oricum un comportament ideal. Acest lucru se întâmplă pentru că asociaţia many-to-one este din default “lazy”: Asta poate fi util în multe cazuri, dar dacă, ca în cazul nostru ştim anticipat că vrem să avem toate asociaţiile, cădem în anti patter Select N+1. Din fericire putem evita asta, acţionând asupra proprietăţilor interfeţei ICriteria:

 

   1: var criteria = session.CreateCriteria<Order>()
   2: .SetFetchMode("Customer",FetchMode.Eager)
   3: ;

Dacă schimbăm modul fetch al asociativei schimbăm în mod dramatic query-urile executate, ce se reduc la una singură:

   1: SELECT this_.Id as Id3_1_, ... FROM Orders this_ inner join Customers customer2_ on this_.CustomerId=customer2_.Id

 

 

Aceasta este o optimizare pe care trebuie să încercăm mereu să o facem. Puteam obţine acelaşi rezultat doar adăugând atributul “fetch” în tag-ul <many-to-one/>:

   1: <many-to-one name="Customer" class="Customer" column="CustomerId" fetch="join" not-null="true" cascade="save-update"/>

Desigur, într-un scenariu în care am vrea să schimbăm fetch-mode –ul invers, am putea seta prin cod, SetFetchMode cu valoarea select.

Dar să lăsăm Fetch-Mode-ul cu valoarea join şi să încercăm să facem acelaşi query cu HQL:

   1: [Test]
   2: public void QueryWithHql()
   3: {
   4:     CreateSomeOrdersAndCustomers();
   5:     using (ISession session = sessionFactory.OpenSession())
   6:     {
   7:         var query = session.CreateQuery("from Order");
   8:         foreach (Order order in query.List<Order>())
   9:         {
  10:             Console.WriteLine("Order:{0} Customer:{1}", order.OrderCode, order.Customer.Name);
  11:         }
  12:     }
  13: }

Vom descoperi că HQL nu cunoaşte tipul de fetch specificat de atributul fetch al relaţiei <many-to-one/> !

Asta pentru că HQL prevede posibilitatea de a specifica fetch mode –ul textual:

 

   1: var query = session.CreateQuery("from Order o join fetch o.Customer");

 

Această modificare ne readuce la singurul query pentru toate cele trei situaţii.

Şi cu asta am terminat definiţia relaţiei <many-to-one/> şi, având în vedere că acesta este un curs de la *zero* nu am luat unul câte unul fiecare atribut al relaţiei many-to-one, ci o cantitate suficientă pentru a începe folosirea acesteia în mod eficace.

 

Descarca Sursa
Partea a 2-a

Parte 4
Saturday, 25 September 2010 11:30:00 (GMT Daylight Time, UTC+01:00)  #    Comments [0] - Trackback
hbm2net | NHibernate | NHibernate Tutorial

Note: For visitors of your site, this entry is only displayed for users with the preselected language Romanian/română (ro)

Continuare la Prima parte: De ce avem nevoie?

În această parte construim un proiect cu NHibernate ce ne va permite menţinerea unei prime entităţi. De fapt partea OR/M va fi menţinută cât mai simplu posibil, nici un fel de reference sau colecţii sau componente: obiectivul este de a crea un schelet ce funcţionează şi ce poate fi testat. Desigur, toate acestea vor servi drept punct de plecare, anumite lucruri vă vor părea evidente, altele mai puţin, procedaţi cum credeţi mai bine, dar cum acesta este un curs „de la zero” vreau să mă opresc asupra tuturor punctelor.

Decidem să punem entităţile într-o Class Library ( dll ). Aceasta este o alegere ce probabil este bine să facem şi pentru a trimite o adevărată soluţie în producţie. Să creăm deci o nouă solution cu Visual Studio. Deosebim numele soluţiei cu NHFromScratch.All, şi adăugăm în interior un nou proiect de tipul class library, pe care-l vom numi NHFromScratch.Entities.

clip_image002

În interiorul directorului rădăcină al soluţiei, creăm un subdirector „Lib”, în stare să conţină dependinţele soluţiei (NHibernate), de exemplu în completarea dependinţei NH vedeţi nunit.framework.dll, dependinţă de care vom avea nevoie în scurt timp când vom vrea să testăm entităţile noastre.

clip_image004

Bun, pasul următor este crearea mapping-ului pentru prima şi singura entitate pe care vrem sa o administrăm. Există nenumărate strategii pentru a crea un mapping pentru NH, cea pe care o prezint aici fiind o soluţie “plain vanilla”, fără înflorituri şi cu un cost zero: cu practica, fiecare alege metoda pe care o consideră mai bună, dar pentru a învăţa eu cred că este de ajutor scrierea fişierului XML. Adăugăm deci un subdirector „Mapping” în class library pe care tocmai am creat-o, iar în acest folder creăm un nou fişier cu numele entităţii (Customer în cazul nostru) cu extensia .hbm.xml. Dacă am abilitat intellisense-ul, după cum am sfătuit în prima parte, unicul tag pe care trebuie să-l reţinem este <hibernate-mapping>, toate celelalte vor fi expuse via intellisense după ce namespace-ul va fi specificat în mod corect, după cum puteţi vedea mai jos:

clip_image006

Este foarte important să vă amintiţi să setaţi “Embedded Resource” ca build action în fişierul de mapping, altfel… configurarea NHibernate-ului nu va funcţiona. Ca să fim precişi acest lucru s-ar putea evita, dar pentru asta ar trebui să distribuim fişierele de mapping împreună cu aplicaţia, şi sincer să fiu, este o opţiune care nu îmi place şi pe care nu am mai văzut-o pusă în practică.

După care nu rămâne decât să scriem efectiv mapping-ul. În acest prim test vom folosi doar trei elemente de mapping: class,key si property, practic minimul indispensabil şi pentru a avea o unitate la lucru. Vom aprofunda variile aspecte în următoarele părţi.

   1: <?xml version="1.0" encoding="utf-8" ?>
   2: <hibernate-mapping xmlns="urn:nhibernate-mapping-2.2" 
   3: assembly="NHFromScratch.Entities">
   4: <class name="Customer" table="Customers">
   5: <id name="Id" type="Int32">
   6: <generator class="native"/>
   7: </id>
   8: <property name="Name" type="String" not-null="true"/>
   9: <property name="AddressLine1" type="String" not-null="true"/>
  10: <property name="AddressLine2" type="String" />
  11: <property name="City" type="String" not-null="true"/>
  12: <property name="ZipCode" type="String" not-null="true"/>
  13: <property name="Active" type="Boolean" not-null="true"/>
  14: </class>
  15: </hibernate-mapping>

Să spunem oricum câteva cuvinte pentru a explica în mare ce conţine mapping-ul entităţii Customer. Mai întâi de toate sunt specificate namespace-ul şi assembly-ul conţinute în entitatea mappată. Acest lucru nu este obligatoriu, dar ne permite să scriem într-un mod mai clar numele clasei mai departe. Tag class-a indică deci clasa pe care intenţionăm să o utilizăm ca entity. Amintim că adesea nu există un raport 1:1 între tabele şi entităţi, dar în acest exemplu simplu vom avea o astfel de situaţie. Tag class-a ne permite să definim numele clasei şi tabelul cu care se asociază în BD. Tag-ul din josul <id>- ului este necesar: NHibernate vrea să ştie în ce fel database-ul „distinge” entităţile. Există nenumărate moduri pentru a face acest lucru, iar modul este ales de sub-tag generator: în acest exemplu am specificat un generator „nativ”, ce înseamnă că un id este univoc desemnat de BD (identity, auto-crescător) oricare ar fi numele său în baza de date target. Succesiv vin proprietăţile, cu numele lor frumoase şi tipuri dat. Veţi observa că nu am specificat nici un nume pentru relativa coloană în BD: simplu, dacă numele este acelaşi, nimeni nu ne obligă să o facem, să nu ne repetăm. Dimpotrivă, tipul ar fi un surplus dacă am scrie manual clasa de mai jos: NHibernate are obiceiul de a deduce informaţiile oriunde este posibil. Dar noi nu vrem să creăm clasa manual, şi de aceea ne folosim de hbm2net (vezi prima parte). Pentru a dezlănţui acest tool, folosim un pre build step al Visual Studio, ca cel de mai jos:

clip_image008

Practic îi cerem hbm2net-ului să lucreze pe toate fişierele .hbm.xml pe care le găseşte în subdirectorul mapping al soluţiei, să pună output-urile (mai bine spus clasele) în directorul proiectului. Deşi hbm2net este un editor template based care poate genera aproape orice artefact plecând de la hbm, fără parametrii se limitează să genereze clasele ce corespund mapping-ului. Lansând build-ul în output ar trebui să vedeţi şi progress-ul hbm2net-ului:

clip_image010

Odată terminat build-ul, veţi avea fişierul autogenerat pentru entitate în file system. Adăugaţi-l la proiect.

clip_image012

Aruncaţi o privire la clasa auto generată, aruncaţi o privire şi la mapping: dacă aţi încercat vreun alt tool pentru NHibernate veţi observa că lucrul pe care l-aţi făcut până acum pare mult mai simplificat. Scriem numai strictul necesar, ceea ce nu ne trebuie lăsăm de-o parte, pentru că acesta este NHibernate de la ZERO.

clip_image014

Pentru a completa soluţia adăugăm o reference la NHibernate: NHibernate si Iesi.Collection sunt singurele assembly pe care trebuie să le anexăm la momentul build-ului.

A sosit în sfârşit momentul să facem testul. Adăugăm la solution un proiect de tipul Class Library, unde vom introduce unit testele .

clip_image016

Soluţia testului, asemănător unui proiect care trebuie să meargă în producţie, are nevoie de anumite reference non statice de la NHibernate. Noi le avem în directorul Lib al soluţiei, trebuie să le mutăm lângă cea a dll-ului de producţie. Putem alege să facem acest lucru cu un post-build step, după cum este ilustrat mai jos.

clip_image018

Şi reference-urile acestui proiectului cer puţin mai multă implicare: avem nevoie de Iesi.Collection & NHibernate ca de obicei dar şi de assembly –ul nostru cu entităţi, de log4net pentru logging, şi în plus, desigur, de nunit.framework.

clip_image020

Mie personal îmi place bara verde a NUnit- ului, deci testăm proiectul cu NUnit, punând NUnit.exe ca “start external program”.

clip_image022

Acum creăm unit testul nostru, cu scopul de a configura NH, puţin log, şi vedem dacă reuşim să creăm Database-ul. Da, în acest tutorial database-ul nu îl vom face manual, pentru că, chiar dacă acesta este NHibernate de la ZERO, vrem să scriem cât mai puţin cod posibil.

Codul unit testului este următorul:

 

 

   1: namespace NHFromScratch.Tests
   2: {
   3:     [TestFixture]
   4:     public class TestCustomer
   5:     {
   6:         static TestCustomer()
   7:         {
   8:             ConfigureLogForNet();
   9:         }
  10:  
  11:         ISessionFactory sessionFactory;
  12:         [SetUp]
  13:         public void Setup()
  14:         {
  15:             
  16:             CreateSessionFactory();
  17:         }
  18:  
  19:         [Test]
  20:         public void CanCreatedatabse()
  21:         {
  22:             SchemaExport export = new SchemaExport(CreateConfiguration());
  23:             export.Execute(true, true, false);
  24:         }
  25:  
  26:         [Test]
  27:         public void CanPersistCustomer()
  28:         {
  29:             Console.WriteLine("\n****** SAVE A CUSTOMER ********");
  30:             //save a customer
  31:             using (var session = sessionFactory.OpenSession())
  32:             using(var transaction = session.BeginTransaction())
  33:             {
  34:  
  35:                 Customer c = new Customer()
  36:                 {
  37:                     Active = true
  38:                      ,
  39:                     AddressLine1 = "xxxx"
  40:                      ,
  41:                     City = "Cuneo"
  42:                      ,
  43:                     Name = "Bill Gates"
  44:                      ,
  45:                     ZipCode = "12060"
  46:                 };
  47:                 session.Save(c);
  48:                 transaction.Commit();
  49:             }
  50:             Console.WriteLine("\n****** RETRIEVE A CUSTOMER ********");
  51:             //retrieve a customer
  52:             using (var session = sessionFactory.OpenSession())
  53:             
  54:             {
  55:  
  56:                 var customer = session.CreateCriteria<Customer>()
  57:                     .Add(Expression.Eq("Name", "Bill Gates"))
  58:                     .UniqueResult();
  59:  
  60:                 Assert.NotNull(customer);
  61:             }
  62:  
  63:             Console.WriteLine("\n****** MODIFY A CUSTOMER ********");
  64:             //modify a customer
  65:             using (var session = sessionFactory.OpenSession())
  66:             using (var transaction = session.BeginTransaction())
  67:             {
  68:  
  69:                 var customer = session.CreateCriteria<Customer>()
  70:                     .Add(Expression.Eq("Name", "Bill Gates"))
  71:                     .UniqueResult<Customer>();
  72:                 customer.ZipCode = "0000";
  73:                 Assert.NotNull(customer);
  74:                 
  75:                 transaction.Commit();
  76:             }
  77:  
  78:  
  79:             Console.WriteLine("\n****** VERIFY CUSTOMER IS MODIFIED ********");
  80:             //verify mod
  81:             using (var session = sessionFactory.OpenSession())
  82:             {
  83:  
  84:                 var customer = session.CreateCriteria<Customer>()
  85:                     .Add(Expression.Eq("Name", "Bill Gates"))
  86:                     .UniqueResult<Customer>();
  87:                 
  88:                 Assert.NotNull(customer);
  89:                 Assert.AreEqual("0000", customer.ZipCode);
  90:             }
  91:  
  92:  
  93:             Console.WriteLine("\n****** DELETE A CUSTOMER ********");
  94:             //delete a customer
  95:             using (var session = sessionFactory.OpenSession())
  96:             using (var transaction = session.BeginTransaction())
  97:             {
  98:  
  99:                 var customer = session.CreateCriteria<Customer>()
 100:                     .Add(Expression.Eq("Name", "Bill Gates"))
 101:                     .UniqueResult();
 102:                 session.Delete(customer);
 103:                 transaction.Commit();
 104:             }
 105:             Console.WriteLine("\n****** VERIFY CUSTOMER IS DELETED ********");
 106:             //verify delete
 107:             using (var session = sessionFactory.OpenSession())
 108:             
 109:             {
 110:  
 111:                 var customer = session.CreateCriteria<Customer>()
 112:                     .Add(Expression.Eq("Name", "Bill Gates"))
 113:                     .UniqueResult();
 114:                 Assert.IsNull(customer);
 115:             }
 116:         }
 117:  
 118:  
 119:  
 120:         private static void ConfigureLogForNet()
 121:         {
 122:             TraceAppender app = new TraceAppender();
 123:             app.Layout = new SimpleLayout();
 124:             //BasicConfigurator.Configure( app);
 125:         }
 126:  
 127:         private void CreateSessionFactory()
 128:         {
 129:             Configuration cfg = CreateConfiguration();
 130:             sessionFactory = cfg.BuildSessionFactory();
 131:         }
 132:  
 133:         private static Configuration CreateConfiguration()
 134:         {
 135:             Configuration cfg = new Configuration();
 136:             cfg.Configure();
 137:             // implicitamente carichiamo tutti i mapping che si trovano nell'assembly che
 138:             // contiene customer
 139:             cfg.AddAssembly(typeof(Customer).Assembly);
 140:             return cfg;
 141:         }
 142:  
 143:  
 144:     }
 145: }

 

 

Odată ce am făcut asta lansăm testul “CanCreateDatabase” şi, după cum probabil suspectăm, ajungem pe roşu cu logger-ul care spune ceva de genul:

 

  

 

 

clip_image024

În realitate am „uitat” să configurăm NHibernat –ul, şi deci la care BD poate miza? După cum vedeţi, faptul că avem log-ul abilitat reduc dubiile în privinţa greşelii. Printre altele lipseşte chiar şi database-ul…

Deci creăm mai întâi un database gol, nu este nevoie de nici un tabel, lăsăm ca NHibernate să facă munca grea.

clip_image026

În exemplu am creat un database cu numele NHFROMSCRATCH în SQLEXPRESS local. Apoi, întorcându-mă la , când se vedea conţinutul ZIP, vă veţi aminti de directorul Configuration_Templates : în aceast director există nişte template –uri de configurare, unde este de ajuns să înlocuieşti câteva lucruri pentru a fi pregătiţi să lucrăm cu baza de date aleasă:

clip_image028

În cazul nostru alegem MSSQL. Conţinutul fişierului după modificările de caz este acesta:

 

   1: <?xml version="1.0" encoding="utf-8" ?>
   2:  
   3:  
   4: <hibernate-configuration xmlns="urn:nhibernate-configuration-2.2">
   5:   <session-factory name="NHibernate.Test">
   6:     <property name="connection.driver_class">NHibernate.Driver.SqlClientDriver</property>
   7:     <property name="connection.connection_string">Server=.\SQLEXPRESS;initial catalog=NHFROMSCRATCH;Integrated Security=SSPI</property>
   8:     <property name="adonet.batch_size">10</property>
   9:     <property name="show_sql">true</property>
  10:     <property name="dialect">NHibernate.Dialect.MsSql2005Dialect</property>
  11:     <property name="use_outer_join">true</property>
  12:     <property name="command_timeout">60</property>
  13:     <property name="query.substitutions">true 1, false 0, yes 'Y', no 'N'</property>
  14:     <property name="proxyfactory.factory_class">NHibernate.ByteCode.LinFu.ProxyFactoryFactory, NHibernate.ByteCode.LinFu</property>
  15:   </session-factory>
  16: </hibernate-configuration>

 

După cum se vede, modificările substanţiale se afla în proprietatea connection string, şi în show sql, pe care l-am pus true, pentru a vizualiza query-urile în timp ce NHibernate le execută: un truc ce ne va permite nu numai să vedem cum NHibernate face query-urile, dar mai ales câte face, pentru a face performance tuning. Această sesiune de configurare va putea fi pusă în file-ul de configurare a aplicaţiei, sau aproape de fişierul binarului ce o foloseşte într-un fişier numit hibernate.cfg.xml, soluţie aleasă în exemplu.

clip_image030

Să ne amintim că, după ce am introdus fişierul Hibernate.cfg.xml în proiect, trebuie să setăm flag-ul “copy if newer” astfel fiind copiat automat în directorul de ieşire.

Să vedem acum cum este făcut testul “CanCreateDatabase”:

 

   1: [Test]
   2: public void CanCreatedatabse()
   3: {
   4:    SchemaExport export = new SchemaExport(CreateConfiguration());
   5:    export.Execute(true, true, false);
   6: }

 

 

 

Folosim clasa Helper SchemaExport: această clasă, pornind de la configurarea curentă, se ocupă cu crearea schemei database-ului. Acum lansând testul, acesta se comportă mai bine, iar în partea console putem vedea DLL-ul care a fost trimis la database.

clip_image032

Prin flag-ul funcţiei Export, putem supune script-ul la sistemul database de mai jos, în cazul nostru vom obţine în BD singurul tabel al minusculului nostru model de test:

clip_image034

Deci să facem o scurtă recapitulare până la acest punct: cu numai un fişier (cel de mapping) am creat un database şi codul c#. Şi totul graţie hbm2net-ului, ce este un codegenerator open source ce se poate recupera de la NHContrib. Cu puţină răbdare putem modifica şi/sau scrie template adiţionali pentru hbm2net pentru a genera alte tipuri de artefacts, de exemplu DTO sau altele, dar asta vom vedea mai târziu. Practic putem vedea hbm2net ca un generator de cod programabil, care din default produce codul entităţilor pornind de la fişierul hbm.

clip_image036

Acum putem scrie codul de probă pentru model. Dând valoare show-sql –ului din configurare „true”, NH trimite în standard output query-urile, deci la momentul debug-ului este posibil să inspecţionăm ce se întâmplă după culise. Simplul nostru unit test creează/modifică/anulează o simplă instanţă a entităţii customer; iată codul unit testului care face toate acestea :

   1: namespace NHFromScratch.Tests
   2: {
   3:     [TestFixture]
   4:     public class TestCustomer
   5:     {
   6:         static TestCustomer()
   7:         {
   8:             ConfigureLogForNet();
   9:         }
  10:  
  11:         ISessionFactory sessionFactory;
  12:         [SetUp]
  13:         public void Setup()
  14:         {
  15:             
  16:             CreateSessionFactory();
  17:         }
  18:  
  19:         [Test]
  20:         public void CanCreatedatabse()
  21:         {
  22:             SchemaExport export = new SchemaExport(CreateConfiguration());
  23:             export.Execute(true, true, false);
  24:         }
  25:  
  26:         [Test]
  27:         public void CanPersistCustomer()
  28:         {
  29:             Console.WriteLine("\n****** SAVE A CUSTOMER ********");
  30:             //save a customer
  31:             using (var session = sessionFactory.OpenSession())
  32:             using(var transaction = session.BeginTransaction())
  33:             {
  34:  
  35:                 Customer c = new Customer()
  36:                 {
  37:                     Active = true
  38:                      ,
  39:                     AddressLine1 = "xxxx"
  40:                      ,
  41:                     City = "Cuneo"
  42:                      ,
  43:                     Name = "Bill Gates"
  44:                      ,
  45:                     ZipCode = "12060"
  46:                 };
  47:                 session.Save(c);
  48:                 transaction.Commit();
  49:             }
  50:             Console.WriteLine("\n****** RETRIEVE A CUSTOMER ********");
  51:             //retrieve a customer
  52:             using (var session = sessionFactory.OpenSession())
  53:             
  54:             {
  55:  
  56:                 var customer = session.CreateCriteria<Customer>()
  57:                     .Add(Expression.Eq("Name", "Bill Gates"))
  58:                     .UniqueResult();
  59:  
  60:                 Assert.NotNull(customer);
  61:             }
  62:  
  63:             Console.WriteLine("\n****** MODIFY A CUSTOMER ********");
  64:             //modify a customer
  65:             using (var session = sessionFactory.OpenSession())
  66:             using (var transaction = session.BeginTransaction())
  67:             {
  68:  
  69:                 var customer = session.CreateCriteria<Customer>()
  70:                     .Add(Expression.Eq("Name", "Bill Gates"))
  71:                     .UniqueResult<Customer>();
  72:                 customer.ZipCode = "0000";
  73:                 Assert.NotNull(customer);
  74:                 
  75:                 transaction.Commit();
  76:             }
  77:  
  78:  
  79:             Console.WriteLine("\n****** VERIFY CUSTOMER IS MODIFIED ********");
  80:             //verify mod
  81:             using (var session = sessionFactory.OpenSession())
  82:             {
  83:  
  84:                 var customer = session.CreateCriteria<Customer>()
  85:                     .Add(Expression.Eq("Name", "Bill Gates"))
  86:                     .UniqueResult<Customer>();
  87:                 
  88:                 Assert.NotNull(customer);
  89:                 Assert.AreEqual("0000", customer.ZipCode);
  90:             }
  91:  
  92:  
  93:             Console.WriteLine("\n****** DELETE A CUSTOMER ********");
  94:             //delete a customer
  95:             using (var session = sessionFactory.OpenSession())
  96:             using (var transaction = session.BeginTransaction())
  97:             {
  98:  
  99:                 var customer = session.CreateCriteria<Customer>()
 100:                     .Add(Expression.Eq("Name", "Bill Gates"))
 101:                     .UniqueResult();
 102:                 session.Delete(customer);
 103:                 transaction.Commit();
 104:             }
 105:             Console.WriteLine("\n****** VERIFY CUSTOMER IS DELETED ********");
 106:             //verify delete
 107:             using (var session = sessionFactory.OpenSession())
 108:             
 109:             {
 110:  
 111:                 var customer = session.CreateCriteria<Customer>()
 112:                     .Add(Expression.Eq("Name", "Bill Gates"))
 113:                     .UniqueResult();
 114:                 Assert.IsNull(customer);
 115:             }
 116:         }
 117:  
 118:  
 119:  
 120:         private static void ConfigureLogForNet()
 121:         {
 122:             TraceAppender app = new TraceAppender();
 123:             app.Layout = new SimpleLayout();
 124:             //BasicConfigurator.Configure( app);
 125:         }
 126:  
 127:         private void CreateSessionFactory()
 128:         {
 129:             Configuration cfg = CreateConfiguration();
 130:             sessionFactory = cfg.BuildSessionFactory();
 131:         }
 132:  
 133:         private static Configuration CreateConfiguration()
 134:         {
 135:             Configuration cfg = new Configuration();
 136:             cfg.Configure();
 137:             // implicitamente carichiamo tutti i mapping che si trovano nell'assembly che
 138:             // contiene customer
 139:             cfg.AddAssembly(typeof(Customer).Assembly);
 140:             return cfg;
 141:         }
 142:  
 143:  
 144:     }
 145: }

 

 

Şi iată un screenshots despre cum NH operează din spatele culiselor:

clip_image038

În următoarea parte vom vedea mai bine detaliile fişierului mapping şi tag-ul <many-to-one> împreună cu <bag>.

Descarca Sursa

Prima parte

Partea a 3-a

Saturday, 25 September 2010 09:04:54 (GMT Daylight Time, UTC+01:00)  #    Comments [0] - Trackback
hbm2net | NHibernate | NHibernate Tutorial

Note: For visitors of your site, this entry is only displayed for users with the preselected language Romanian/română (ro)

 

Mergi la partea a 2-a: Crearea unui proiect

Mi-a venit ideea de a produce o serie de post-uri ce va permite acelora care vor să înceapă să lucreze cu NHibernate, sau cel puţin să încerce să vadă dacă acest OR/M reprezintă alegerea potrivită, să creeze în mod ordonat un cod curat, cunoscând bine de ce este nevoie, evitând parcursul „încurcat” care adesea se face la început.

Iniţial trebuie să descărcăm NHibernate. Locul potrivit pentru a face acest lucru este proiectul sourceforge, iar modul potrivit pentru a o face este procurarea fişierelor binary necesare. Mergând la linkul de download, veţi vedea ceva de genul:

clip_image002

După cum vedeţi există mai multe versiuni şi un release, ce în acest caz este reprezentat de 3.0.0 Alpha2. Eu am încercuit versiunea 2.1.2 GA pentru că este aceea pe care o vom folosi pentru această serie de articole, dar am încercuit şi GA: “general availability”. Deci, de ce nu am descărcat ultima versiune? Ce înseamnă GA ? NHibernate are o politică de predare bine consolidată şi marchează cu GA versiunile de „producţie”. Deci dacă nu cunoaşteţi anumite funcţii care există numai în alfa „de serviciu”, dacă nu vreţi să vă fie schimbate sub nas funcţii si comportamente, descărcaţi cele mai recente versiuni GA pe care le găsiţi. Pentru moment nu există „Setup”-uri ale NHibernate, cred că au existat câteva în trecut, dar oricum chiar dacă găsiţi ceva, sugestia mea este să le evitaţi: NH vă permite să faceţi un private deploy, iar acest lucru ne va fi folositor în faza de predare a aplicaţiei pe maşinile de producţie. Să descărcăm deci fişierul NHIbernate-2.1.2.GA-bin.zip. Odată descărcat, layout-ul conţinutului este schematizat mai jos:

clip_image004

În root, pe lângă ceva fleacuri, găsim două foldere importante de care avem nevoie pentru a pune la treabă NHibernate: Required_Bins, and Required_For_LazyLoading. Toate dll-urile conţinute în fişierul required bin trebuie să fie eliberate împreună cu aplicaţia (pe lângă faptul că este necesar în fază de programare J ), în timp ce din directorul Required_For_LazyLoading se poate alege folder-ul de distribuit în baza unui proxy generator ales. Acesta este un subiect pe care îl vom discuta mai târziu, pentru moment e de ajuns să ştim că NHibernate poate deriva în interior câteva dintre clasele noastre când hotărâm să avem entităţi „Lazy”, iar acest lucru se obţine prin librării externe de generare de clase „proxy”. Tipul de proxy generator ce trebuie folosit depinde de configurare, şi de aceeaşi configurare depinde şi ce folder distribuim. În exemple vom folosi LinFu. Să spunem că această alegere are impact scăzut în viaţa proiectului vostru, mai puţin în anumite situaţii particulare, deci pentru moment putem continua.

În fază de dezvoltare sunt foarte importante fişierele nhibernate-mapping.xsd si nhibernate-configuration.xsd. Aceste două fişiere vă permit să aveţi intellisens-ul abilitat pentru fișierele de mapping şi pentru configurare şi deci sunt absolut indispensabile. Deşi au ajuns în directorul required_bin, practic sunt inutile în fază de distribuţie. Pentru a activa intellisense-ul trebuie să copiaţi cele două fişiere de mai sus în directorul cuvenit din visual studio:

clip_image006

Atenţie:se poate întâmpla ca dintr-o dată Visual Studio să nu vă mai furnizeze intellisens-ul: acest lucru se întâmplă adesea deoarece există mai multe versiuni ale aceleiaşi scheme XML asociate. Aruncați o privire la acest post, unde se explică cum se poate rezolva problema.

În plus, pentru a programa şi pentru a urma exemplele în acest tutorial va fi nevoie de NUnit. Dacă îl aveţi deja, folosiţi-l, dacă nu îl aveţi puteţi recupera ultima versiune de pe site-ul oficial. Eu de obicei descarc binarul, ci nu setup-ul, şi îl decompactez sub fişierul programe. Şi NUnit funcţionează cu simplul deploy Xcopy.

Ultimul lucru înainte de a începe: când lucraţi cu NHibernate există câteva activităţi repetitive care ar fi bine să le evitaţi. Vă sfătuiesc să descărcaţi tool-ul hbm2net ce ne va permite generarea automată a claselor în cursul tutorial. Tool-ul se află în proiectul NHContrib. Descărcaţi şi decompactaţi fişierul zip într-un folder, de exemplu în C:\hbm2net. Tool-ul este un compilator linie de comandă şi va fi lansat automat de Visual Studio, dar pentru ca acest lucru să se întâmple trebuie să adăugaţi directorul unde aţi pus hbm2net în variabila de mediu PATH. Pentru a face acest lucru mergeţi la „Caracteristicile Sistemului”:

clip_image008

iar apoi adăugaţi path-ul de mai sus variabilei PATH:

clip_image010

În Partea a 2-a vom vedea cum se organizează un proiect cu NH.

Saturday, 25 September 2010 08:12:24 (GMT Daylight Time, UTC+01:00)  #    Comments [0] - Trackback
hbm2net | NHibernate | NHibernate Tutorial

# Saturday, 18 September 2010

continua da Parte3: <many-to-one/>

Sempre continuando a lavorare sul sistema ordini minimale che stiamo analizzando, ci potrebbe tornare utile sapere quali ordini sono associati ad un particolare Customer. Lasciando così come è il modello, possiamo già farlo, sia tramite HQL che tramite ICriteria. Ricordiamo che questo è il corso NHibernate da *ZERO* per cui non si usa LINQ to NH, ne qualsiasi altra API esterna ad NHIbernate, non perchè queste siano o meno efficaci, solo perchè si vuole analizzare il comportamento base. Molto semplicemente facciamo i due nuovi unit test, il primo con HQL:

   1: [Test]
   2: public void QueryCustomerOrdersWithHQL()
   3: {
   4:    CreateSomeOrdersAndCustomers();
   5:    using (ISession session = sessionFactory.OpenSession())
   6:    {
   7:        var query = session.CreateQuery("from Order o join fetch o.Customer where o.Customer.Name=:customer")
   8:            .SetParameter("customer","Customer A");
   9:        foreach (NHFromScratch.Entities.Order order in query.List<NHFromScratch.Entities.Order>())
  10:        {
  11:            Console.WriteLine("Order:{0} Customer:{1}", order.OrderCode, order.Customer.Name);
  12:        }
  13:    }
  14: }

Che lanciato produce il seguente output a console:

 

   1: NHibernate: select order0_.Id as Id3_0_, customer1_.Id as Id2_1_, order0_.OrderCode as OrderCode3_0_, order0_.OrderDate as OrderDate3_0_, order0_.DueDate as DueDate3_0_, order0_.CustomerId as CustomerId3_0_, customer1_.Name as Name2_1_, customer1_.AddressLine1 as AddressL3_2_1_, customer1_.AddressLine2 as AddressL4_2_1_, customer1_.City as City2_1_, customer1_.ZipCode as ZipCode2_1_, customer1_.Active as Active2_1_ from Orders order0_ inner join Customers customer1_ on order0_.CustomerId=customer1_.Id, Customers customer2_ where order0_.CustomerId=customer2_.Id and customer2_.Name=@p0;@p0 = 'Customer A'
   2: Order:ORD00000 Customer:Customer A
   3: Order:ORD00001 Customer:Customer A

Notiamo che nell’ HQL abbiamo comunque specificato il join fetch sull’associazione o.Customer: questo sempre per evitare l’antipattern select N+1.

Lo stesso risultato lo possiamo ottenere con la API ICriteria, ecco lo unit test:

   1: [Test]
   2: public void QueryCustomerOrdersWithCriteria()
   3: {
   4:     CreateSomeOrdersAndCustomers();
   5:     using (ISession session = sessionFactory.OpenSession())
   6:     {
   7:         var criteria = session.CreateCriteria<NHFromScratch.Entities.Order>()
   8:             .CreateCriteria("Customer")
   9:             .Add(Expression.Eq("Name","Customer A")
  10:             );
  11:  
  12:         foreach (var order in criteria.List<NHFromScratch.Entities.Order>())
  13:         {
  14:             Console.WriteLine("Order:{0} Customer:{1}", order.OrderCode, order.Customer.Name);
  15:         }
  16:     }
  17: }

Questo produce l’output di trace seguente:

   1: NHibernate: SELECT this_.Id as Id9_1_, this_.OrderCode as OrderCode9_1_, this_.OrderDate as OrderDate9_1_, this_.DueDate as DueDate9_1_, this_.CustomerId as CustomerId9_1_, customer1_.Id as Id8_0_, customer1_.Name as Name8_0_, customer1_.AddressLine1 as AddressL3_8_0_, customer1_.AddressLine2 as AddressL4_8_0_, customer1_.City as City8_0_, customer1_.ZipCode as ZipCode8_0_, customer1_.Active as Active8_0_ FROM Orders this_ inner join Customers customer1_ on this_.CustomerId=customer1_.Id WHERE customer1_.Name = @p0;@p0 = 'Customer A'
   2: Order:ORD00000 Customer:Customer A
   3: Order:ORD00001 Customer:Customer A

Quindi il problema potrebbe dirsi già risolto… sfortunatamente in un modo non troppo object oriented. Meglio sarebbe avere una collezione di ordini facilmente accessibile dall’oggetto customer, qualcosa tipo customer.Orders. Per fare questo ci viene incontro il più semplice tag di associazione per esprimere una collezione di entità collegate:

<bag/>

Questo rende dal punto di vista object oriented ciò che nel database esiste già in forma implicita. Se nell’algebra relazionale una FK esprime di per sè una relazione bidirezionale, nell’ambito oggetti questa relazione va esplicitata. Per cui diventa necessario modificare il mapping, con la prima semplice aggiunta nella classe customer di un <bag/> di Ordini:

   1: <hibernate-mapping xmlns="urn:nhibernate-mapping-2.2" namespace="NHFromScratch.Entities" 
   2:                                                       assembly="NHFromScratch.Entities">
   3:   <class name="Customer" table="Customers">
   4:     <id name="Id" type="Int32">
   5:       <generator class="native"/>
   6:     </id>
   7:     <property name="Name" type="String" not-null="true"/>
   8:     <property name="AddressLine1" type="String" not-null="true"/>
   9:     <property name="AddressLine2" type="String" />
  10:     <property name="City" type="String" not-null="true"/>
  11:     <property name="ZipCode" type="String" not-null="true"/>
  12:     <property name="Active" type="Boolean" not-null="true"/>
  13:     <bag name="Orders">
  14:       <key column="CustomerId"/>
  15:       <one-to-many class="Order"/>
  16:     </bag> 
  17:   </class>
  18:   
  19: </hibernate-mapping>

Facendo un rebuild della solution, otteniamo un diagramma di classi per le nostre entità come il seguente:

image

Che di fatto ci mostra la relazione bidirezionale tra il cliente e i suoi ordini. Vediamo con un altro unit test come possiamo lavorare con un customer ed ottenere gli ordini a lui collegati:

   1: [Test]
   2: public void QueryCustomerOrdersWithBag()
   3: {
   4:     CreateSomeOrdersAndCustomers();
   5:     using (ISession session = sessionFactory.OpenSession())
   6:     {
   7:         var customerA = session.CreateCriteria<Customer>()
   8:             .Add(Expression.Eq("Name", "Customer A")
   9:             ).UniqueResult<Customer>();
  10:         Console.WriteLine("Obtained customer instance");
  11:         foreach (var order in customerA.Orders)
  12:         {
  13:             Console.WriteLine("Order:{0} Customer:{1}", order.OrderCode, order.Customer.Name);
  14:         }
  15:     }
  16: }

Questo produce il seguente output a console:

   1: Obtained customer instance
   2: NHibernate: SELECT orders0_.CustomerId as CustomerId1_, orders0_.Id as Id1_, orders0_.Id as Id3_0_, orders0_.OrderCode as OrderCode3_0_, orders0_.OrderDate as OrderDate3_0_, orders0_.DueDate as DueDate3_0_, orders0_.CustomerId as CustomerId3_0_ FROM Orders orders0_ WHERE orders0_.CustomerId=@p0;@p0 = 1
   3: Order:ORD00000 Customer:Customer A
   4: Order:ORD00001 Customer:Customer A

Come si vede, la collezione è stata recuperata dallo storage solo quando necessaria. Anche sulla collection abbiamo un comportamento “lazy”, che è opportuno in molti casi, ma non sempre. Possiamo anche qui modificare il comportamento del lazy specificandolo nel mapping:

 
   1: <bag name="Orders" fetch="join">
   2:   <key column="CustomerId"/>
   3:   <one-to-many class="Order"/>
   4: </bag> 
Facendo rigirare il test di prima otteniamo un output leggermente diverso:
   1: Obtained customer instance
   2: Order:ORD00000 Customer:Customer A
   3: Order:ORD00001 Customer:Customer A

Nessuna query: In effetti durante il caricamento del customer è stata risolta anche l’associazione alla collection di ordini, la query è stata difatti la seguente:

   1: SELECT this_.Id ... orders2_.CustomerId as CustomerId3_... FROM Customers this_ left outer join Orders orders2_ on this_.Id=orders2_.CustomerId WHERE this_.Name = @p0;@p0 = 'Customer A'

In tutti i casi il bag che ci arriva è una lista potenzialmente disordinata. Possiamo chiedere ad NH di fare, o meglio di chiedere al DB di fare, un ordinamento per una particolare proprietà dell’entity collezionata. Nel nostro esempio potremmo volere un ordinamento per data di ordine, aggiungendo al mapping questo attributo:

   1: <bag name="Orders" fetch="join" order-by="OrderDate" >

ed ottenendo quindi la interrogazione qui di seguito:

1: SELECTFROM Customers this_ left outer join Orders orders2_ on this_.Id=orders2_.CustomerId WHERE this_.Name = @p0

ORDER BY orders2_.OrderDate;

@p0 = 'Customer A'

Con l’opportuna clausola id ordinamento. Vediamo ora cosa succede facendo dei test con HQL. Il primo:

   1: [Test]
   2: public void QueryCustomerOrdersWithBagUsingHQL()
   3: {
   4:    CreateSomeOrdersAndCustomers();
   5:    using (ISession session = sessionFactory.OpenSession())
   6:    {
   7:        IQuery query = session.CreateQuery("from Customer c where c.Name=:name")
   8:            .SetParameter("name", "Customer A");
   9:        
  10:        var list = query.List<Customer>();
  11:        Assert.AreEqual(1, list.Count);
  12:        var customerA = list[0];
  13:        foreach (var order in customerA.Orders)
  14:        {
  15:            Console.WriteLine("Order:{0} Customer:{1}", order.OrderCode, order.Customer.Name);
  16:        }
  17:    }
  18: }
 
Vediamo che il numero di query ritorna ad un comportamento lazy:
 
   1: select customer0_.Id as Id2_, customer0_.Name as Name2_, customer0_.AddressLine1 as AddressL3_2_, customer0_.AddressLine2 as AddressL4_2_, customer0_.City as City2_, customer0_.ZipCode as ZipCode2_, customer0_.Active as Active2_ from Customers customer0_ where customer0_.Name=@p0;@p0 = 'Customer A'
   2: SELECT orders0_.CustomerId as CustomerId1_, orders0_.Id as Id1_, orders0_.Id as Id3_0_, orders0_.OrderCode as OrderCode3_0_, orders0_.OrderDate as OrderDate3_0_, orders0_.DueDate as DueDate3_0_, orders0_.CustomerId as CustomerId3_0_ FROM Orders orders0_ WHERE orders0_.CustomerId=@p0 ORDER BY orders0_.OrderDate;@p0 = 1
   3: Order:ORD00000 Customer:Customer A
   4: Order:ORD00001 Customer:Customer A

Tuttavia anche qui possiamo specificare in HQL la politica di fetch, modificando l’HQL in: “from Customer c join fetch c.Orders where c.Name=:name”. Sfortunatamente il test fallisce miseramente:

image

Questo è un comportamento “strano” di NH. In effetti c’è un solo Customer nei nostri test che soddisfa la relazione richiesta, tuttavia effettuando la join i record restituiti sono più di uno ( in effetti n, dove n è il numero degli ordini ). Va detto che NH non crea istanze diverse degli oggetti, ma di fatto mette nella lista di output delle reference alla stessa entità.

Si può ovviare a questo problema con un “ResultTransformer”, ovvero un post-processor dei risultati di NH. Possiamo costruire dei post processor ( IResultTransformer ) per manipolare in modo che riteniamo opportuno i risultati di un ICriteria o di un HQL. NH fornisce dei transformer di utilità, uno dei quali ci torna utile in questo frangente:

   1: IQuery query = session.CreateQuery("from Customer c join fetch c.Orders where c.Name=:name")
   2:     .SetParameter("name", "Customer A")
   3:     .SetResultTransformer(Transformers.DistinctRootEntity)
   4:     ;

Questa trasformazione fa collassare le entità “identiche” in una sola, ed ecco che le righe ritornate sono quelle aspettate, e la query diventa una sola, avendo specificato il comportamento non lazy.

Cosa succede se proviamo a rimuovere degli ordini dal cliente? ( attenzione, nella realtà questo non accadrà quasi mai: ci saranno probabilmente delle strategie di soft-delete, qui stiamo facendo degli esempi ) Ecco un unit test per provare questo scenario:

   1: [Test]
   2: public void RemoveOrderForCustomers()
   3: {
   4:     CreateSomeOrdersAndCustomers();
   5:     using (ISession session = sessionFactory.OpenSession())
   6:     using(ITransaction trns = session.BeginTransaction() )
   7:     {
   8:         var customerA = session.CreateCriteria<Customer>()
   9:            .Add(Expression.Eq("Name", "Customer A")
  10:            ).UniqueResult<Customer>();
  11:         Console.WriteLine("Obtained customer instance");
  12:         customerA.Orders.RemoveAt(0);
  13:         trns.Commit();
  14:     }
  15: }

 

Se lanciamo questo test, otteniamo il seguente, famigerato errore:

   1: NHFromScratch.Tests.TestOrders.RemoveOrderForCustomers:
   2: NHibernate.Exceptions.GenericADOException : could not delete collection rows: [NHFromScratch.Entities.Customer.Orders#1][SQL: UPDATE Orders SET CustomerId = null WHERE CustomerId = @p0 AND Id = @p1]
   3:   ---->; System.Data.SqlClient.SqlException : Cannot insert the value NULL into column 'CustomerId', table 'NHFROMSCRATCH.dbo.Orders'; column does not allow nulls. UPDATE fails.
   4: The statement has been terminated.

In pratica NH cerca di “sganciare” l’ordine liberandolo dal Customer, ma noi abbiamo specificato che Customer non è nullabile, quindi il ragionamento è corretto. L’unico modo è dire che l’owner di questa associazione è Customer, inserendo inverse=”true” intendendo in questo modo che questo “lato” dell’associazione non è il responsabile della collezione, in questo modo:

   1: <bag name="Orders" fetch="join" order-by="OrderDate" inverse="true">
   2:   <key column="CustomerId"/>
   3:   <one-to-many class="Order"/>
   4: </bag> 

Sfortunatamente il test continua a non funzionare, stavolta lamenta che nulla è stato cancellato:

image

Quello che possiamo fare è forzare la cancellazione con il paramtero “cascade”:

   1: <bag name="Orders" fetch="join" order-by="OrderDate" inverse="true" cascade="all-delete-orphan">
   2:   <key column="CustomerId"/>
   3:   <one-to-many class="Order"/>
   4: </bag> 

ed ecco finalmente il test funzionante:

image

Il cascade ha effetti simili anche per le operazioni di inserimento di nuovi ordini: essi verranno salvati automaticamente al salvataggio dell’entità Customer.

Prima di concludere questa parte vale la pena ancora dare un occhiata ad un comportamento di cui è utile essere a conoscenza per problematiche di ottimizzazione. Supponiamo di voler sapere quanti ordini ha un Customer, e di avere la associazione in modalità lazy:

   1: [Test]
   2:    public void OrdersCount()
   3:    {
   4:        CreateSomeOrdersAndCustomers();
   5:        using (ISession session = sessionFactory.OpenSession())
   6:        {
   7:            var customerA = session.CreateCriteria<Customer>()
   8:               .Add(Expression.Eq("Name", "Customer A")
   9:               ).UniqueResult<Customer>();
  10:            Console.WriteLine("Obtained customer instance");
  11:            Assert.AreEqual(2, customerA.Orders.Count);
  12:        }
  13:       
  14: }
Se andiamo a vedere le query, notiamo che per contare gli elementi della collezione Orders, NH la riempie. Questo potrebbe andare a discapito delle performance se ci interessa il comportamento lazy della collection, ecco quindi che ci viene incontro l’attributo lazy=”extra”:
Ecco il nuovo mapping:
   1: <bag name="Orders" lazy="extra" order-by="OrderDate" inverse="true" cascade="all-delete-orphan">
   2:   <key column="CustomerId"/>
   3:   <one-to-many class="Order"/>
   4: </bag> 

ed ecco la pulitissima query che viene eseguita:

NHibernate: SELECT count(Id) FROM Orders WHERE CustomerId=@p0;@p0 = 1

Bene, anche questa parte è finita, non tutte le possibili combinazioni di attributi per <bag/> sono state trattate, ma è stato posto l’accento su alcuni problemi classici, il minimo per partire e sperimentare per proprio conto. Ultime due cose:

  • Fino ad ora la equivalenza classe-tabella si è sempre manifestata, ma non sarà sempre così.
  • Guardiamo alle query che genera NH inizialmente per capire cosa succede, ma sempre di più dobbiamo imparare a dare per scontato il comportamento e guardare le query per motivi di ottimizzazione.
Download Sorgenti
Parte 3
Parte 5
Saturday, 18 September 2010 22:24:26 (GMT Daylight Time, UTC+01:00)  #    Comments [1] - Trackback
NHibernate | NHibernate Tutorial

# Saturday, 11 September 2010

continua da Parte2: Creare un progetto

Quando una entità del nostro modello ha una proprietà del tipo di una altra entità del dominio, cioè ha un riferimento ad una altra entità, possiamo esplicitare questo concetto nel file di mapping con il tag <many-to-one/>. Continuando ad arricchire il modello iniziato nella Parte2, decidiamo di inserire l’entità ordine, e poichè un ordine ha necessariamente un cliente, potremo vedere come esplicitare questo concetto. Nella vita l’entità ordine sarà più complessa, ma per il momento ci accontentiamo di vedere la relazione tra l’ordine e chi lo ha fatto.

Aggiungiamo un file di mapping così fatto:

   1: <?xml version="1.0" encoding="utf-8" ?>
   2: <hibernate-mapping xmlns="urn:nhibernate-mapping-2.2" namespace="NHFromScratch.Entities" 
   3:                                                       assembly="NHFromScratch.Entities">
   4:   <class name="Order" table="Orders">
   5:     <id name="Id" type="Int32">
   6:       <generator class="native"/>
   7:     </id>
   8:     <property name="OrderCode" type="AnsiString" />
   9:     <property name="OrderDate" type="DateTime"/>
  10:     <property name="DueDate" type="DateTime"/>
  11:     <many-to-one name="Customer" class="Customer" column="CustomerId"/>
  12:     <!-- details comes here -->
  13:   </class>
  14:   
  15: </hibernate-mapping>

Come si vede abbiamo aggiunto un paio di proprietà di tipo data, ma importante novità c’è il tag <many-to-one/> che ci dice in questa minimale definizione:

  • Che nome ha la proprietà con l’attributo “name”
  • Qual’è l’entità esterna,cioè il suo tipo ( l atabella non ci interessa più )
  • Opzionalmente il nome della colonna ( in pratica il nome della colonna che sarà una foreign-key nella tabell aorder VS la tabella customer ) con l’attributo “column”.

Se non esplicitassi column, il nome sarebbe lo stesso della proprietà. Per inciso: column applica anche alle proprietà semplici, con la medesima semantica.

Bene, ora ricordiamoci di mettere “embedded resource” al nuovo file hbm, che chiameremo fantasiosamente Order.hbm.xml, lanciamo la compilazione per ottenere una prima versione del file .cs order.generated.cs, che aggiungeremo al progetto e compileremo.

s1

Fatto questo possiamo lanciare gli unit test, e ricreare il database:

s2

Il test “vira al verde” e vediamo come la tabella order compaia nello script DDL, e come la foreign key che punta al Customer sia inserita opportunamente nella tabella order. I più attenti poi abvranno notato come la proprietà code sia stata definita come VARCHAR, mentre le altre colonne stringa nella tabella customer siano invece NVARCHAR, questo è provocato dalla scelta di utilizzare come tipo per la stringa OrderCode AnsiString anzichè string. Questo non ha nessun effetto nel codice C#, ma cambia la DDL del database, quindi scegliete la più opportuna. Altra cosa importante, la FK è nullabile, questo non è molto bello poichè abbiamo detto che obbligatoriamente un Order deve avere un Customer associato: basta usare l’attributo not-null = “true” aggiunto al tag <many-to-one/>.

   1: <many-to-one name="Customer" class="Customer" column="CustomerId" not-null="true"/>

Questo modificherà la DDL come qui di seguito:

   1: create table Orders (
   2:        Id INT IDENTITY NOT NULL,
   3:       OrderCode VARCHAR(255) null,
   4:       OrderDate DATETIME null,
   5:       DueDate DATETIME null,
   6:       CustomerId INT not null,
   7:       primary key (Id)
   8:    )

 

Alla fine il nostro nuovo DB avrà questo schema:

Senza nome

A questo punto possiamo di creare qualche test, per provare il modello come funziona. Diciamo che vogliamo creare dei test ripetibili, quindi la nostra TestFixture ricreerà tutte le volte un DB da zero, così ogni test sarà indipendente. Il primo test interessante potrebbe essere la creazione di un ordine, con contestualmente un nuovo cliente. Questa volta vedremo meglio il dettaglio delle varie operazioni che andremo a fare. Ecco il codice del test che proveremo a fare:

 

 

   1: public void CreateAnOrderWithANewCustomer()
   2: {
   3:     using(ISession session = sessionFactory.OpenSession() )
   4:     using (ITransaction transaction = session.BeginTransaction())
   5:     {
   6:         Order order = new Order();
   7:         order.Customer = new Customer()
   8:         {
   9:             Active = true
  10:             ,
  11:             AddressLine1 = "xxxxxxx xxx xxxx"
  12:             ,
  13:             AddressLine2 = "xx xxxxxxx xxxxx"
  14:             ,
  15:             City = "Milano"
  16:             ,
  17:             Name = "Customer Name"
  18:             ,
  19:             ZipCode = "00000"
  20:         };
  21:         order.OrderCode="ORD00000";
  22:         order.OrderDate = DateTime.Now;
  23:         order.DueDate = order.OrderDate.Value.AddMonths(2);
  24:         session.SaveOrUpdate(order);
  25:         transaction.Commit();
  26:     }
  27: }

Come prima cosa creiamo una “Sessione”, qualcosa che assomiglia ad una connessione al DB, ma è qualcosa di più, è un unità di lavoro. All’interno di un unityà di lavoro le entità diventano degne di questo nome ed NHibernate ne traccia le modifiche e si occupa di garantirne l’unicità a parità di identificativo DB, e tante altre cose. Diciamo qualche buona norma sulla session:

  • Costa poco creare una session ( diversamente dal Session Factory )
  • Cercate di far durare poco le sessioni da un punto di vista temporale
  • Limitate il numero di entità con cui lavora una sessione
  • La session non è multithread.

Immediatamente dopo abbiamo aperto una transazione, perchè andremo alla fine a persistere qualcosa di nuovo nel database, e vogliamo fare tutte le operazioni nella nostra unità di lavoro ( UOW = unit of work ) in modo consistente ed atomico. In una sola UOW è possibile avere più transazioni.

Lanciamo il test e vediamno cosa succede nel text output di NUnit, per vedere cosa fa NH sul DB. Sfortunatamente il test “vira al rosso”:

s3

 

Questo è il famigerato errore:

“NHFromScratch.Tests.TestOrders.CreateAnOrderWithANewCustomer:
NHibernate.PropertyValueException : not-null property references a null or transient valueNHFromScratch.Entities.Order.Customer”

Significa in pratica che stiamo cercando di salvare un entità ( Order ) che ha una referenza ad un’altra entità, ma quest’ultima non è persistente ( cioè transitoria=Transient ). Per risolvere questo problema abbiamo due strade:

Salvare l’entità riferita ( se quest’ultima è ancora transitoria ) prima di salvare l’entità ordine:

   1: session.SaveOrUpdate(order.Customer);
   2: session.SaveOrUpdate(order);
   3: transaction.Commit();

Questo fa diventare il test “verde”, e queste sono le query che sono inviate al DB:

   1: ***** NHFromScratch.Tests.TestOrders.CreateAnOrderWithANewCustomer
   2: NHibernate: INSERT INTO Customers (Name, AddressLine1, AddressLine2, City, ZipCode, Active) VALUES (@p0, @p1, @p2, @p3, @p4, @p5); select SCOPE_IDENTITY();@p0 = 'Customer Name', @p1 = 'xxxxxxx xxx xxxx', @p2 = 'xx xxxxxxx xxxxx', @p3 = 'Milano', @p4 = '00000', @p5 = True
   3: NHibernate: INSERT INTO Orders (OrderCode, OrderDate, DueDate, CustomerId) VALUES (@p0, @p1, @p2, @p3); select SCOPE_IDENTITY();@p0 = 'ORD00000', @p1 = 11/09/2010 20.48.24, @p2 = 11/11/2010 20.48.24, @p3 = 1

L’altro modo è quello di agire sull’attributo “cascade” del tag <many-to-one/>, per esempio modificandolo così:

   1: <many-to-one name="Customer" class="Customer" column="CustomerId" not-null="true" cascade="save-update"/>

Nel test possiamo dinuovo limitarci a salvare Order, ci pensa NH, grazie all’attributo “cascade” a fare tutte le considerazioni per mantenere consistente l’operazione sul database.

Adesso ci spostiamo alla parte lettura, creiamo un test con un paio di clienti e qualche ordine, e proviamo a fare delle estrazioni di entità già persistite. Supponiamo di creare una base di entità, in maniera del tutto analoga a quella sopra, creando due clienti, Customer A e Custome B, rispettivamente con 2 ed 1 ordine ciascuno. Il primo test prevede il recupero delle entità persistite tramite la creazione di un ICriteria. Ecco il codice del test:

   1: [Test]
   2: public void QueryWithICRiteria()
   3: {
   4:    CreateSomeOrdersAndCustomers();
   5:    using (ISession session = sessionFactory.OpenSession())
   6:    {
   7:        var criteria = session.CreateCriteria<Order>();
   8:        foreach (Order order in criteria.List<Order>())
   9:        {
  10:            Console.WriteLine("Order:{0} Customer:{1}", order.OrderCode, order.Customer.Name);
  11:        }
  12:    }
  13: }

Come si vede c’è un bel foreach che stampa dei dettagli dell’ordine e del cliente ad esso associato. Se però vediamo le query che fa NH scopriamo un problemino:

nplusone

In pratica, la List forza l’esecuzione della prima query, ma tutte le volte che andiamo ad indagare la proprietà “Customer” ecco che viene sparata una nuova query al DB, per recuperare l’entità riferita. Malgrado NH sia così bravo da non effettuare la query per lo stesso customer 2 volte, quello che accade non è comunque un comportamento ideale. Questo accade perchè l’associazione many-to-one è per default “lazy”: questo può essere utile in tantissimi casi, ma se, come nel nostro caso, sappiamo in anticipo che vogliamo avere tutte le associazioni, stiamo ricadendo nell’ anti patter Select N+1. Fortunatamente possiamo evitarlo, in primo modo agendo sulle proprietà di ICriteria:

   1: var criteria = session.CreateCriteria<Order>()
   2: .SetFetchMode("Customer",FetchMode.Eager)
   3: ;

Cambiare il modo di fetch dell associativa cambia drammaticamente le query eseguite, che si riducono ad una:

   1: SELECT this_.Id as Id3_1_, ... FROM Orders this_ inner join Customers customer2_ on this_.CustomerId=customer2_.Id

Questa è un ottimizzazione che dobbiamo sempre cercare di fare. Potevamo ottenere lo stesso risultato solo aggiungendo l’attributo “fetch” nel tag <many-to-one/>:

   1: <many-to-one name="Customer" class="Customer" column="CustomerId" fetch="join" not-null="true" cascade="save-update"/>

Ovviamente, in uno scenario in cui volessimo cambiare il fetch-mode all’inverso, potremmo impostare via codice il SetFetchMode a select.

Lasciamo ora il fetch mode a join e proviamo a fare la stessa query con HQL:

   1: [Test]
   2: public void QueryWithHql()
   3: {
   4:     CreateSomeOrdersAndCustomers();
   5:     using (ISession session = sessionFactory.OpenSession())
   6:     {
   7:         var query = session.CreateQuery("from Order");
   8:         foreach (Order order in query.List<Order>())
   9:         {
  10:             Console.WriteLine("Order:{0} Customer:{1}", order.OrderCode, order.Customer.Name);
  11:         }
  12:     }
  13: }

Scopriremo che HQL ignora il tipo di fetch specificato dall’attributo fetch di <many-to-one/> ! Questo perchè HQL prevede la possibilità di specificare il fetch mode letteralmente:

   1: var query = session.CreateQuery("from Order o join fetch o.Customer");

Questa modifica ci porta dinuovo all’unica query per tutte e tre le istanze.

Con questo abbiamo finito la definizione di <many-to-one/> e, visto che questo è un corso da *zero* non abbiamo visto uno per uno tutti gli attributi di many-to-one, ma una quantità bastante a iniziare ad usarlo in modo efficace.

Diciamo in ultimo 2 cose:

  1. Mappare una FK di db con il corrispondente tipo chiave è un grave antipattern.
  2. Una FK nel database lega implicitamente 2 tabelle, qui abbiamo specificato uno dei lati dell’associazione, l’altro lato lo vedremo nella prossima parte.

 

Download Sorgenti
Parte 2

Parte 4

Saturday, 11 September 2010 22:36:29 (GMT Daylight Time, UTC+01:00)  #    Comments [0] - Trackback
NHibernate | NHibernate Tutorial

# Friday, 03 September 2010

Meglio, più in generale, non funziona più l’intellisense per qualsiasi documento XML per cui abbiamo uno ( o più.. ) XSD. Questo accade perchè Visual Studio trova più sorgenti per lo schema dichiarato nell’ XML, nel caso NHibernate abbiamo la seguente dichiarazione in testa:

<?xml version="1.0"?>
<hibernate-mapping xmlns="urn:nhibernate-mapping-2.2" assembly="....." namespace="....." >

Se abbiamo nella solution, o nella directory %Program Files%/Microsoft Visual Studio XXX/Xml/Schemas  uno o più file che definiscono lo stesso namespace, in questo caso

<xs:schema targetNamespace="urn:nhibernate-mapping-2.2" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns="urn:nhibernate-mapping-2.2" elementFormDefault="qualified" attributeFormDefault="unqualified">

Visual Studio trova un ambiguità per cui smette di fornire l’intellisense. La situazione si può risolvere facilmente senza disperare:

Basta cliccare sul documento XML da visual studio e dalla finestra proprietà editare la voce Schemas come mostrato qui sotto:

xmlschemas

Così facendo si apre una finestra in cui è possibile abilitare/disabilitare gli schema che visual studio intende usare:

xmlschema2

Deselezionando uno degli schemi ambigui tutto torna a funzionare. Ovviamente questo funziona per qualsiasi ambiguità di schema, non solo per NHibernate.

Friday, 03 September 2010 10:28:31 (GMT Daylight Time, UTC+01:00)  #    Comments [3] - Trackback
NHibernate | Programmin

# Wednesday, 01 September 2010
continua da Parte1: Cosa ci serve

In questa parte costruiamo un progetto con NHibernate che ci consentirà di persistere una prima entità. In effetti la parte OR/M sarà tenuta il più semplice possibile, niente reference o collezioni o componenti: l’obiettivo è creare uno scheletro funzionante e testabile. Ovviamente tutto quanto detto servirà da spunto, alcune cose potranno sembrare ovvie, altre meno, fate come credete meglio, ma poichè questo è un corso “da zero” voglio soffermarmi su tutti i punti.

Decidiamo di mettere le entità in una Class Library ( dll ). Questa è una scelta che probabilmente sarà conveniente fare anche per una soluzione vera da mandare in produzione. Creiamo quindi una nuova solution con Visual Studio. Distinguiamo il nome della soluzione con NHFromScratch.All, ed aggiungiamo all' interno un nuovo progetto di tipo class library, che andiamo a chiamare NHFromScratch.Entities.

t1

All’ interno della cartella radice della solution, creiamo una sottocartella “Lib”, atta a contenere le dipendenze della soluzione ( NHibernate e non ), ad esempio in aggiunta alle dipendenze di NH vedete la nunit.framework.dll, dipendenza che ci servirà a breve quando vorremo testare le nostre entità.

t2

Bene, il passo successivo è creare il mapping per la prima ed unica entità che vogliamo gestire. Ci sono una moltitudine di strategie per creare un mapping per NH, quella che presento qua è una soluzione “plain vanilla”, senza nessun fronzolo e a costo zero: con la pratica ognuno sceglie il metodo che meglio crede, ma per imparare è mia opinione che scrivere il file XML sia di aiuto. Aggiungiamo quindi una sottocartella “Mapping” nella class library appena creata, e in questo folder creiamo un nuovo file con il nome della entità ( Customer nel nostro caso ) con l’estensione .hbm.xml. Se abbiamo abilitato l’intellisense, come fortemente consigliato nella parte 1 l’unico tag che dobbiamo ricordare a memoria è <hibernate-mapping>, tutti gli altri saranno esposti via intellisense dopo che il namespace sarà correttamente specificato, come mostrato qui sotto:

 t4

Cosa importantissima da ricordare, ricordiamo di impostare “Embedded Resource” come build action sul file di mapping, altrimenti… la configurazione di NHibernate non funzionerà. Ad essere precisi questo si potrebbe evitare, ma per farlo dovremmo distribuire i file di mapping insieme all’applicazione, ed è un’opzione che francamente non mi piace e nemmeno ho mai visto praticare.

Fatto questo non rimane che scrivere difatto il mapping. In questo primo test useremo solo tre elementi di mapping: class,key e property, praticamente il minimo indispensabile per avere un entità al lavoro. Approfondiremo i vari aspetti nelle prossime parti.

   1: <?xml version="1.0" encoding="utf-8" ?>
   2: <hibernate-mapping xmlns="urn:nhibernate-mapping-2.2" namespace="NHFromScratch.Entities" 
   3:                                                       assembly="NHFromScratch.Entities">
   4:   <class name="Customer" table="Customers">
   5:     <id name="Id" type="Int32">
   6:       <generator class="native"/>
   7:     </id>
   8:     <property name="Name" type="String" not-null="true"/>
   9:     <property name="AddressLine1" type="String" not-null="true"/>
  10:     <property name="AddressLine2" type="String" />
  11:     <property name="City" type="String" not-null="true"/>
  12:     <property name="ZipCode" type="String" not-null="true"/>
  13:     <property name="Active" type="Boolean" not-null="true"/>
  14:   </class>
  15:   
  16: </hibernate-mapping>
  17:  
  18:  
  19:  

Facciamo tuttavia due parole per dire a grandi linee cosa contiene il mapping della entità Customer. Innanzitutto è specificato il namespace e l’assembly che contiene l’entità mappata. Questo non è obbligatorio, ma ci consente di scrivere in maniera più leggibile il nome della classe più avanti. Il tag class indica appunto la classe che intendiamo utilizzare come entity. Ricordiamo che spesso non esiste un rapporto 1:1 tra tabelle ed entità, ms in questo semplice esempio così accadrà. Il tag class ci permette di definire il nome della classe, e a quale tabella è associata su DB. Il tag più sotto <id> è necessario: NHibernate vuole sapere come il database “distingue” le entità. Ci sono svariati modi per fare questo, e ll modo viene scelto dal sotto-tag generator: in questo esempio abbiamo specificato un generator “native”, che significa id univoco assegnato dal DB ( identity, auto-incrementale ) comunque esso si chiami nel DB target. Successivamente vengono le proprietà, con i loro bei nomi e tipi dato. Noterete che non ho specificato nessun nome per la corrispondente colonna sul DB: semplicemente se  il nome è uguale nessuno ci obbliga a farlo, non ripetiamo noi stessi. Anzi, diciamo anche che il tipo sarebbe un surplus se scrivessimo a mano la classe sottostante: NHibernate usa molto inferire le informazioni ovunque sia possibile. Noi però la classe non abbiamo voglia di farla a mano, per cui ci serviamo di hbm2net ( vedi la parte 1 ). Per scatenare questo tool, usiamo un pre build step di Visual Studio, come quello qui sotto:

t6

in pratica diciamo ad hbm2net di lavorare su tutti i file .hbm.xml che trova nella sottocartella mapping della solution, di mettere gli output ( ovvero le classi ) nella cartella del progetto. Sebbene hbm2net sia un editor template based che può generare un po’ qualsiasi artefatto partendo dall hbm, senza parametri si limita a generare le classi corrispondenti al mapping. Lanciando la build nell’output dovreste vedere il progress anche di hbm2net:

t7

Finita la build, vi trovate il file autogenerato per la entity su file system. Aggiungetelo al progetto.

t8

Date un’occhiata alla classe autogenerata, date un occhiata anche al mapping: se avete provato qualche altro tool per NH noterete che il lavoro fatto finora sembra molto semplificato. Stiamo scrivendo solo lo stretto indispensabile, quello che non ci serve lo lasciamo assolutamente da parte, perchè questo è NHibernate da ZERO.

t9

Bene, per completare la solution aggiungiamo la reference ad NHibernate: NHibernate e Iesi.Collection sono gli unici assembly che dobbiamo referenziare a tempo di build.

E’ finalmente l’ora di andare in test. Aggiungiamo alla solution un progetto di tipo Class Library, in cui inseriremo gli unit test.

t10

La solution di test, in modo simile ad un progetto che deve andare in produzione, necessita delle reference non statiche di NHibernate. Noi le abbiamo nella cartella Lib della solution, dobbiamo spostarli a fianco della dll di produzione. Possiamo scegliere di farlo con un post-build step, come illustrato qui sotto.

t11

Anche le reference di questo progetto sono un po’ più impegnative: ci serve Iesi.Collection & NHibernate come al solito, ma anche il nostro assembly con le entità, log4net per il logging, ed in più, ovvviamente, nunit.framework, va da sè.

t12

A me personalmente piace la barra verde di NUnit, per cui testiamo il progetto con NUnit, mettendo NUnit.exe come “start external program”.

t13

A questo punto creiamo il nostro unit test, con lo scopo di configurare NH, un po’ di log, e vedere se riusciamo a creare il Database. Sì, in questo tutorial il database non lo facciamo a mano, perchè anche se questo è NHibernate da ZERO, vogliamo scrivere il meno codice possibile.

Il codice dello unit test è qui di seguito:

   1: namespace NHFromScratch.Tests
   2: {
   3:     [TestFixture]
   4:     public class TestCustomer
   5:     {
   6:         static TestCustomer()
   7:         {
   8:             ConfigureLogForNet();
   9:         }
  10:  
  11:         ISessionFactory sessionFactory;
  12:         [SetUp]
  13:         public void Setup()
  14:         {
  15:             
  16:             CreateSessionFactory();
  17:         }
  18:  
  19:         [Test]
  20:         public void CanCreatedatabse()
  21:         {
  22:             SchemaExport export = new SchemaExport(CreateConfiguration());
  23:             export.Execute(true, true, false);
  24:         }
  25:  
  26:         [Test]
  27:         public void CanPersistCustomer()
  28:         {
  29:             Console.WriteLine("\n****** SAVE A CUSTOMER ********");
  30:             //save a customer
  31:             using (var session = sessionFactory.OpenSession())
  32:             using(var transaction = session.BeginTransaction())
  33:             {
  34:  
  35:                 Customer c = new Customer()
  36:                 {
  37:                     Active = true
  38:                      ,
  39:                     AddressLine1 = "xxxx"
  40:                      ,
  41:                     City = "Cuneo"
  42:                      ,
  43:                     Name = "Bill Gates"
  44:                      ,
  45:                     ZipCode = "12060"
  46:                 };
  47:                 session.Save(c);
  48:                 transaction.Commit();
  49:             }
  50:             Console.WriteLine("\n****** RETRIEVE A CUSTOMER ********");
  51:             //retrieve a customer
  52:             using (var session = sessionFactory.OpenSession())
  53:             
  54:             {
  55:  
  56:                 var customer = session.CreateCriteria<Customer>()
  57:                     .Add(Expression.Eq("Name", "Bill Gates"))
  58:                     .UniqueResult();
  59:  
  60:                 Assert.NotNull(customer);
  61:             }
  62:  
  63:             Console.WriteLine("\n****** MODIFY A CUSTOMER ********");
  64:             //modify a customer
  65:             using (var session = sessionFactory.OpenSession())
  66:             using (var transaction = session.BeginTransaction())
  67:             {
  68:  
  69:                 var customer = session.CreateCriteria<Customer>()
  70:                     .Add(Expression.Eq("Name", "Bill Gates"))
  71:                     .UniqueResult<Customer>();
  72:                 customer.ZipCode = "0000";
  73:                 Assert.NotNull(customer);
  74:                 
  75:                 transaction.Commit();
  76:             }
  77:  
  78:  
  79:             Console.WriteLine("\n****** VERIFY CUSTOMER IS MODIFIED ********");
  80:             //verify mod
  81:             using (var session = sessionFactory.OpenSession())
  82:             {
  83:  
  84:                 var customer = session.CreateCriteria<Customer>()
  85:                     .Add(Expression.Eq("Name", "Bill Gates"))
  86:                     .UniqueResult<Customer>();
  87:                 
  88:                 Assert.NotNull(customer);
  89:                 Assert.AreEqual("0000", customer.ZipCode);
  90:             }
  91:  
  92:  
  93:             Console.WriteLine("\n****** DELETE A CUSTOMER ********");
  94:             //delete a customer
  95:             using (var session = sessionFactory.OpenSession())
  96:             using (var transaction = session.BeginTransaction())
  97:             {
  98:  
  99:                 var customer = session.CreateCriteria<Customer>()
 100:                     .Add(Expression.Eq("Name", "Bill Gates"))
 101:                     .UniqueResult();
 102:                 session.Delete(customer);
 103:                 transaction.Commit();
 104:             }
 105:             Console.WriteLine("\n****** VERIFY CUSTOMER IS DELETED ********");
 106:             //verify delete
 107:             using (var session = sessionFactory.OpenSession())
 108:             
 109:             {
 110:  
 111:                 var customer = session.CreateCriteria<Customer>()
 112:                     .Add(Expression.Eq("Name", "Bill Gates"))
 113:                     .UniqueResult();
 114:                 Assert.IsNull(customer);
 115:             }
 116:         }
 117:  
 118:  
 119:  
 120:         private static void ConfigureLogForNet()
 121:         {
 122:             TraceAppender app = new TraceAppender();
 123:             app.Layout = new SimpleLayout();
 124:             //BasicConfigurator.Configure( app);
 125:         }
 126:  
 127:         private void CreateSessionFactory()
 128:         {
 129:             Configuration cfg = CreateConfiguration();
 130:             sessionFactory = cfg.BuildSessionFactory();
 131:         }
 132:  
 133:         private static Configuration CreateConfiguration()
 134:         {
 135:             Configuration cfg = new Configuration();
 136:             cfg.Configure();
 137:             // implicitamente carichiamo tutti i mapping che si trovano nell'assembly che
 138:             // contiene customer
 139:             cfg.AddAssembly(typeof(Customer).Assembly);
 140:             return cfg;
 141:         }
 142:  
 143:  
 144:     }
 145: }

Fatto questo lanciamo il test “CanCreateDatabase” e, come probabilmente sospettiamo, andiamo in rosso con il logger che dice qualcosa come qua sotto:

t14

In effetti, ci siamo “dimenticati” di configurare NHibernate, per cui a che DB potrebbe mai puntare ? Come vedete avere il log abilitato lascia pochi dubbi rispetto a quale sia l’errore. Tra l’altro manca persino il database…

Quindi creiamo prima un database vuoto, non serve fare nessuna tabella, lasciamo che sia NHibernate a fare il lavoro sporco

t16

 

Nell’esempio ho creato un database con nome NHFROMSCRATCH sull’SQLEXPRESS locale. Poi tornando alla  , quando si vedeva il contenuto dello ZIP, rocorderete la cartella Configuration_Templates: in questa cartella ci sono appunto dei template di configurazione, in cui basta sostituire un po’ di cose per essere pronti a lavorare con il db scelto:

t15

Nel nostro caso scegliamo MSSQL. Il contenuto del file, dopo le modifiche del caso è questo:

   1: <?xml version="1.0" encoding="utf-8" ?>
   2:  
   3:  
   4: <hibernate-configuration xmlns="urn:nhibernate-configuration-2.2">
   5:   <session-factory name="NHibernate.Test">
   6:     <property name="connection.driver_class">NHibernate.Driver.SqlClientDriver</property>
   7:     <property name="connection.connection_string">Server=.\SQLEXPRESS;initial catalog=NHFROMSCRATCH;Integrated Security=SSPI</property>
   8:     <property name="adonet.batch_size">10</property>
   9:     <property name="show_sql">true</property>
  10:     <property name="dialect">NHibernate.Dialect.MsSql2005Dialect</property>
  11:     <property name="use_outer_join">true</property>
  12:     <property name="command_timeout">60</property>
  13:     <property name="query.substitutions">true 1, false 0, yes 'Y', no 'N'</property>
  14:     <property name="proxyfactory.factory_class">NHibernate.ByteCode.LinFu.ProxyFactoryFactory, NHibernate.ByteCode.LinFu</property>
  15:   </session-factory>
  16: </hibernate-configuration>

 

Come si vede, le modifiche sostanziali sono nella proprietà connection string, e in show_sql, che ho messo a true, per visualizzare le query mentre NH le esegue: un trucco che ci permetterà non tanto di vedere come NHibernate fa le query, ma piuttosto quante ne fa, per fare performance tuning. Questa sessione di configurazione potrà essere messa nel file di configurazione dell’applicazione, o vicino al file binario che la usa in un file di nome hibernate.cfg.xml, soluzione scelta nell’esempio.

t17

Ricordiamoci che, dopo aver inserito il file hibernate.cfg.xml nel progetto, dobbiamo impostare il flag “copy if newer” cosicchè verrà copiato automaticamente nella cartella di uscita.

vediamo ora come è fatto il test “CanCreateDatabase”:

   1: [Test]
   2: public void CanCreatedatabse()
   3: {
   4:    SchemaExport export = new SchemaExport(CreateConfiguration());
   5:    export.Execute(true, true, false);
   6: }

 

Usiamo la classe di Helper SchemaExport: questa classe, partendo dalla Configurazione corrente, si occupa di effettuare la creazione dello schema del database. Ora lanciando il test questi si comporta meglio, e nella porzione console è possibile  vedere la DDL che è stata inviata al database.

 

t18

Tramite il flag della funzione Export, è possibile sottometere lo script al sistema database sottostante, nel nostro caso otterremo su DB l’unica tabella del nostro minuscolo modello di test:

t19

Quindi facciamo un breve recap fino a questo punto: con un solo file ( il mapping appunto ) abbiamo creato un database e il codice c#. Tutto questo grazie ad hbm2net, che è un code generator open source recuoperabile da NHContrib. Con un po’ di pazienza è possibil emodificare e/o scrivere template aggiuntivi per hbm2net per geenrare altri tipidi artefacts, ad esempio DTO o altro, ma questo sarà visto più avanti. In pratica possiamo vedere hbm2net come un generatore di codice programmabile, che di default produce il codice delle entity partendo dai file hbm.

t18b

 

Ora possiamo scrivere il codice di prova per il modello. Mettendo a “true” il valore di show_sql nella configurazione, NH manda in standard output le query, per cui in debug è possibile ispezionare cosa succede dietro le quinte. Il nostro semplice unit test crea/modifica/cancella una semplice istanza della entità customer, ecco il codice dello unit test che fa tutto questo:

   1: namespace NHFromScratch.Tests
   2: {
   3:     [TestFixture]
   4:     public class TestCustomer
   5:     {
   6:         static TestCustomer()
   7:         {
   8:             ConfigureLogForNet();
   9:         }
  10:  
  11:         ISessionFactory sessionFactory;
  12:         [SetUp]
  13:         public void Setup()
  14:         {
  15:             
  16:             CreateSessionFactory();
  17:         }
  18:  
  19:         [Test]
  20:         public void CanCreatedatabse()
  21:         {
  22:             SchemaExport export = new SchemaExport(CreateConfiguration());
  23:             export.Execute(true, true, false);
  24:         }
  25:  
  26:         [Test]
  27:         public void CanPersistCustomer()
  28:         {
  29:             Console.WriteLine("\n****** SAVE A CUSTOMER ********");
  30:             //save a customer
  31:             using (var session = sessionFactory.OpenSession())
  32:             using(var transaction = session.BeginTransaction())
  33:             {
  34:  
  35:                 Customer c = new Customer()
  36:                 {
  37:                     Active = true
  38:                      ,
  39:                     AddressLine1 = "xxxx"
  40:                      ,
  41:                     City = "Cuneo"
  42:                      ,
  43:                     Name = "Bill Gates"
  44:                      ,
  45:                     ZipCode = "12060"
  46:                 };
  47:                 session.Save(c);
  48:                 transaction.Commit();
  49:             }
  50:             Console.WriteLine("\n****** RETRIEVE A CUSTOMER ********");
  51:             //retrieve a customer
  52:             using (var session = sessionFactory.OpenSession())
  53:             
  54:             {
  55:  
  56:                 var customer = session.CreateCriteria<Customer>()
  57:                     .Add(Expression.Eq("Name", "Bill Gates"))
  58:                     .UniqueResult();
  59:  
  60:                 Assert.NotNull(customer);
  61:             }
  62:  
  63:             Console.WriteLine("\n****** MODIFY A CUSTOMER ********");
  64:             //modify a customer
  65:             using (var session = sessionFactory.OpenSession())
  66:             using (var transaction = session.BeginTransaction())
  67:             {
  68:  
  69:                 var customer = session.CreateCriteria<Customer>()
  70:                     .Add(Expression.Eq("Name", "Bill Gates"))
  71:                     .UniqueResult<Customer>();
  72:                 customer.ZipCode = "0000";
  73:                 Assert.NotNull(customer);
  74:                 
  75:                 transaction.Commit();
  76:             }
  77:  
  78:  
  79:             Console.WriteLine("\n****** VERIFY CUSTOMER IS MODIFIED ********");
  80:             //verify mod
  81:             using (var session = sessionFactory.OpenSession())
  82:             {
  83:  
  84:                 var customer = session.CreateCriteria<Customer>()
  85:                     .Add(Expression.Eq("Name", "Bill Gates"))
  86:                     .UniqueResult<Customer>();
  87:                 
  88:                 Assert.NotNull(customer);
  89:                 Assert.AreEqual("0000", customer.ZipCode);
  90:             }
  91:  
  92:  
  93:             Console.WriteLine("\n****** DELETE A CUSTOMER ********");
  94:             //delete a customer
  95:             using (var session = sessionFactory.OpenSession())
  96:             using (var transaction = session.BeginTransaction())
  97:             {
  98:  
  99:                 var customer = session.CreateCriteria<Customer>()
 100:                     .Add(Expression.Eq("Name", "Bill Gates"))
 101:                     .UniqueResult();
 102:                 session.Delete(customer);