Mapping a collection of enums with NHibernate
Specifically, using Attributes for the mappings.
Currently I have this working mapping the collection as type Int32 and NH seems to take care of it, but it's not exactly ideal.
The error I receive is "Unable to determine type" when trying to map the collection as of the type of the enum I am trying to map.
I found a post that said to define a class as
public class CEnumType : EnumStringType {
public CEnumType() : base(MyEnum) { }
}
and then map the enum as CEnumType, but this gives "CEnumType is not mapped" or something similar.
So has anyone got experience doing this?
So anyway, just a simple reference code snippet to give an example with
[NHibernate.Mapping.Attributes.Class(Table = "OurClass")]
public class CClass : CBaseObject
{
public enum EAction
{
do_action,
do_other_action
};
private IList<EAction> m_class_actions = new List<EAction>();
[NHibernate.Mapping.Attributes.Bag(0, Table = "ClassActions", Cascade="all", Fetch = CollectionFetchMode.Select, Lazy = false)]
[NHibernate.Mapping.Attributes.Key(1, Column = "Class_ID")]
[NHibernate.Mapping.Attributes.Element(2, Column = "EAction", Type = "Int32")]
public virtual IList<EAction> Actions
{
get { return m_class_actions; }
set { m_class_actions = value;}
}
}
So, anyone got the correct attributes for me to map this collection of enums as actual enums? It would be really nice if they were stored in the db as strings instead of ints too but it's not completely necessary.
You will need to map your CEnum type directly. In XML mappings this would mean creating a new class mapping element in your NHibernate XML mappings file.
<hibernate-mapping xmlns="urn:nhibernate-mapping-2.2" assembly="YourAssembly"
auto-import="true" default-lazy="false">
...
<class name="YourAssemblyNamespace.CEnum" table="CEnumTable" mutable="false" >
<id name="Id" unsaved-value="0" column="id">
<generator class="native"/>
</id>
...
</class>
</hibernate-mapping>
To do it with attribute mappings, something like this on top of your CEnum class:
[NHibernate.Mapping.Attributes.Class(Table = "CEnumTable")] //etc as you require
This is the way i do it. There's probably an easier way but this works for me.
Edit: sorry, i overlooked that you want it as a list. I don't know how to do that...
Edit2: maybe you can map it as a protected IList[string], and convert to public IList[EAction] just as i do with a simple property.
public virtual ContractGroups Group
{
get
{
if (GroupString.IsNullOrEmpty())
return ContractGroups.Default;
return GroupString.ToEnum<ContractGroups>(); // extension method
}
set { GroupString = value.ToString(); }
}
// this is castle activerecord, you can map this property in NH mapping file as an ordinary string
[Property("`Group`", NotNull = true)]
protected virtual string GroupString
{
get;
set;
}
/// <summary>
/// Converts to an enum of type <typeparamref name="TEnum"/>.
/// </summary>
/// <typeparam name="TEnum">The type of the enum.</typeparam>
/// <param name="self">The self.</param>
/// <returns></returns>
/// <remarks>From <see href="http://www.mono-project.com/Rocks">Mono Rocks</see>.</remarks>
public static TEnum ToEnum<TEnum>(this string self)
where TEnum : struct, IComparable, IFormattable, IConvertible
{
Argument.SelfNotNull(self);
return (TEnum)Enum.Parse(typeof(TEnum), self);
}
instead of
[NHibernate.Mapping.Attributes.Element(2, Column = "EAction", Type = "Int32")]
try
[NHibernate.Mapping.Attributes.Element(2, Column = "EAction", Type = "String")]
ie: change the Int32 to String
While I haven't tried using it myself, I stumbled across this code a little while ago and it looks pretty interesting:
http://www.lostechies.com/blogs/jimmy_bogard/archive/2008/08/12/enumeration-classes.aspx
Like I said, I haven't used it myself, but I'm going to give it a go in a project RSN.
Related
I have an entity A with a bool property (lets call it BProp).
In a second entity there is a bag mapping of A elements with a where condition on BProp like this:
<bag name="MyBag" cascade="all-delete-orphan" inverse="false" where="BProp = 1">
<key column="Structure_Id" />
<one-to-many entity-name="A" />
</bag>
The problem is tha BProp = 1 works with SqlServer and Oracle but breaks PostgreSQL which needs a condition like
where="BProp = true"
Is there a clever way to create a single hbm.xml mapping for all the three db I have to suppport?
As the query substition does not seem to be a usable solution in this filter case, you may go for query interceptor (here a quite rough implementation) :
public class BooleanInterceptor : EmptyInterceptor, IInterceptor
{
public string TrueToken { get; set; }
public string FalseToken { get; set; }
NHibernate.SqlCommand.SqlString IInterceptor.OnPrepareStatement(NHibernate.SqlCommand.SqlString sql)
{
return sql.Replace("{TrueToken}", TrueToken).Replace("{FalseToken}", FalseToken);
}
}
Your mapping would then be where="BProp = {TrueToken}"
It would be used like this
var interceptor = new BooleanInterceptor ();
using (var session = sessionFactory.OpenSession(interceptor))
{
interceptor.FalseToken = "0"; // this replacement value should be taken from a config file
interceptor.TrueToken = "1"; // see above
// your code here
session.Close();
}
I bet there are way better solutions, but I hope this will help anyway
-------------------------- previous answer
I use to insert query substitutions in my NHibernate configuration, like this
var cfg = new Configuration();
cfg.Properties.Add("dialect", "NHibernate.Dialect.MsSql2005Dialect");
.
cfg.Properties.Add("query.substitutions", "true 1, false 0");
.
I guess you could have your xml mapping with where="BProp = true" and just insert the cfg property for query.substitions depending if the dialect is MsSql / Oracle or PosgresSQL
I used this How do you map an enum as an int value with fluent NHibernate? to map in the past but I've recently upgraded to NHibernate 3 and this doesn't seem to work anymore. I've put breakpoints in my EnumConvention class and they're not being hit. The query that is hitting the database has the enum as a string which is the default configuration.
How does this work with NHibernate 3?
Update
Here is part of the mapping file that is generated:
<property name="ComponentType" type="FluentNHibernate.Mapping.GenericEnumMapper`1[[...ComponentType, ..., Version=1.0.0.0, Culture=neutral, PublicKeyToken=null]], FluentNHibernate, Version=1.1.0.0, Culture=neutral, PublicKeyToken=8aa435e3cb308880">
<column name="ComponentTypeId" />
</property>
It doesn't seem right that it would be using a GenericEnumMapper when an IUserTypeConvention is specified for enums.
Here is my convention:
public class EnumConvention : IUserTypeConvention
{
public void Accept( IAcceptanceCriteria<IPropertyInspector> criteria )
{
criteria.Expect( e => e.Property.PropertyType.IsEnum );
}
public void Apply( IPropertyInstance instance )
{
instance.CustomType( instance.Property.PropertyType );
}
}
Simply doing Map( m => m.MyEnum ).CustomType<MyEnum>() seems to work just fine now.
If anyone knows why IUserTypeConvention doesn't work with Fluent NHibernate in NHibernate 3, I'd still like to know why. Maybe it's because mapping the custom type to the enum works now, but why wasn't it removed from the lib then?
I'm running into a similar problem with Nhibernate 3.0GA and FluentNh (rebuild with the latest NH version). UserTypeConventions are not getting registered properly.
problem described here :
http://groups.google.com/group/nhusers/browse_thread/thread/c48da661f78bfad0
You should inherit your convention not from IUserTypeConvention, but from FluentNHibernate.Conventions.UserTypeConvention.
For example, this is the exact convention I use to map boolean and nullable booleans to a custom type called UserTrueFalseType:
/// <summary>
/// Convention: Boolean fields map to CHAR(1) T/F/Null
/// </summary>
public class BooleanTrueFalseConvention : FluentNHibernate.Conventions.UserTypeConvention<UserTrueFalseType>
{
/// <summary>
/// Accept field type criteria
/// </summary>
/// <param name="criteria"></param>
public override void Accept(FluentNHibernate.Conventions.AcceptanceCriteria.IAcceptanceCriteria<FluentNHibernate.Conventions.Inspections.IPropertyInspector> criteria)
{
criteria.Expect(instance =>
instance.Property.PropertyType.Equals(typeof(System.Boolean))
||
instance.Property.PropertyType.Equals(typeof(System.Nullable<System.Boolean>))
);
}
}
This works with NH 3.3 and the last version of Fluent.
I just had a NHibernate related problem where I forgot to map one property of a class.
A very simplified example:
public class MyClass
{
public virtual int ID { get; set; }
public virtual string SomeText { get; set; }
public virtual int SomeNumber { get; set; }
}
...and the mapping file:
<?xml version="1.0" encoding="utf-8" ?>
<hibernate-mapping xmlns="urn:nhibernate-mapping-2.2"
assembly="MyAssembly"
namespace="MyAssembly.MyNamespace">
<class name="MyClass" table="SomeTable">
<property name="ID" />
<property name="SomeText" />
</class>
</hibernate-mapping>
In this simple example, you can see the problem at once:
there is a property named "SomeNumber" in the class, but not in the mapping file.
So NHibernate will not map it and it will always be zero.
The real class had a lot more properties, so the problem was not as easy to see and it took me quite some time to figure out why SomeNumber always returned zero even though I was 100% sure that the value in the database was != zero.
So, here is my question:
Is there some simple way to find this out via NHibernate?
Like a compiler warning when a class is mapped, but some of its properties are not.
Or some query that I can run that shows me unmapped properties in mapped classes...you get the idea.
(Plus, it would be nice if I could exclude some legacy columns that I really don't want mapped.)
EDIT:
Okay, I looked at everything you proposed and decided to go with the meta-data API...that looks the easiest to understand for me.
Now that I know what to search for, I found some examples which helped me to get started.
So far, I have this:
Type type = typeof(MyClass);
IClassMetadata meta = MySessionFactory.GetClassMetadata(type);
PropertyInfo[] infos = type.GetProperties();
foreach (PropertyInfo info in infos)
{
if (meta.PropertyNames.Contains(info.Name))
{
Console.WriteLine("{0} is mapped!", info.Name);
}
else
{
Console.WriteLine("{0} is not mapped!", info.Name);
}
}
It nearly works, except one thing:
IClassMetadata.PropertyNames returns the names of all the properties except the ID.
To get the ID, I have to use IClassMetadata.IdentifierPropertyName.
Yes, I could save .PropertyNames in a new array, add .IdentifierPropertyName to it and search that array.
But this looks strange to me.
Is there no better way to get all mapped properties including the ID?
You could use the NHibernate meta-data API to find the mapped properties, and reflection to find all the properties.
Edit No, there isn't any other way list all the properties including the id. It isn't that hard to use:
foreach (PropertyInfo info in infos)
{
if (meta.PropertyNames.Contains(info.Name) || info.Name = meta.IdentifierPropertyName)
{
Console.WriteLine("{0} is mapped!", info.Name);
}
else
{
Console.WriteLine("{0} is not mapped!", info.Name);
}
}
There are two tools I'm aware of that can help with this:
Fluent NHibernate persistence specification testing
Nhibernate Ghostbuster
but they don't specifically address the problem you had with an unmapped property. The best solution is to write good unit tests that ensure that the properties you want to persist are persisted correctly. It's tedious but necessary.
nHibernate is giving the error : Custom type does not implement UserCollectionType: myApp.Domain.OrderLineCollection.
BindingList implements IList, so why is nHibernate trying to use UserCollectionType instead of IList?
public class OrderHeader
{
public virtual int OrderHeaderId { get; set; }
public virtual string OrderNumber { get; set; }
public virtual OrderLineCollection Line { get; set; }
}
public class OrderLineCollection : BindingList<OrderHeader> { }
public class OrderHeaderMap : ClassMap<OrderHeader>
{
public OrderHeaderMap()
{
WithTable("Orders");
Id(x => x.OrderHeaderId, "OrderId").GeneratedBy.Identity();
Map(x => x.OrderNumber);
HasMany(x => x.Line).WithKeyColumn("OrderHeaderId").AsList();
}
}
<list name="Line">
<key column="OrderHeaderId" />
<index />
<one-to-many class="myApp.Domain.OrderLine, myApp.Domain, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" />
</list>
NHibernate has it's own custom typed list which implements IList underneath.
I'm afraid you won't be able to use yours without creating nHibernate UserType.
But i might be wrong and would be glad to hear why. :)
You might want to check the XML that's created by fluentNHibernate - it's quite possible they take the type of the Line property and set it explicitly.
This should work if you don't set the type explicitly. I tried implementing a custom collection deriving from IList - and it worked when I didn't specify the type on the bag/list whatever in the mapping.
Ok, I did a quick test Arnis L. is right - it probably won't work without implementing UserCollectionType. In my experience, it's a pain to implement .
(somehow I remembered doing something like this but I guess my mind's playing tricks on me)
I look at the NHibernate source code and at least for PersistentBag and PersistentList NHibernate will instanciate a ArrayList object as the back end list, not a OrderLineCollection as one could thought. When you implement IUserColletionType there is a method who tells NHibernate what collection it should create, and also what Persistent collection Hibernate should use to sav. Take a look at this link might help a lot. But I still cant do Nhibernate work with BindingList.
I have the following class:
public class MyClass
{
private List<long> _myList = new List<long>();
public virtual string MyID { get; set; }
public virtual string MyData
{
get
{
return SomeStaticClass.Serialize(_myList);
}
set
{
_myList = SomeStaticClass.Deserialize<List<long>>(value);
}
}
public virtual List<long> MyList
{
get { return _myList; }
}
}
And the following mapping file:
<hibernate-mapping xmlns="urn:nhibernate-mapping-2.2"
assembly="MyNamespace"
namespace="MyNamespace">
<class name="MyNamespace.MyClass" table="MY_TABLE">
<id name="MyID" column="MY_ID" type="System.String">
<generator class="assigned"></generator>
</id>
<property name="MyData" column="MY_DATA"></property>
</class>
</hibernate-mapping>
When I try to run the following line:
session.Delete("From MyClass m");
I am getting a QuerySyntaxException with the message "MyClass is not mapped [From MyClass s]".
When I change the name of the "MyID" field to "ID" in the mapping file, the exception becomes
NHibernate.PropertyNotFoundException: Could not find a getter for property 'ID' in class 'MyNamespace.MyClass'.
so I am assuming it can find the mapping file. I made sure that the mapping file is an embedded resource, checked and dobule checked the namespace and class names in the mapping file. What may cause the error? I think it may relate to the MyList property which is not mapped but I am not sure since I am using non-mapped properties on my other classes without a problem.
EDIT: I tried overriding this class, with a class which has no "MyData" property and redefining "MyList" property as string. I am still receiving the same error for my overridden class.
EDIT 2: Tried with a very simple class with the same property names with same return types and only simple get; set; blocks. I still get the same error. I am almost sure that nhibernate can see my mapping files because if I change the name of a single property, it gives me PropertyNotFound instead of "class in not mapped".
How are you loading the hbms? If they are resources, make sure you've actually set the files to be embedded resources in Visual Studio
In case of mapping
<class name="MyClass" table="MY_TABLE">
you should use, for example:
session.CreateQuery("from MyClass")
but not:
session.CreateQuery("from MY_TABLE")
what about if you use
session.Delete("From MyNamespace.MyClass m");
I was just looking at the HQL reference and noticed in their cat example they use fully qualified objects, i.e. Eg.Cat.
You should set the related *.hbm.xml as Embedded Resource.
Make sure your "Build Action of the file" is "Embedded Resource".
I had this problem. I forgot to put hbm in the name of mapping XML files.
it seems a bit strange you are specifying the namespace twice in the mapping file. I would try just specifying the name attribute as just "MyClass" instead of "MyNamespace.MyClass" so it would be
<class name="MyClass" table="MY_TABLE">
I had a similar problem like this. Basically, I included a new project into the solution and I did not map the namespace in the hibernate.cfg.xml file.