Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

NHibernate - Changing sub-types

How do you go about changing the subtype of a row in NHibernate? For example if I have a Customer entity and a subclass of TierOneCustomer, I have a case where I need to change a Customer to a TierOneCustomer but the TierOneCustomer should have the same Id (PK) as the original Customer entity.

The mapping looks something like this:

<class name="Customer" table="SiteCustomer" discriminator-value="C">
  <id name="Id" column="Id" type="Int64">
    <generator class="identity" />
  </id>
  <discriminator column="CustomerType" />
  ... properties snipped ...

  <subclass name="TierOneCustomer" discriminator-value="P">
    ... more properties ...
  </subclass>
</class>

I'm using the one-table per class hierarchy model, so using plain-sql, it'd be just a matter of a sql update of the discriminator (CustomerType) and set the appropriate columns relevant for the type. I can't find the solution in NHibernate, so would appreciate any pointers.

I'm also thinking whether the model is correct considering this use-case, but before I go down that route, I want to make sure doing as described above is actually possible in the first place. If not, I'll almost certainly think about changing the model.

like image 206
Andy Whitfield Avatar asked Jan 25 '09 21:01

Andy Whitfield


3 Answers

Short answer is yes, you can change the discriminator value for the particular row(s) using native SQL.

However, I don't think NHibernate is intended to work this way, since the discriminator is generally "invisible" to the Java layer, where its value is supposed to be set initially according to the class of the persisted object and never changed.

I recommend looking into a cleaner approach. From the standpoint of the object model, you're trying to convert a superclass object into one of its subclass types while not changing the identity of its persisted instance, and that's where the conflict is (the converted object isn't really supposed to be the same thing). Two alternative approaches are:

  • Create a new instance of TierOneCustomer based on the information in the original Customer object, then delete the original object. If you were relying on the Customer's Primary Key for retrieval, you'll need to take note of the new PK.

or

  • Change your approach so the object type (discriminator) doesn't need to change. Instead of relying on a subclass to distinguish TierOneCustomer from Customer, you can use a property that you can modify freely at any time, i.e. Customer.Tier = 1.

Here are some related discussions on the Hibernate Forums that may be of interest:

  1. Can we update the discriminator column in Hibernate
  2. Table-per-Class Problem: Discriminator and Property
  3. Converting a persisted instance into a subclass
like image 93
David Crow Avatar answered Sep 29 '22 23:09

David Crow


You're doing something wrong.

What you are trying to do is to change the type of an object. You can't do that in .NET or in Java. That simply doesn't make sense. An object is of exactly one concrete type, and its concrete type cannot be changed from the time the object is created until the time the object is destroyed (black magic notwithstanding). In order to accomplish what you are trying to do, but with the class hierarchy you laid out, you would have to destroy the customer object which you want to turn into a tier-one customer object, create a new tier-one customer object, and copy all the relevant properties from the customer object to the tier-one customer object. That is how you do it with objects, in object-oriented languages, with your class hierarchy.

Obviously, the class hierarchy you have isn't working for you. You don't destroy customers in real life when they become tier-one customers! So don't do it with objects either. Instead, come up with a class hierarchy that makes sense, given the scenarios you need to implement. Your use scenarios include:

  • A customer who previously is not tier-one status now becomes tier-one status.

That means you need a class hierarchy which can accurately capture this scenario. As a hint, you should favor composition over inheritance. That means, it may be a better idea to have a property named IsTierOne, or a property named DiscountStrategy, etc., depending on what works best.

The entire purpose of NHibernate (and Hibernate for Java) is to make the database invisible. To allow you to work with objects natively, with the database magically there behind the scenes to make your objects persistent. NHibernate will let you work with the database natively, but that's not the type of scenario which NHibernate is built for.

like image 28
yfeldblum Avatar answered Sep 29 '22 23:09

yfeldblum


This is REALLY late, but may be of use to the next person looking to do something similar:

While the other answers are correct that you shouldn't change the discriminator in most cases, you can do it purely within the scope of NH (no native SQL), with some clever use of mapped properties. Here's the gist of it using FluentNH:

public enum CustomerType //not sure it's needed
{
   Customer,
   TierOneCustomer
}

public class Customer
{
   //You should be able to use the Type name instead,
   //but I know this enum-based approach works
   public virtual CustomerType Type 
   { 
      get {return CustomerType.Customer;} 
      set {} //small code smell; setter exists, no error, but it doesn't do anything.
   }
   ...
}

public class TierOneCustomer:Customer
{
   public override CustomerType Type {get {return CustomerType.TierOneCustomer;} set{}}
   ...
}

public class CustomerMap:ClassMap<Customer>
{
   public CustomerMap()
   {
      ...
      DiscriminateSubClassesOnColumn<string>("CustomerType");
      DiscriminatorValue(CustomerType.Customer.ToString());
      //here's the magic; make the discriminator updatable
      //"Not.Insert()" is required to prevent the discriminator column 
      //showing up twice in an insert statement
      Map(x => x.Type).Column("CustomerType").Update().Not.Insert();
   }
}

public class TierOneCustomerMap:SubclassMap<TierOneCustomer>
{
   public CustomerMap()
   {
      //same idea, different discriminator value
      ...
      DiscriminatorValue(CustomerType.TierOneCustomer.ToString());
      ...
   }
}

The end result is that the discriminator value is specified for inserts, and used to determine the instantiated type on retrieval, but then if a record of a different subtype with the same Id is saved (as if the record was cloned or un-bound from the UI to a new type), the discriminator value is updated on the existing record with that ID as an object property, so that future retrievals of that type are as the new object. The setter is required on the properties because AFAIK NHibernate can't be told that a property is read-only (and thus "write-only" to the DB); in NHibernate's world, if you write something to the DB, why wouldn't you want it back?

I used this pattern recently to allow users to change the basic type of a "tour", which is in reality a set of rules governing the scheduling of the actual "tour" (a single digital "visit" to a client's on-site equipment to ensure it all works properly). While they're all "tour schedules" and need to be collectable in lists/queues etc as such, the different types of schedules require very different data and very different processing, calling for a similar data structure as the OP has. I therefore completely understand the OP's desire to treat a TierOneCustomer in a substantially different way while minimizing the effect at the data layer, so, here ya go.

like image 20
KeithS Avatar answered Sep 29 '22 23:09

KeithS