NHibernate: Enum to list and store values in datab

2019-04-14 01:51发布

问题:

I am using enum's and a custom Selector class to help choose between radiobuttons, dropdowns, checkboxes, etc. I am using NHibernate. With a single selection (radiobuttons, dropdowns), the value from attribute [Display(Name = "[Some Text]")] will be populated in the database table (NOTE: I am using an extension to use Display(Name)). However, with multiple selections (checkboxes, multilist), I cannot figure out how to get the values of the enum selections into the database.

Here are parts of my model (each in separate files) (EDIT: I gave them generic names so as not to further confuse the issue):

public enum MyEnum
{
    [Display(Name = "Text for enum1")]
    enum1,
    //Left out 2 - 10 for brevity
    [Display(Name = "Text for enum10")]
    enum10
}
...
public class MyEnumSelectorAttribute : SelectorAttribute
{
    public override IEnumerable<SelectListItem> GetItems()
    {
        return Selector.GetItemsFromEnum<MyEnum>();
    }
}
...
[Display(Name = "This is a checkboxlist (select one or more check boxes)?")]
[MyEnumSelector(BulkSelectionThreshold = 10)]
public virtual List<string> MyEnumCheckBox { get; set; }
...
public List<string> MyEnumCheckBox
{
    get { return Record.MyEnumCheckBox; }
    set { Record.MyEnumCheckBox = value; }
}

