nhibernate 3.0 optimistic lock and related entities - nhibernate

I've noticed in my database (SQL Server) that the column I'm using for versioning is getting updated for an entity whenever that entity is added to a related table.
So, for example, if I have a Person entity and I add a Person to a Club, the Person's record in the database will have a new version. Of course, in the database, it's the Club's record that is added/updated with the Person's primary key.
I'm a bit surprised by that behavior because no other data for the Person entity is changing.
My question is if there is a way to turn that off; that is, to only have the version column updated when data in that row changes. What might the implications of that kind of configuration be?
EDIT Here's the real code
Model
public partial class ObligationProgramFund : IMyObject
{
private System.Int32 _id;
// ... many other properties
private IList<ObligationProgramFundOrganization> _obligationPFOrganizations;
private System.Byte[] _recordVersion;
public ObligationProgramFund() : base()
{
_obligationPFOrganizations = new List<ObligationProgramFundOrganization>();
}
public virtual System.Int32 Id
{
get { return _id; }
set { _id = value; }
}
public virtual IList<ObligationProgramFundOrganization> ObligationPFOrganizations
{
get { return _obligationPFOrganizations; }
set { _obligationPFOrganizations = value; }
}
public virtual System.Byte[] RecordVersion
{
get { return _recordVersion; }
set { _recordVersion = value; }
}
}
public partial class ObligationProgramFundOrganization : IMyObject
{
private ObligationProgramFund _obligationProgramFund;
private System.Int32 _id;
private Organization _organization;
private System.Byte[] _recordVersion;
// .. other properties
public ObligationProgramFundOrganization() : base()
{
}
public virtual System.Int32 Id
{
get { return _id; }
set { _id = value; }
}
public virtual System.Byte[] RecordVersion
{
get { return _recordVersion; }
set { _recordVersion = value; }
}
public virtual ObligationProgramFund ObligationProgramFund
{
get { return _obligationProgramFund; }
set { _obligationProgramFund = value; }
}
public virtual Organization Organization
{
get { return _organization; }
set { _organization = value; }
}
}
public partial class Organization : IMyObject
{
private IList<ObligationProgramFundOrganization> _obligationPFOrganizations;
private System.Int32 _id;
private System.Byte[] _recordVersion;
// other properties
public Organization() : base()
{
_obligationPFOrganizations = new List<ObligationProgramFundOrganization>();
}
public virtual System.Int32 Id
{
get { return _id; }
set { _id = value; }
}
public virtual System.Byte[] RecordVersion
{
get { return _recordVersion; }
set { _recordVersion = value; }
}
public virtual IList<ObligationProgramFundOrganization> ObligationPFOrganizations
{
get { return _obligationPFOrganizations; }
set { _obligationPFOrganizations = value; }
}
}
Mapping
public partial class ObligationProgramFundMap : ClassMap<ObligationProgramFund>
{
public ObligationProgramFundMap()
{
Table("[MySchema2].[dbo].[ObligationProgramFund]");
OptimisticLock.Version();
DynamicUpdate();
LazyLoad();
Id(x=>x.Id)
.Access.CamelCaseField(Prefix.Underscore)
.Column("[Id]")
.GeneratedBy.Identity();
Version(x=>x.RecordVersion)
.Access.CamelCaseField(Prefix.Underscore)
.Column("[RecordVersion]")
.CustomSqlType("timestamp")
.Not.Nullable()
.UnsavedValue("null")
.CustomType("BinaryBlob")
.Generated.Always();
// other properties/components
HasMany(x=>x.ObligationPFOrganizations)
.Access.CamelCaseField(Prefix.Underscore)
.Cascade.AllDeleteOrphan()
.Fetch.Select()
.Inverse()
.LazyLoad()
.KeyColumns.Add("ObligationProgramFundId");
}
}
public partial class ObligationProgramFundOrganizationMap : ClassMap<ObligationProgramFundOrganization>
{
public ObligationProgramFundOrganizationMap()
{
Table("[MySchema2].[dbo].[ObligationProgramFundOrganization]");
OptimisticLock.Version();
DynamicUpdate();
LazyLoad();
Id(x=>x.Id)
.Access.CamelCaseField(Prefix.Underscore)
.Column("[Id]")
.GeneratedBy.Identity();
Version(x=>x.RecordVersion)
.Access.CamelCaseField(Prefix.Underscore)
.Column("[RecordVersion]")
.CustomSqlType("timestamp")
.Not.Nullable()
.UnsavedValue("null")
.CustomType("BinaryBlob")
.Generated.Always();
// other properties
References(x=>x.ObligationProgramFund)
.Access.CamelCaseField(Prefix.Underscore)
.Cascade.SaveUpdate()
.Fetch.Select()
.Columns("ObligationProgramFundId");
References(x=>x.Organization)
.Access.CamelCaseField(Prefix.Underscore)
.Cascade.SaveUpdate()
.Fetch.Select()
.Columns("OrganizationId");
}
}
public partial class OrganizationMap : ClassMap<Organization>
{
public OrganizationMap()
{
Table("[MySchema2].[dbo].[Organization]");
OptimisticLock.Version();
DynamicUpdate();
LazyLoad();
Id(x=>x.Id)
.Access.CamelCaseField(Prefix.Underscore)
.Column("[Id]")
.GeneratedBy.Identity();
Version(x=>x.RecordVersion)
.Access.CamelCaseField(Prefix.Underscore)
.Column("[RecordVersion]")
.CustomSqlType("timestamp")
.Not.Nullable()
.UnsavedValue("null")
.CustomType("BinaryBlob")
.Generated.Always();
// other properties
HasMany(x=>x.ObligationPFOrganizations)
.Access.CamelCaseField(Prefix.Underscore)
.Cascade.AllDeleteOrphan()
.Fetch.Select()
.Inverse()
.LazyLoad()
.KeyColumns.Add("OrganizationId");
}
}
So my entities are
Organization 1 - ∞ ObligationProgramFundOrganization ∞ - 1 ObligationProgramFund
The issue I've observed is when a new ObligationProgramFundOrganization is created (the associative table), the record version for ObligationProgramFund is updated.
The actual senario is that I have a table of ObligationProgramFundOrganizations on the ObligationProgramFund edit form. A ObligationProgramFundOrganization is added via AJAX calls. When I go to then save the ObligationProgramFund, I get a concurrency exception. I am sure no other user actually edited the ObligationProgramFund as it's all running locally.

