简体   繁体   中英

NHibernate: Replacing a many-to-one with a left join

The code I written actually does it's job but it is however not very optimal.

In my example I have two very simple tables

在此处输入图片说明

A simple SQL Query to get the person name and related country name (if available) would look like this

SELECT Person.Name AS Name, Country.CountryName AS Country FROM Person LEFT OUTER JOIN Country ON Country.CountryId = Person.CountryId

在此处输入图片说明

And a well constructed object picking up data with NHibernate looks like this

在此处输入图片说明

Simple enough, right. My models looks like this

namespace NHibernateLeftJoin.Models
{
    public class Country
    {
        public virtual int CountryId { get; set; }
        public virtual string CountryName { get; set; }
    }

    public class Person
    {
        public virtual int PersonId { get; set; }
        public virtual string Name { get; set; }
        public virtual int CountryId { get; set; }
        public virtual Country CountryObject { get; set; }
    }
}

And the mappings

<?xml version="1.0" encoding="utf-8" ?>
<hibernate-mapping xmlns="urn:nhibernate-mapping-2.2" auto-import="true">
  <class name="NHibernateLeftJoin.Models.Country, NHibernateLeftJoin" lazy="true">
    <id name="CountryId">
      <generator class="native" />
    </id>
    <property name="CountryName" />
  </class>

  <class name="NHibernateLeftJoin.Models.Person, NHibernateLeftJoin" lazy="true">
    <id name="PersonId">
      <generator class="native" />
    </id>
    <property name="Name" />
    <property name="CountryId" />
    <many-to-one name="CountryObject" class="NHibernateLeftJoin.Models.Country" lazy="false"
                 column="CountryId" outer-join="true" unique="true" not-null="false" cascade="none" />
  </class>
</hibernate-mapping>

The issue with this approach is that NHibernate fetches one 'Person' row and calls the 'Contry' table for each row, and if we are having thousands of rows that's not very good. It seem to be impossible to perform this in the same neat way as we do with SQL or it's me using the totally wrong approach.

The VS project can be found here for anyone interested https://dl.dropboxusercontent.com/u/6208162/NHibernateLeftJoin.zip

Thanks

== EDIT ==

A query may look like this

private static IList<Person> GetPersons()
{
    using (var session = NHibernateHelper.OpenSession())
    {
        //using (var transaction = session.BeginTransaction()) {}
        IQuery query = session.CreateQuery("FROM Person");
        return query.List<Person>();
    }
}

The sql generated by that will look like this

NHibernate: select person0_.PersonId as PersonId1_, person0_.Name as Name1_, person0_.CountryId as CountryId1_ from Person person0_

NHibernate: SELECT country0_.CountryId as CountryId0_0_, country0_.CountryName as CountryN2_0_0_ FROM Country country0_ WHERE country0_.CountryId=@p0;@p0 = 1 [Type: Int32 (0)]

NHibernate: SELECT country0_.CountryId as CountryId0_0_, country0_.CountryName as CountryN2_0_0_ FROM Country country0_ WHERE country0_.CountryId=@p0;@p0 = 2 [Type: Int32 (0)]

NHibernate: SELECT country0_.CountryId as CountryId0_0_, country0_.CountryName as CountryN2_0_0_ FROM Country country0_ WHERE country0_.CountryId=@p0;@p0 = 3 [Type: Int32 (0)]

This is quite logical with my data, the first query it fetches all Persons and then it fetches the data for the CountryObject with three different queries (there are three unique countries mapped to users).

Thanks

First off, remove this property... public virtual int CountryId { get; set; } public virtual int CountryId { get; set; } public virtual int CountryId { get; set; } You can access the CountryId via... CountryObject.CountryId . I think what you are looking for is the fetch="join" on your ManyToOne. Which tells NH, to outer-join to this table EVERY time you query a Person. This would make the ManyToOne non-lazy.

http://nhibernate.info/doc/nh/en/#mapping-declaration-manytoone

Usually, I would avoid this setting and use a QueryOver to manually join or eager fetch any ManyToOnes. If I'm working with a single entity, I'll use the default lazy loading for ManyToOnes.

Session.QueryOver<Person>()
    .Fetch(x => x.CountryObject).Eager
    .List();

well one way to get around this is to project into a class when you write the query.

A linq query to do something like this might look like this

var people = from p in _session.Query<Person>() 
select new 
{ 
     Name = p.Name, 
     Country = p.Country == null ? String.Empty : p.Country.Name;
}

Also you don't need the CountryId on the Person object. You should be able to grab the countryId like this person.Country.Id if a person is associated with a Country.

The solution was to implement the join in the CreateQuery method as follows

IQuery query = session.CreateQuery("FROM Person AS P LEFT JOIN FETCH P.CountryObject");
return query.List<Person>();

And write the mapping file for Person like this

<?xml version="1.0" encoding="utf-8" ?>
<hibernate-mapping xmlns="urn:nhibernate-mapping-2.2" auto-import="true">
  <class name="NHibernateLeftJoin.Models.Person, NHibernateLeftJoin" lazy="true">
    <id name="PersonId">
      <generator class="native" />
    </id>
    <property name="Name" />
    <property name="CountryId" />
    <many-to-one name="CountryObject" class="NHibernateLeftJoin.Models.Country"
                 column="CountryId" not-found="ignore"
                 cascade="none" />
  </class>
</hibernate-mapping>

In my opinion I can't understand why this query isn't automatically written when adding the parameters fetch="join" outer-join="true" to the many-to-one element in the mapping file.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM