views:

80

answers:

3

Good afternoon.

Before I begin my explanation, I have had a look at other similar questions but the subtle differences (mainly in purpose of design) mean that the solutions provided in these answers to not apply to me.

I am attempting to create a 'Base data access library' for use with future projects so that I do not have to spend my time coding the same logic across multiple projects. The intention is to simply import this library and instantly have the ability to inherit from a IBaseDao and instantly have standard 'CRUD' abilities from the get-go.

Because (when coding this base library) I have no way of knowing exactly what my business objects will be (Customer, FileStructure, Cat, Dog, Order ect) I have defined an interface within my base library which all business objects must implement before they can be used with this library. Such an interface is defined as follows:

public interface IEntity
{
    /// Indicates weather this entity is used as test
    /// data or if it is a real-world entity.
    bool IsMockObject { get; set; }

    /// This is not intended for use in a 'real' application
    /// and is only used in testing.
    string ReadableName { get; set; }

    /// The unique identifier for this object.
    Guid Id { get; set; }
}

So now in the 'real' application I intend to have something similar to the following:

public class Customer : IEntity
{
    public Customer()
    {

    }

    string name = "";
    public virtual String Name
    {
        get
        {
            return name;
        }
        set
        {
            name = value;
        }
    }

    private DateTime birthday = DateTime.Now;
    public virtual DateTime Birthday
    {
        get;
        set;
    }

    private List<Order> orders = new List<Order>();
    public virtual List<Order> Orders
    {
        get
        {
            return orders;
        }
        set
        {
            orders = value;
        }
    }

    #region IEntity Members

    private bool isMockObject;
    public virtual bool IsMockObject
    {
        get
        {
            return true;
        }
        set
        {
            isMockObject = value;
        }
    }

    private string readableName;
    public virtual string ReadableName
    {
        get
        {
            return readableName;
        }
        set
        {
            readableName = value;
        }
    }

    private Guid id;
    public virtual Guid Id
    {
        get
        {
            return id;
        }
        set
        {
            id = value;
        }
    }
    #endregion
}

Now here is where the issue arises. During my unit tests, when i try and save a 'Customer' I get a parameter count mismatch exception. I suspect the cause of this issue is that the Customer table within my database does not contain columns for all the IEntity properties. It does however contain an Id column. So far, my CustomerMap looks like this:

public class CustomerMap : ClassMap<Customer>
{
    public CustomerMap()
    {
        Table("Customer");

        // From IEntity
        Id(x => x.Id);

        Map(x => x.Name);
        Map(x => x.Birthday);

        // From IEntity
        Map(x => x.IsMockObject);

        // From IEntity
        Map(x => x.ReadableName);
        HasMany(x => x.Orders);
    }
}

What I actually want NHibernate to do when saving is save in the Customer table, all the properties unique to Customer as well as the IEntity.Id property then save in a seperate table called (if IsMockEntity is true) MockEntitiy the Id of the mock object and its readable name.

I find ClassMap's pretty difficult to understand at the moment as I have done very little with NHibernate past basic persistence. Any help or links to relevant material greatly appreciated.

Thank you for your time.

A: 

The first thing I would do is

  1. Set the List to an IList; NHibernate needs to be able to inject its own type of enumerable to the object, not C#'s defined List.
  2. Make sure NHibernate is able to read and write to all of your properties, i.e. do not set default values in the mappings. If you need that, you could do it in the constructor (or somewhere else).

_

public class Customer
{
    public Customer()
    {
        IsMockObject = true;
        Orders = new List<Order>();
    }

    public virtual Guid Id { get; set; }
    public virtual string Name { get; set; }
    public virtual DateTime Birthday { get; set; }  

    // Protected: the user can't modify this!
    public virtual bool IsMockObject { get; protected set; }

    public virtual string ReadableName { get; set; }

    // Needs to be an IList<T>, NOT a regular List<T>.
    public virtual IList<Order> Orders { get; set; }
}

_

I don't think there's any problems with your ClassMap, but I'd change these things first and then see what's up. If it still doesn't work, do notify us of the new exception thrown.

Rafael Belliard
I am fairly certain there is an issue with the ClassMap because when I have a look at what NHibernate is actually trying to do, it is trying to add the 'IsMockObject' and 'ReadableName' values into the Customer table instead of taking the 'Id' from the Customer object and putting that alongside the 'ReadableName' in the 'MockEntities' table. I can see why it is trying to do that because the ClassMap for Customer holds no mention of the MockEntites table.
A: 

Ok I have modified the code to reflect your suggested changes and have also done some more 'digging' to try and solve this issue. I now have the following database structure:

TABLE Customer
Id uniqueidentifier
Name varchar(50)
Birthday datetime

TABLE Order
Id uniqueidentifier
CustomerId uniqueidentifier
Price float

TABLE MockEntities
Id uniqueidentifier
EntityId uniqueidentifier
ReadableName nvarchar(MAX)

and thus, the following ClassMap's. From what I have managed to gather from other sources, what I am trying to achieve here is a join of some kind. As to weathers it is an outer or inner...im not sure.

 public class CustomerMap : ClassMap<Customer>
{
    public CustomerMap()
    {
        Table("Customer");

        Id(x => x.Id);
        Map(x => x.Name);
        Map(x => x.Birthday);
        Join("MockEntities", me =>
            {
                Id<Guid>("Id");
                me.Map(xx => xx.ReadableName);
                me.Map(xx => xx.Id, "EntityId");
            });
        HasMany(x => x.Orders).Cascade.All().Not.LazyLoad();

    }
}