And here is the Selector.cs class (in case it's relevant to the problem) that help's to choose radiobuttons, checkboxes, dropdowns, etc.:

public class Selector
{
    public IEnumerable<SelectListItem> Items { get; set; }

    public string OptionLabel { get; set; }

    public bool AllowMultipleSelection { get; set; }

    public int BulkSelectionThreshold { get; set; }

    public static string GetEnumDescription(string value, Type enumType)
    {
        var fi = enumType.GetField(value.ToString());
        var display = fi
            .GetCustomAttributes(typeof(DisplayAttribute), false)
            .OfType<DisplayAttribute>()
            .FirstOrDefault();
        if (display != null)
        {
            return display.Name;
        }
        return value;
    }

    public static IEnumerable<SelectListItem> GetItemsFromEnum<T>
        (T selectedValue = default(T)) where T : struct
    {
        return from name in Enum.GetNames(typeof(T))
               let enumValue = Convert.ToString((T)Enum.Parse
                   (typeof(T), name, true))

               select new SelectListItem
               {
                   Text = GetEnumDescription(name, typeof(T)),
                   Value = enumValue,
                   Selected = enumValue.Equals(selectedValue)
               };
    }
}

public static class SelectorHelper
{
    public static IEnumerable<SelectListItem> ToSelectList
        (this IEnumerable data)
    {
        return new SelectList(data);
    }

    public static IEnumerable<SelectListItem> ToSelectList
        (this IEnumerable data, string dataValueField, 
        string dataTextField)
    {
        return new SelectList(data, dataValueField, dataTextField);
    }

    public static IEnumerable<SelectListItem> ToSelectList<T>
        (this IEnumerable<T> data, Expression<Func<T, object>> 
        dataValueFieldSelector, Expression<Func<T, string>> 
        dataTextFieldSelector)
    {
        var dataValueField = dataValueFieldSelector.ToPropertyInfo().Name;
        var dataTextField = dataTextFieldSelector.ToPropertyInfo().Name;
        return ToSelectList(data, dataValueField, dataTextField);
    }
}

The Selector class is paired with a template Selector.cshtml that has some logic to figure out which to pick (radiobutton, checkboxes, etc.).

I am getting various errors trying either List<string>, List<MyEnum>, IList<string>, IList<MyEnum>, IEnumerable<MyEnum> and IEnumerable<MyEnum>. This error only comes with checkboxes or multilists since they use List<string>. Dropdowns, for example, work fine with no errors. Here is a sample dropdown model (can reuse enum above) that works and will allow mapping to the DB through NHibernate:

[Required(ErrorMessage = "Please select one option")]
[Display(Name = "This is a dropdown list (select one option)?")]
[MyEnumSelector(BulkSelectionThreshold = 0)] //0 selects dropdown
public virtual MyEnum? MyEnumDropDown { get; set; }

public MyEnum? MyEnumDropDown
    {
        get { return Record.MyEnumDropDown; }
        set { Record.MyEnumDropDown = value; }
    }

Here are some of the errors I am getting based on what I've tried:

List<string> error:

NHibernate.Transaction.ITransactionFactory - DTC transaction prepre phase failed NHibernate.PropertyAccessException: Invalid Cast (check your mapping for property type mismatches); setter of MyNameSpace.Models.MyRecord ---> System.InvalidCastException: Unable to cast object of type 'NHibernate.Collection.Generic.PersistentGenericBag1[System.String]' to type 'System.Collections.Generic.List1[System.String]'.

List<MyEnum> error:

NHibernate.Transaction.ITransactionFactory - DTC transaction prepre phase failed System.InvalidCastException: Unable to cast object of type 'System.Collections.Generic.List1[MyNameSpace.Models.MyEnum]' to type 'System.Collections.Generic.ICollection1[System.String]'.

IList<string> error:

NHibernate.AdoNet.AbstractBatcher - Could not execute command: INSERT INTO MyEnumCheckBox (MyRecord_id, Value) VALUES (@p0, @p1) System.Data.SqlServerCe.SqlCeException (0x80004005): The specified table does not exist. [ MyEnumCheckBox ]

The other variations I tried were similar errors, except that if I used <MyEnum> it would show an error like this:

System.Collections.Generic.List1[MyNameSpace.Models.MyEnum]' to type 'System.Collections.Generic.ICollection1[System.String]'.

Any thoughts on how to use enum's in this scenario when trying to insert multiple selected enums using NHibernate?

回答1:

Since using lists is used for separate tables by default in Nhibernate, you'll have to use a mapping functions to map a single value from the database to a list of values in your model.

I've demonstrated how to do it here with a working sample, but I'll sum it up once more on your question.

There are 2 major mapping function types based on your problem. They both include that you:

  1. move your MyEnumSelector attribute from the Record class to your model class,
  2. change the type of your Record's MyEnumCheckBox property and
  3. change your model's MyEnumCheckBox property so that it maps to and from your Record's MyEnumCheckBox property

So those mapping functions are:

  1. int to List<MyEnumSelector> - this is the same technique I've shown in the linking post. It includes marking your MyEnumSelector enum with the [Flags] attribute and setting it's items to be of values of subsequent powers of 2. Your Record's MyEnumCheckBox property should be of type int. The mapping part then involves mapping the bits of the Record's MyEnumCheckBox property to and from the list of corresponding MyEnumSelector values
  2. string to List<MyEnumSelector> - this involves setting your Record's MyEnumCheckBox property to be of type string. The mapping part then involves mapping the delimited string of the Record's MyEnumCheckBox property to and from the list of corresponding MyEnumSelector values. Delimiter could be comma or semicolon or some other character that you won't use as a name of your MyEnumSelector item values.

Major differences of these 2 approaches are:

  1. Enum size - when using int as your database type your're limited to 32 bits of data. This means that you can't have more than 32 items in your enum. Strings don't have this kind of limitation
  2. Ease of searching the database for specific values - when using int, you'll have to deal with bit-wise database operators which are quite messy and not that easy to work with, while you could use simple LIKE operator when using string mapping
  3. Size used in database - int uses always 4 bytes (32-bits) while string mapping uses that amount (32-bits) for only one character inside the string (if it's of type char, varchar or text, and double the size - 64-bits when using nchar, nvarchar or ntext) so it will use much more space for each row in the database
  4. Speed of mapping - bit-wise mapping used in int mapping is quite fast while string mapping uses string manipulation functions which are much slower. This shouldn't be the problem though if you're dealing with a small amount of data. But if you're going to deal with a massive amount of data this could be a huge issue.


回答2:

I would make the checkbox values the int value for the enums.

A few suggestions:

  • Use System.DayOfWeek enum in place of your own enum.
  • An enum can be mapped as a custom type in NHibernate.
  • Since days of the week are one word, there's no need for a Display attribute. The control values should be the int value for the enum (you can direct cast).
  • Use partial views for the different control sets (dropdown, radio, etc.)


回答3:

PersistentGenericBag is definitely not a List<string>, but it does implement IList<string>:

public class PersistentGenericBag<T> : PersistentBag, IList<T>

When you use IList<string> instead, are you sure you get the exact same error from the same spot in the code?