4

I have an entity where a composite id is used. I changed to code to make use of wrapping the composite id in a seperate key class. I expected that with Linq I could do a comparison on key object and with the Criteria API to use Restrictions.IdEq but both fail. I need to explicitly compare the key values to make it work.

I cannot find any documentation if this should work so for the moment I am stuck with direct comparisons but this means that when I alter the key that I also need to update the query code which is obviously not what I would want.

As a side note, I tried this with NHibernate 3.0.0 Alpha 2 and 3.

Domain

Mapping

<?xml version="1.0" encoding="utf-8" ?>
<hibernate-mapping xmlns="urn:nhibernate-mapping-2.2"
                   assembly="Cwc.Pulse.Dal"
                   namespace="Cwc.Pulse.Dal">
  <class name="AddonStatus">
    <composite-id name="Id">
      <key-many-to-one name="Context" column="Context_Id" class="Context" />
      <key-property name="AddonType" column="Addon_Id"/>
    </composite-id>
    <property name="Status" />
  </class>
</hibernate-mapping>

Class

public class AddonStatus
{
    public virtual string Status { get; set; }
    public virtual Key Id { get; protected set; }

    public AddonStatus()
    {
        Id = new Key();
    }

    public class Key
    {
        public virtual Context Context { get; set; }
        public virtual AddonType AddonType { get; set; }

        public override int GetHashCode()
        {
            return ContextId.GetHashCode() ^ AddonType.GetHashCode();
        }

        public override bool Equals(object obj)
        {
            if (this == obj) return true;
            var o = obj as Key;
            if (null == o) return false;
            return Context == o.Context && AddonType == o.AddonType;
        }
    }
}

Working queries

The queries below work and as you can see I compare the key values explicitly. I do not compare the key object.

Linq

from status
in session.Query<AddonStatus>()
where status.Id.Context == context && status.Id.AddonType == addonType
select status

Criteria API

session.CreateCriteria<AddonStatus>()
.Add(Restrictions.Eq("Id.Context", context))
.Add(Restrictions.Eq("Id.AddonType", addonType))

Expected to work but dont

I expect the following queries to work. Either in efficiently for linq in memory instead of the database but I expect the criteria api to be smart enough to handle such composite id´s in queries.

Both linq and criteria api queries make use of a Key object comparison.

var key = new AddonStatus.Key
{
    Context = context,
    AddonType = addonType
};

Linq

from status
in session.Query<AddonStatus>()
where status.Id == key
select status

Criteria API

session.CreateCriteria<AddonStatus>()
.Add(Restrictions.IdEq(key))

So if anyone has such a scenario working then what am I doing wrong?

Pascal Thivent
  • 562,542
  • 136
  • 1,062
  • 1,124
Ramon Smits
  • 2,482
  • 1
  • 18
  • 20

2 Answers2

0

Interestingly, I'm getting almost the exact opposite of this behavior in 2.1.2.

My mapping (simplified):

<!-- Subscriber class -->
<class name="Subscriber" >
<composite-id name="SubscriberKey" class="SubscriberKey">
  <key-property name="Request" column="RequestID" type="int"/>
  <key-many-to-one name="User" column="UserID" class="User" not-found="ignore" />
</composite-id>

<!-- User class - note that this goes to a different schema, 
  and is not mutable.  Who knows if that's important... -->
<class name="User" schema="AnotherDb.dbo" mutable="false">
<id name="Id" column="UserID" type="int">
  <generator class="native" />
</id>
<property name="FirstName" column="FirstName" type="string" />
<property name="LastName" column="LastName" type="string" />

goes to:

public class User
{
    public virtual int? Id {get; protected set;}
    public virtual string FirstName { get; protected set; }
    public virtual string LastName { get; protected set; }

    public User() { }
}

public class Subscriber
{
    public virtual SubscriberKey SubscriberKey { get; set; }
    public virtual User User { get; set; }

    public Subscriber() { }
}

public class SubscriberKey
{
    public override bool Equals(object obj)
    {
        if (obj is SubscriberKey && obj != null)
            return ((SubscriberKey)obj).Request == Request 
                && ((SubscriberKey)obj).User.Id == User.Id;

        return false;
    }

    public override int GetHashCode()
    {
        return (Request.ToString() + User.Id.ToString()).GetHashCode();
    }

    public virtual int Request { get; set; }
    public virtual User User { get; set; }
    public SubscriberKey() { }
}

Things which work:

CreateCriteria<Subscriber>()
    .Add(Restrictions.IdEq(keyInstance))
    .UniqueResult<Subscriber>();
CreateCriteria<Subscriber>()
    .Add(Restrictions.Eq("SubscriberKey.User.Id", aUserID))
    .Add(Restrictions.Eq("SubscriberKey.Request", aRequestID))
    .UniqueResult<Subscriber>();

Things which don't work:

Get<Subscriber>(keyInstance);

I'm thinking this is an inconsistency between their various ID-equaling query forms. When I get time, I'll be building a minimal unit test to submit as a bug example. I'd be interested in any / all thoughts anyone might have on this...


edit: Heeey, I figured it out!

Things which do work, now that I've read this

Get<Subscriber>(new SubscriberKey() { 
    User = Load<User>(aUserID), // the important part!
    Request = aRequestID
});

This will create a proxy object for the User key, without hitting the database (unless necessary). If you swap Load<User> for Get<User>, you'll immediately hit the database to populate the object, rather than respecting your lazy-loading properties. Use Load.

And things like this are precisely why people suggest the (type)Repository pattern - I can do this behind the scenes: Get<>(new SK(){User=Load<>(key.User.Id)}, and still Get(key) by a single key, identical to every other object.

Groxx
  • 2,489
  • 1
  • 25
  • 32
  • Second, what I don't get is that your criteria api query with Restriction.IdEq(..) works! Can you please specify how you implemented the Equals/GetHashCode methods on both Subscriber and SubscriberKey? Maybe something has changed between the beta's and the GA version. Will test this soon to verify. – Ramon Smits Mar 09 '11 at 21:07
  • Third, I am aware of the Load/Get behavior and do not use the criteria api to retrieve an object by its primary key. I simplified the scenerio until all that remained was this but the actual queries result in several inner joins. – Ramon Smits Mar 09 '11 at 21:07
  • @Ramon: Updated code samples to show eq/ghc. Only SubscriberKey needs them, actually, as it *is* the composite-id. It's possible I'm mis-using these, I'm relatively new to NHibernate, but so far it has been working. Now I'm doing battle with dangling sessions in unit tests... – Groxx Mar 10 '11 at 21:25
0

Not directly an answer to your question, but it may be useful to you anyway. You could avoid the (explicit) composite key by mapping the AddonStatus as composite-element on the owner (most probably the Context):

  <class name="Context">
    <map name="AddonStates" table="AddonStatus">
      <key column="Context_Id" /> <!-- Foreign key to the Context -->
      <index column="Addon_Id" /> <!-- Dictionary key -->
      <composite-element>
        <property name="Status" /> <!-- data -->
      </composite-element>
    </map>
  </class>

In the class Context is looks like this:

class Context
{
  IDictionary<AddonType, AddonStatus> AddonStates { get; private set; }
}

This results and pretty the same database structure, but it is different to work with. I can't say if this is what you actually want, but it just looks like it.

Stefan Steinegger
  • 63,782
  • 15
  • 129
  • 193
  • I am aware of this contruct and use it frequently. However, is has nothing to do with querying on composite-id. Another problem with this contruct is when you want to have a query that returns all AddonStatus values where a property of its parent Context has a certain value thus a join between Context and AddonStatus. – Ramon Smits Mar 09 '11 at 20:56