(I've posted an answer to a similar question: Why NHibernate UPDATE reference entity?)
You have version column specified. Which means that any property change (even collection) trigger version update.
In order to prevent a certain property/collection to change version, optimistic-lock="false" property should be set in xml mapping.

Related

Automapping a Composite Model with Composite Iteration with FluentNhibernate

I have a tree structured model and designed it with composite Pattern. for iterating through the entire hierachy Im using Composite Iteration.
I have used this tutorial:
http://www.blackwasp.co.uk/Composite.aspx
but when I want to AutoMap the model, I encounter this problem:
{"The entity '<GetEnumerator>d__0' doesn't have an Id mapped. Use the
Id method to map your identity property. For example: Id(x => x.Id)."}
but getEnumerator is a method. I don't know why handle this like an Entity!!
public IEnumerator<MenuComponent> GetEnumerator()
{
foreach (MenuComponent child in menuComponents)
yield return this;
}
here is my AutoMapping Configuration :
public class AutomappingConfiguration: DefaultAutomappingConfiguration
{
//As we do not explicitly map entities or value objects, we define conventions or exceptions
//for the AutoMapper. We do this by implementing a configuration class.
//this method instructs the AutoMapper to consider only those classes for mapping
//which reside in the same namespace as the Employeeentity.
public override bool ShouldMap(Type type)
{
return type.Namespace == typeof(Menu).Namespace;
}
}
Uploaded the sample code:
public abstract class CombatElement
{
public virtual string Name { get; set; }
public virtual Guid Id { get; set; }
public virtual void Add(
CombatElement element)
{
throw new NotImplementedException();
}
public virtual void
Remove(CombatElement element)
{
throw new NotImplementedException();
}
public virtual
IEnumerable<CombatElement>
GetElements()
{
throw new NotImplementedException();
}
public abstract void Fight();
public abstract void Move();
}
//////
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Diagnostics;
namespace FluentNHibernateMvc3.Models
{
public class Formation : CombatElement
{
private List<CombatElement> _elements;
public virtual IEnumerable<CombatElement> Elements { get { return _elements; } }
public Formation()
{
_elements = new List<CombatElement>();
}
public override void Add(
CombatElement element)
{
_elements.Add(element);
}
public override void
Remove(CombatElement element)
{
_elements.Remove(element);
}
public override void Fight()
{
Debug.WriteLine(this.Name + " Formation is fighting");
}
public override void Move()
{
Debug.WriteLine(this.Name + " Formation is moving");
}
public override
IEnumerable<CombatElement>
GetElements()
{
// yield up this current object first
yield return this;
// iterate through all child elements
foreach (CombatElement fe in
_elements)
{
// + iterate through each of its elements
foreach (CombatElement feInner
in fe.GetElements())
yield return feInner;
}
}
}
}
/////////
public class Soldier : CombatElement
{
public virtual int Rank { get; set; }
public override void Fight()
{
Debug.WriteLine(this.Name + " soldier is fighting");
}
public override void Move()
{
Debug.WriteLine(this.Name + " soldier is fighting");
}
public override
IEnumerable<CombatElement>
GetElements()
{
yield return this;
}
}
and here how I create session factory
// Returns our session factory
private static ISessionFactory CreateSessionFactory()
{
//m => m.FluentMappings.AddFromAssemblyOf<FormationMap>()
return Fluently.Configure()
.Database( CreateDbConfig )
.Mappings(m => m.AutoMappings.Add(CreateMappings()))
.ExposeConfiguration( UpdateSchema )
.CurrentSessionContext<WebSessionContext>()
.BuildSessionFactory();
}
// Returns our database configuration
private static MsSqlConfiguration CreateDbConfig()
{
return MsSqlConfiguration
.MsSql2008
.ConnectionString( c => c.FromConnectionStringWithKey( "testConn" ) );
}
// Returns our mappings
private static AutoPersistenceModel CreateMappings()
{
var cfg = new AutomappingConfiguration();
return AutoMap
.Assemblies(cfg,System.Reflection.Assembly.GetCallingAssembly()).IncludeBase<CombatElement>()
.Conventions.Setup( c => c.Add( DefaultCascade.SaveUpdate() ) );
}
// Updates the database schema if there are any changes to the model,
// or drops and creates it if it doesn't exist
private static void UpdateSchema( Configuration cfg )
{
new SchemaUpdate( cfg )
.Execute( false, true );
}
Does anyone has any idea?

NHibernate: single lazy load property

Update: I'm now convinced that the problem lies in the fact that Document is configured as non lazy. The problem is that I don't control the base class and that means I can't change the base props to virtual...
After reading the docs, I'm under the assumption that I should be able to have a non lazy class with a lazy property. Is this possible? Here's the code I'm using for mapping my class:
public class DocumentoMapping : ClassMap<Documento> {
public DocumentoMapping()
{
Setup();
}
private void Setup()
{
Table("Documentos");
Not.LazyLoad();
Id(doc => doc.Id, "IdDocumentos")
.GeneratedBy.Identity()
.Default(0);
Map(doc => doc.NomeDocumento)
.Not.Nullable();
Map(doc => doc.Descricao);
Map(doc => doc.Bytes, "Documento")
.CustomSqlType("image")
.CustomType<Byte[]>()
.LazyLoad()
.Length(2000000000);
Component(doc => doc.Acao,
accao =>
{
accao.Map(a => a.Login);
accao.Map(a => a.Data);
accao.Map(a => a.UserAD)
.CustomSqlType("int")
.CustomType<ADs>();
})
.Not.LazyLoad();
Map(doc => doc.IdPedidoAssistencia)
.Column("IdPats")
.Not.LazyLoad();
}
}
And here's the code for my class:
public class Documento : Entity, IHasAssignedId<int>{
public virtual Byte[] Bytes { get; private set; }
public Documento()
{
NomeDocumento = Descricao = "";
Acao = new Acao("none", DateTime.Now, ADs.Sranet);
}
public Documento(string nomeDocumento, string descricao, Acao acao)
{
Contract.Requires(!String.IsNullOrEmpty(nomeDocumento));
Contract.Requires(!String.IsNullOrEmpty(descricao));
Contract.Requires(acao != null);
Contract.Ensures(!String.IsNullOrEmpty(NomeDocumento));
Contract.Ensures(!String.IsNullOrEmpty(Descricao));
Contract.Ensures(Acao != null);
NomeDocumento = nomeDocumento;
Descricao = descricao;
Acao = acao;
}
[DomainSignature]
public String NomeDocumento { get; private set; }
[DomainSignature]
public String Descricao { get; private set; }
[DomainSignature]
public Acao Acao { get; private set; }
internal Int32 IdPedidoAssistencia { get; set; }
internal static Documento CriaNovo(String nomeDocumento, String descricao, Byte[] bytes, Acao acao)
{
Contract.Requires(!String.IsNullOrEmpty(nomeDocumento));
Contract.Requires(!String.IsNullOrEmpty(descricao));
Contract.Requires(bytes != null);
Contract.Requires(acao != null);
var documento = new Documento(nomeDocumento, descricao, acao) { Bytes = bytes };
return documento;
}
public void ModificaBytes(Byte[] bytes)
{
Contract.Requires(bytes != null);
Bytes = bytes;
}
public void SetAssignedIdTo(int assignedId)
{
Id = assignedId;
}
[ContractInvariantMethod]
private void Invariants()
{
Contract.Invariant(NomeDocumento != null);
Contract.Invariant(Descricao != null);
Contract.Invariant(Acao != null);
}
}
Base classes are the just for the basic stuff, ie, setting Id and injecting base code for instance comparison. At first sight, I can't really see anything wrong with this code. I mean, the property is virtual, the mapping says it should be virtual, so why does loading it with Get forces a complete load of the properties? For instance, this code:
var d = sess.Get(148);
Ends up generating sql for loading all the properties on the table. Did I get this wrong?
thanks!
Yes, it's confirmed: in order to have lazy load properties on a class, the class will also need to be lazy.

Auditing user using NHibernate Envers fluentconfiguration

I am trying to use NHibernate Envers to log an additional field "user". I have followed several code examples that seem to vary a bit when it comes to syntax, probably because some of them are a bit out of date. However I can't get it to work.
I'm getting this exception:
Only one property may have the attribute [RevisionNumber]!
My Custom Revision Entity:
public class CustomRevisionEntity
{
public virtual int Id { get; set; }
public virtual DateTime RevisionTimestamp { get; set; }
public virtual Guid UserIdentityId { get; set; }
public override bool Equals(object obj)
{
if (this == obj) return true;
var revisionEntity = obj as CustomRevisionEntity;
if (revisionEntity == null) return false;
var that = revisionEntity;
if (Id != that.Id) return false;
return RevisionTimestamp == that.RevisionTimestamp;
}
public override int GetHashCode()
{
var result = Id;
result = 31 * result + (int)(((ulong)RevisionTimestamp.Ticks) ^ (((ulong)RevisionTimestamp.Ticks) >> 32));
return result;
}
}
My IRevisionListener:
public class RevInfoListener : IRevisionListener
{
public void NewRevision(object revisionEntity)
{
var casted = revisionEntity as CustomRevisionEntity;
if (casted != null)
{
casted.UserIdentityId = Guid.NewGuid(); // TODO
}
}
}
First I use mapping by code to map the entity:
_modelMapper.Class<CustomRevisionEntity>(entity =>
{
entity.Property(x => x.Id);
entity.Property(x => x.RevisionTimestamp);
entity.Property(x => x.UserIdentityId);
});
Then I configure Envers and NHibernate
var enversConf = new FluentConfiguration();
enversConf.SetRevisionEntity<CustomRevisionEntity>(x => x.Id, x => x.RevisionTimestamp, new RevInfoListener());
enversConf.Audit<OrganizationEntity>().Exclude(x => x.Version);
configuration.IntegrateWithEnvers(enversConf); // This is the nh-configuration
The last line gives me the exception:
Only one property may have the attribute [RevisionNumber]!
Anyone have any ideas? Myself I would speculate that the default revision entity is still used somehow and when I try to register my custom revision entity this happens.
The error message occurred because the Id property was being mapped twice.
In our mapping class we had this
_modelMapper.BeforeMapClass += (modelInspector, type, classCustomizer) => classCustomizer.Id(type.GetProperty("Id"), (idMapper) =>
{
idMapper.Access(Accessor.Property);
idMapper.Generator(Generators.GuidComb);
});
Then we tried mapping Id again as a property of the CustomRevisionEntity
The final mapping:
_modelMapper.Class<CustomRevisionEntity>(entity =>
{
entity.Id<int>(x => x.Id, mapper => mapper.Generator(Generators.Identity));
entity.Property(x => x.RevisionDate);
entity.Property(x => x.UserIdentityId);
});

Problem with NHibernate mapping when Id is in abstract base class

I'm quite new to nhibernate, I was doing all right until I face this problem, It looks like a NHibernate bug, but being a newbie with it, it can certainly be my fault
Having this base class to do all the Id and equality stuff
public abstract class ObjetoConId
{
public ObjetoConId()
{
Id=NewId();
}
public virtual Guid Id {get;private set;}
public override bool Equals(object o)
{
if (Object.ReferenceEquals(this,o))
return true;
if (o==null) return false;
ObjetoConId oId;
oId= o as ObjetoConId;
if (!Object.ReferenceEquals(oId,null))
return (Id.Equals(oId.Id));
return (base.Equals(o));
}
public override int GetHashCode()
{
byte[] bId;
bId=Id.ToByteArray();
return ((Int32)(bId[8]^bId[12])<<24) +
((Int32)(bId[9]^bId[13])<<16) +
((Int32)(bId[10]^bId[14])<<8) +
((Int32)(bId[11]^bId[15]));
}
public virtual bool Equals(ObjetoConId o)
{
if (Object.ReferenceEquals(this,o))
return true;
if (Object.ReferenceEquals(o,null)) return false;
return (Id.Equals(o.Id));
}
public virtual string toString()
{
return this.GetType().FullName
+ "[id=" + Id + "]";
}
protected virtual Guid NewId()
{
return GuidComb.NewGuid();
}
public static bool operator == (ObjetoConId x,ObjetoConId y)
{
if(Object.ReferenceEquals(x,y))
return true;
if(Object.ReferenceEquals(x,null))
return false;
return x.Equals(y);
}
public static bool operator != (ObjetoConId x,ObjetoConId y)
{
return !(x==y);
}
/// <summary>
/// Metodo interno para permitir el testing
/// </summary>
/// <param name="id"></param>
internal void setId(Guid id)
{
Id=id;
}
}
and this entity
public class Propiedad : ObjetoConId,IPropiedad
{
[Obsolete("Persistance Constructor only")]
public Propiedad ()
{
}
public Propiedad (IList<IDescripcionCalificada> descripciones)
{
Descripciones=new Dictionary<string,IDescripcionCalificada>(descripciones.Count);
foreach(IDescripcionCalificada d in descripciones)
Descripciones.Add(d.Nombre,d);
}
#region IPropiedad implementation
public virtual IDictionary<string, IDescripcionCalificada> Descripciones {get;private set;}
#endregion
}
and this mapping
public class MapeoPropiedad : ClassMap<Propiedad>
{
public MapeoPropiedad()
{
Id(x => x.Id).Column("pro_id").GeneratedBy.Assigned();
HasMany<DescripcionCalificada>(x => x.Descripciones)
.Cascade.SaveUpdate()
.AsMap<string>(index => index.Nombre)
;
}
}
The test for it is
[TestFixture]
public class TestPropiedadPersistencia
{
[TestFixtureSetUp]
public void TestFixtureSetUp()
{
string connectionString="Server=127.0.0.1;Database=Ana;User ID=dev-test;Password=dev-test;";
fcfg=Fluently.Configure()
.Database(PostgreSQLConfiguration.PostgreSQL82.ConnectionString(connectionString))
.Mappings(m => m.FluentMappings.AddFromAssemblyOf<MapeoPropiedad>());
fcfg.ExposeConfiguration(cfg => new SchemaExport(cfg).Create(false, true));
sessions=fcfg.BuildSessionFactory();
}
ISessionFactory sessions;
FluentConfiguration fcfg;
[TestFixtureTearDown]
public void TestFixtureTearDown()
{
fcfg.ExposeConfiguration(cfg => new SchemaExport(cfg).Drop(false, true));
sessions.Close();
sessions = null;
fcfg = null;
}
[Test]
public void CanCorrectlyMapPropiedad()
{
DescripcionCalificada descri1=new DescripcionCalificada("descri",new Descripcion("Esta es la descri"));
DescripcionCalificada descri2=new DescripcionCalificada("descriLarga",new Descripcion("Esta es la descriLarga"));
Dictionary<string,IDescripcionCalificada> descris=new Dictionary<string, IDescripcionCalificada>(2);
descris.Add(descri1.Nombre,descri1);
descris.Add(descri2.Nombre,descri2);
new PersistenceSpecification<Propiedad>(sessions.OpenSession(),new CustomEqualityComparer() )
.CheckProperty(c => c.Descripciones,descris)
.VerifyTheMappings();
}
}
The thing is that the test fails unless I put Not.LazyLoad() in the mapping
It gives a mapping error
Ana.Nucleo.Lenguaje.Test.TestDescripcionCalificadaPersistencia (TestFixtureSetUp):
FluentNHibernate.Cfg.FluentConfigurationException : An invalid or incomplete configuration was used while creating a SessionFactory. Check PotentialReasons collection, and InnerException for more detail.
----> NHibernate.InvalidProxyTypeException : The following types may not be used as proxies:
Ana.Catalogacion.Implementacion.Propiedad: method setId should be 'public/protected virtual' or 'protected internal virtual'
without lazy loading it pass, and if I put the Id property in the Propiedad class and not inherit from ObjetoConID it also pass, with and without the Not.LazyLoad().
Anyone can confirm this is a NH bug, or any help will be appreciated
EDIT:
I've found the problem, my fault. I missed the setId internal function not being virtual protected and confused with the setter of the Id property, and thus missunderstood the execption
Fer
I've found the problem, my fault. I missed the setId internal function not being virtual protected and confused with the setter of the Id property, and thus missunderstood the execption

NHibernate - KeyColumn from Parent Table

my application has the following database structure:
Transactions:
- TransactionID (PK, Identity)
- Type
- TotalAmount
TransactionDetails:
- TransactionDetailID (PK, Identity)
- TransactionID (PK)
- Amount
ProductTransactions:
- TransactionID (PK, FK)
- Discount
ProductTransactionDetails:
- TransactionDetailID (PK, FK)
- ProductID (FK)
I have this mapped using Fluent NHibernate so that ProductTransaction inherits from Transaction and uses a SubclassMap. I did the same for ProductTransactionDetail and TransactionDetail. I also have a property called "Details" which is a list of TransactionDetail on my Transaction entity with the following mapping:
HasMany(x => x.Details)
.KeyColumn("TransactionID")
.Inverse()
.Cascade.All();
I'd like to be able to override this on my ProductTransaction entity. When using virtual and override the compiler complained but new virtual seemed to work. The problem i have is how i map this since the ProductTransactionDetails doesn't have the TransactionID column in the table. It needs to somehow grab it from the parent table but i'm not sure how to do this.
I'd appreciate it if someone could help fix the issue i'm having or let me know if i'm going about things in the wrong way.
Thanks
Comments are in the code...
Domain Model
public class Product : IEquatable<Product>
{
protected internal virtual int Id { get; set; }
public virtual bool Equals(Product other)
{
if (ReferenceEquals(null, other)) return false;
if (ReferenceEquals(this, other)) return true;
return other.Id == Id;
}
#region Implementation of IEquatable
public override bool Equals(object obj)
{
if (ReferenceEquals(null, obj)) return false;
if (ReferenceEquals(this, obj)) return true;
if (obj.GetType() != typeof (Product)) return false;
return Equals((Product) obj);
}
public override int GetHashCode()
{
return Id;
}
public static bool operator ==(Product left, Product right)
{
return Equals(left, right);
}
public static bool operator !=(Product left, Product right)
{
return !Equals(left, right);
}
#endregion Implementation of IEquatable
}
public class Transaction : IEquatable<Transaction>
{
private IList<TransactionDetail> details;
// This is declared protected because it is an implementation
// detail that does not belong in the public interface of the
// domain model. It is declared internal so the fluent mapping
// can see it.
protected internal virtual int Id { get; set; }
public virtual double TotalAmount { get; set; }
// This is declared as a IList even though it is recommended
// to use ICollection for a Bag because the the Testing Framework
// passes a HashSet to NHibernate and NHibernate attempts to cast
// it to a List since it is declared a Bag in the mapping.
public virtual IList<TransactionDetail> Details
{
// I lazily initialize the collection because I do not like
// testing for nulls all through my code but you may see
// issues with this if you use Cascade.AllDeleteOrphan in
// the mapping.
get { return details ?? (details = new List<TransactionDetail>()); }
set { details = value; }
}
#region Implementation of IEquatable
// Do not forget to declare this function as virtual or you will
// get a mapping exception saying that this class is not suitable
// for proxying.
public virtual bool Equals(Transaction other)
{
if (ReferenceEquals(null, other)) return false;
if (ReferenceEquals(this, other)) return true;
return other.Id == Id;
}
public override bool Equals(object obj)
{
if (ReferenceEquals(null, obj)) return false;
if (ReferenceEquals(this, obj)) return true;
if (obj.GetType() != typeof(Transaction)) return false;
return Equals((Transaction)obj);
}
public override int GetHashCode()
{
return Id;
}
public static bool operator ==(Transaction left, Transaction right)
{
return Equals(left, right);
}
public static bool operator !=(Transaction left, Transaction right)
{
return !Equals(left, right);
}
#endregion Implementation of IEquatable
}
public class TransactionDetail : IEquatable<TransactionDetail>
{
protected internal virtual int Id { get; set; }
public virtual double Amount { get; set; }
#region Implementation of IEquatable
public virtual bool Equals(TransactionDetail other)
{
if (ReferenceEquals(null, other)) return false;
if (ReferenceEquals(this, other)) return true;
return other.Id == Id;
}
public override bool Equals(object obj)
{
if (ReferenceEquals(null, obj)) return false;
if (ReferenceEquals(this, obj)) return true;
if (obj.GetType() != typeof (TransactionDetail)) return false;
return Equals((TransactionDetail) obj);
}
public override int GetHashCode()
{
return Id;
}
public static bool operator ==(TransactionDetail left, TransactionDetail right)
{
return Equals(left, right);
}
public static bool operator !=(TransactionDetail left, TransactionDetail right)
{
return !Equals(left, right);
}
#endregion Implementation of IEquatable
}
public class ProductTransaction : Transaction, IEquatable<ProductTransaction>
{
public virtual double Discount { get; set; }
// This is declared 'new' because C# does not support covariant
// return types until v4.0. This requires clients to explicitly
// cast objects of type Transaction to ProductTransaction before
// invoking Details. Another approach would be to change the
// property's name (e.g., ProductDetails) but this also requires
// casting.
public virtual new IList<ProductTransactionDetail> Details
{
get { return base.Details.OfType<ProductTransactionDetail>().ToList(); }
set { base.Details = null == value ? null : value.Cast<TransactionDetail>().ToList(); }
}
#region Implementation of IEquatable
public virtual bool Equals(ProductTransaction other)
{
return base.Equals(other);
}
public override bool Equals(object obj)
{
if (ReferenceEquals(null, obj)) return false;
if (ReferenceEquals(this, obj)) return true;
return Equals(obj as ProductTransaction);
}
public override int GetHashCode()
{
return base.GetHashCode();
}
public static bool operator ==(ProductTransaction left, ProductTransaction right)
{
return Equals(left, right);
}
public static bool operator !=(ProductTransaction left, ProductTransaction right)
{
return !Equals(left, right);
}
#endregion Implementation of IEquatable
}
public class ProductTransactionDetail : TransactionDetail, IEquatable<ProductTransactionDetail>
{
public virtual Product Product { get; set; }
#region Implementation of IEquatable
public virtual bool Equals(ProductTransactionDetail other)
{
return base.Equals(other);
}
public override bool Equals(object obj)
{
if (ReferenceEquals(null, obj)) return false;
if (ReferenceEquals(this, obj)) return true;
return Equals(obj as ProductTransactionDetail);
}
public override int GetHashCode()
{
return base.GetHashCode();
}
public static bool operator ==(ProductTransactionDetail left, ProductTransactionDetail right)
{
return Equals(left, right);
}
public static bool operator !=(ProductTransactionDetail left, ProductTransactionDetail right)
{
return !Equals(left, right);
}
#endregion Implementation of IEquatable
}
Fluent Mapping
internal sealed class ProductMap : ClassMap<Product>
{
internal ProductMap()
{
Table("Product")
;
LazyLoad()
;
Id(x => x.Id)
.Column("ProductId")
.GeneratedBy.Identity()
;
}
}
internal sealed class TransactionMap : ClassMap<Transaction>
{
internal TransactionMap()
{
// The table name is surrounded by back ticks because
// 'Transaction' is a reserved word in SQL. On SQL Server,
// this translates to [Transaction].
Table("`Transaction`")
;
LazyLoad()
;
Id(x => x.Id)
.Column("TransactionId")
.GeneratedBy.Identity()
;
Map(x => x.TotalAmount)
.Column("TotalAmount")
.Not.Nullable()
;
// You should consider treating TransactionDetail as a value
// type that cannot exist outside a Transaction. In this case,
// you should mark the relation as Not.Inverse and save or
// update the transaction after adding a detail instead of
// saving the detail independently.
HasMany(x => x.Details)
.KeyColumn("TransactionID")
.Cascade.All()
.Not.Inverse()
.AsBag()
;
// You have a Type column in your example, which I took to
// mean that you wanted to use the Table Per Hierarchy
// strategy. It this case you need to inform NHibernate
// which column identifies the subtype.
DiscriminateSubClassesOnColumn("Type")
.Not.Nullable()
;
}
}
internal sealed class TransactionDetailMap : ClassMap<TransactionDetail>
{
internal TransactionDetailMap()
{
Table("TransactionDetail")
;
LazyLoad()
;
Id(x => x.Id)
.Column("TransactionDetailId")
.GeneratedBy.Identity()
;
Map(x => x.Amount)
.Column("Amount")
.Not.Nullable()
;
}
}
internal sealed class ProductTransactionMap : SubclassMap<ProductTransaction>
{
internal ProductTransactionMap()
{
KeyColumn("TransactionId")
;
// I recommend giving the discriminator column an explicit
// value for a subclass. Otherwise, NHibernate uses the fully
// qualified name of the class including the namespace. If
// you later move the class to another namespace or rename
// the class then you will have to migrate all of the data
// in your database.
DiscriminatorValue("TransactionKind#product")
;
Map(x => x.Discount)
.Column("Discount")
.Nullable()
;
// Do not map the over-ridden version of
// the Details property. It is handled
// by the base class mapping.
}
}
internal sealed class ProductTransactionDetailMap : SubclassMap<ProductTransactionDetail>
{
internal ProductTransactionDetailMap()
{
// There was no Type column in your example for this table,
// whcih I took to mean that you wished to use a Table Per
// Class strategy. In this case, you need to provide the
// table name even though it is a subclass.
Table("ProductTransactionDetail")
;
KeyColumn("TransactionDetailId")
;
References(x => x.Product)
.Column("ProductId")
.Not.Nullable()
;
}
}
Unit Tests
[TestClass]
public class UnitTest1
{
private static ISessionFactory sessionFactory;
private static Configuration configuration;
[TestMethod]
public void CanCorrectlyMapTransaction()
{
using (var dbsession = OpenDBSession())
{
var product = new Product();
dbsession.Save(product);
new PersistenceSpecification<Transaction>(dbsession)
.CheckProperty(t => t.TotalAmount, 100.0)
.CheckComponentList(
t => t.Details,
new[] {
new TransactionDetail {
Amount = 75.0,
},
new ProductTransactionDetail {
Amount = 25.0,
Product = product,
},
}
)
.VerifyTheMappings()
;
}
}
private static Configuration Configuration
{
get
{
return configuration ?? (
configuration = forSQLite().Mappings(
m => m.FluentMappings
.Conventions.Setup(x => x.Add(AutoImport.Never()))
.Add(typeof(ProductMap))
.Add(typeof(ProductTransactionMap))
.Add(typeof(ProductTransactionDetailMap))
.Add(typeof(TransactionMap))
.Add(typeof(TransactionDetailMap))
)
.BuildConfiguration()
);
}
}
private static ISessionFactory SessionFactory
{
get { return sessionFactory ?? (sessionFactory = Configuration.BuildSessionFactory()); }
}
private static ISession OpenDBSession()
{
var session = SessionFactory.OpenSession();
// Ideally, this would be done once on the database
// session but that does not work when using SQLite as
// an in-memory database. It works in all other cases.
new SchemaExport(configuration)
.Execute(
true, // echo schema to Console
true, // create schema on connection
false, // just drop do not create
session.Connection, // an active database connection
null // writer for capturing schema
);
return session;
}
private static FluentConfiguration forSQLite()
{
return Fluently.Configure()
.Database(
SQLiteConfiguration
.Standard
.InMemory()
.ShowSql()
);
}
}