The ClassMap for Order looks almost identical but for the sake of completeness I shall include it in this post.

public class OrderMap : ClassMap<Order>
{
    public OrderMap()
    {
        Table("Order");
        Id(x => x.Id);
        Map(x => x.Price);
        Map(x => x.CustomerId, "CustomerId");
        Join("MockEntities", me =>
            {
                Id<Guid>("Id");
                me.Map(xx => xx.ReadableName);
                me.Map(xx => xx.Id, "EntityId");
            });
    }
}

However, now, when I run my tests I am given the following error:

`System.Data.SqlServerCeException: The column name is not valid. [ Node name (if any) = ,Column name = Customer id ]

Just in case it is useful to anyone, a stack trace from the NUnit GUI:

at System.Data.SqlServerCe.SqlCeCommand.CompileQueryPlan()
at System.Data.SqlServerCe.SqlCeCommand.ExecuteCommand(CommandBehavior behavior, String method, ResultSetOptions options)
at System.Data.SqlServerCe.SqlCeCommand.ExecuteNonQuery()
at NHibernate.AdoNet.AbstractBatcher.ExecuteNonQuery(IDbCommand cmd)
at NHibernate.Persister.Entity.AbstractEntityPersister.Insert(Object id, Object[] fields, Boolean[] notNull, Int32 j, SqlCommandInfo sql, Object obj, ISessionImplementor session)
at NHibernate.Persister.Entity.AbstractEntityPersister.Insert(Object id, Object[] fields, Object obj, ISessionImplementor session)
at NHibernate.Action.EntityInsertAction.Execute()
at NHibernate.Engine.ActionQueue.Execute(IExecutable executable)
at NHibernate.Engine.ActionQueue.ExecuteActions(IList list)
at NHibernate.Engine.ActionQueue.ExecuteActions()
at NHibernate.Event.Default.AbstractFlushingEventListener.PerformExecutions(IEventSource session)
at NHibernate.Event.Default.DefaultFlushEventListener.OnFlush(FlushEvent event)
at NHibernate.Impl.SessionImpl.Flush()
at NHibernate.Transaction.AdoTransaction.Commit()
at MySolution.DataAccess.ConnectionProviders.NHibernateConnection.Create[TEntity](TEntity objectToCreate) in MySolution.DataAccess\ConnectionProviders\NHibernateConnection.cs:line 154
at MySolution.Testing.DataAccess.Connection.Testing_Connections.Function_NHibernateCreateNestedEntity()

And finally, the SQL that NHibernate is using:

NHibernate: INSERT INTO Customer (Name, Birthday, Id) VALUES (@p0, @p1, @p2);@p0 = 'David', @p1 = 03/09/2010 07:28:22, @p2 = 68fb7dd7-5379-430f-aa59-9de6007b2d56
NHibernate: INSERT INTO MockEntities (ReadableName, EntityId, Customer_id) VALUES (@p0, @p1, @p2);@p0 = 'Function_NHibernateCreateNestedEntity1', @p1 = 00000000-0000-0000-0000-000000000000, @p2 = 68fb7dd7-5379-430f-aa59-9de6007b2d56
07:28:28,851 ERROR [TestRunnerThread] AbstractBatcher [(null)]- Could not execute command: INSERT INTO MockEntities (ReadableName, EntityId, Customer_id) VALUES (@p0, @p1, @p2)
The column name is not valid. [ Node name (if any) = ,Column name = Customer_id ]

Im afraid im somewhat out of my depth when it comes to this issue. Previous to me trying to get this joining in the mappings working, the system was saving a Customer object and its contained Order collection very well. Now however, it seems as if it is expecting a column I have not defined anywhere, namely "Customer id".

Any input greatly appreciated, thank you.

A: 
public class CustomerMap : ClassMap<Customer>
{
    public CustomerMap()
    {
        Table("Customer");

        Id(x => x.Id);

        ...

At this point, you are defining the Customer.Id field that is blowing up, so you need to check a few things.

Firstly, you need to make sure it exists. (In your case I think it probably does)

Secondly, looking at your SQL we have a problem...

@p2 = 68fb7dd7-5379-430f-aa59-9de6007b2d56

It looks like this is a string, but it isn't wrapped in single quotes.

@p2 = '68fb7dd7-5379-430f-aa59-9de6007b2d56'

This is probably because NHibernate is under the mistaken impression that we are dealing with an integer, which I think you can fix by adjusting your CustomerMap like this.

public class CustomerMap : ClassMap<Customer>
{
    public CustomerMap()
    {
        Table("Customer");

        Id<Guid>(x => x.Id);

        ...

This answer wouldn't be complete if I didn't point out that numeric ids are almost certainly better than anything else, as they result in faster queries, better indexing and are an accepted convention. However, this should get things moving again for you.

Sohnee
The change from `Id(x => x.Id) to `Id<Guid>("Id") has made no difference to the error mentioned. I understand what you mean by the insert not being encapsulated with apostrophies but the Id column in the table is defined as a 'uniqueidentifier' and thus, if Nhibernate were to try and add this value as a string, the types would not match and an error would occur. The main issue here is why NHibernate feels as if there should be a 'Customer_id' column on the MockEntities table.
Yes, that sounds like another great reason to stick to numeric ids!
Sohnee