Wednesday, January 8, 2014

Partial Class Is A Boon For Code Generator Developers

I got this following exception with NHibernate..

Cannot instantiate abstract class or interface: TestInheritance.DomainModels.BusinessEntity

..with these AdventureWorks2012 domain models:

public abstract class BusinessEntity
{
    public virtual int BusinessEntityID { get; set; }
}

public class Person : BusinessEntity
{
    public virtual string FirstName { get; set; }
    public virtual string LastName { get; set; }
}

public class Employee : Person
{
    public virtual string NationalIDNumber { get; set; }
    public virtual string JobTitle { get; set; }
}

public class Store : BusinessEntity
{
    public virtual string Name { get; set; }        
}

The code I tried:

var list = s.Query<BusinessEntity>().ToList();


Basically, I just want to get all business entities. Upon reading this..

After suffering this error message for a while the reason turned out to be almost logical: In an @Inheritance of type Joined, there was an entry in the root table but no entry in any of the inheriting tables. -- Kolov



..it became obvious why mapping only the above domain models produces an error, there's some BusinessEntityID in abstract BusinessEntity that is not in any of the domain models above, the fallback of NHibernate is to instantiate the base class when its ID is not in the inheritance tree, hence resulting to an exception, since abstract classes cannot be instantiated. In fact, we can also make the error go away by making the BusinessEntity domain model (an abstract class) a concrete class, however there's no sense making BusinessEntity a concrete class.


Armed with the above knowledge in mind, I queried which tables are referencing the BusinessEntity domain model:

SELECT  
  ForeignTableSchema = KCU1.TABLE_SCHEMA
  ,ForeignConstraintName = KCU1.CONSTRAINT_NAME
  ,ForeignTableName = KCU1.TABLE_NAME 
  ,ForeignColumnName = KCU1.COLUMN_NAME
  ,ForeignOrdinalPosition = KCU1.ORDINAL_POSITION

  ,ReferencedTableSchema = KCU2.TABLE_SCHEMA
  ,ReferencedConstraintName = KCU2.CONSTRAINT_NAME
  ,ReferencedTableName = KCU2.TABLE_NAME 
  ,ReferencedColumnName = KCU2.COLUMN_NAME
  ,ReferencedOrdinalPosition = KCU2.ORDINAL_POSITION

   
FROM INFORMATION_SCHEMA.REFERENTIAL_CONSTRAINTS AS RC 

INNER JOIN INFORMATION_SCHEMA.KEY_COLUMN_USAGE AS KCU1 
  ON KCU1.CONSTRAINT_CATALOG = RC.CONSTRAINT_CATALOG  
  AND KCU1.CONSTRAINT_SCHEMA = RC.CONSTRAINT_SCHEMA 
  AND KCU1.CONSTRAINT_NAME = RC.CONSTRAINT_NAME 

INNER JOIN INFORMATION_SCHEMA.KEY_COLUMN_USAGE AS KCU2 
  ON KCU2.CONSTRAINT_CATALOG = RC.UNIQUE_CONSTRAINT_CATALOG  
  AND KCU2.CONSTRAINT_SCHEMA = RC.UNIQUE_CONSTRAINT_SCHEMA 
  AND KCU2.CONSTRAINT_NAME = RC.UNIQUE_CONSTRAINT_NAME 
  AND KCU2.ORDINAL_POSITION = KCU1.ORDINAL_POSITION 

WHERE KCU2.CONSTRAINT_NAME = 'PK_BusinessEntity_BusinessEntityID'
ORDER BY KCU1.TABLE_NAME

Here's the result:






Knowing that I forgot to include the Vendor domain model, I then mapped it, then the exception problem goes away! Just merely looking at the result above, it's not instantly obvious if the BusinessEntityAddress and/or BusinessEntityContact is the table(s) that is also causing the exception above. You can only infer it by looking at their primary key, if their primary key relates one-to-one to BusinessEntity, then those models needed be mapped too. However seeing they are not one-to-one to BusinessEntity, then not mapping them won't cause exception to the abstract BusinessEntity domain model, to wit:




As we can see, they are not an aggregate root, those domain models makes sense only within the domain of an another domain model. This is where things get tricky for the code generator, even if we can indicate that the BusinessEntity is an abstract domain model (hence we can prevent it from becoming the aggregate root to BusinessEntityAddress, BusinessEntityContact or any domain models for that matter), it's impossible for code generator to deduce on which aggregate root the BusinessEntityAddress and BusinessEntityContact belongs to. This is where the code generator needed an intervention from someone with business knowledge of the domain models, these models are meant to be mapped manually, partial class totally empowers this needed manual mapping.



Happy Coding! ツ



Mapping:
public class BusinessEntityMapping : ClassMapping<BusinessEntity>
{
    public BusinessEntityMapping()
    {
        Table("Person.BusinessEntity");
        Id(x => x.BusinessEntityID, m => m.Generator(NHibernate.Mapping.ByCode.Generators.Identity));            

    }
}


public class PersonMapping : JoinedSubclassMapping<Person>
{
    public PersonMapping()
    {

        Table("Person.Person");

        Key(k => k.Column("BusinessEntityID"));

        Property(x => x.FirstName);
        Property(x => x.LastName);
    }
}



public class EmployeeMapping : JoinedSubclassMapping<Employee>
{
    public EmployeeMapping()
    {
        Table("HumanResources.Employee");

        Key(k => k.Column("BusinessEntityID"));


        Property(x => x.NationalIDNumber);
        Property(x => x.JobTitle);
    }
}


public class StoreMapping : JoinedSubclassMapping<Store>
{
    public StoreMapping()
    {
        Table("Sales.Store");

        Key(x => x.Column("BusinessEntityID"));

        Property(x => x.Name);
    }
}


public class VendorMapping : JoinedSubclassMapping<Vendor>
{
    public VendorMapping()
    {
        Table("Purchasing.Vendor");

        Key(x => x.Column("BusinessEntityID"));

        Property(x => x.AccountNumber);
        Property(x => x.Name);
    }
}

No comments:

Post a Comment