无法使用QueryOver解析复合属性

时间:2015-03-17 15:48:36

标签: c# nhibernate

在我正在开发的项目中,我在NHibernate中采用了较新的QueryOver语法。但是,我在复合属性上实现排序时遇到问题。

我查询的模型看起来像这样:

public class Person
{
    public virtual int Id { get; set; }
    public virtual string FirstName { get; set; }
    public virtual string LastName { get; set; }

    // Not really relevant; it has an ID, but that's all we care about
    // for this question.
    public virtual Group Group { get; set; }

    // This is the culprit of my troubles.
    public virtual string DisplayName 
    {
        get { return LastName + ", " + FirstName; }
    }
}

...我的映射看起来像这样:

public class PersonMap : ClassMap<Person>
{
    Table("Persons");

    Id(x => x.Id);
    Map(x => x.FirstName);
    Map(x => x.LastName);

    References(x => x.Group)
        .Not.Nullable()
        .Column("GroupId")
        .Fetch.Join();
}

注意: DisplayName仅存在于服务器/客户端堆栈中!不在数据库方面。

但是,问题发生在哪里:我的存储库代码。

public class PersonRepository
{
    // ...Other methods...

    public static IEnumerable<Person> GetPeopleByGroup(int groupId)
    {
        // This just gets a cached NHibernate session
        using(var session = DataContext.GetSession())
        {
            var results = session
                .QueryOver<Person>()
                .Where(p => p.Group.GroupId == groupId)
                // Exception thrown here!
                .OrderBy(p => p.DisplayName)
                .List().ToList();

            return results;
        }
    }
}

据我所知,这应该有效。 问题:为什么NHibernate无法解析我的复合属性,尽管事实上两个属性都是存在该属性的结果?

2 个答案:

答案 0 :(得分:4)

就像@Radim Köhler所指出的那样,黄色的QueryOver规则几乎是“如果没有映射,则无法对其进行查询”

即使你的属性定义很简单,NHibernate也不会深入研究该属性并尝试理解实现,然后将该实现转换为SQL。

但是,根据您的具体情况,可能会有一些解决方法。

如果您的解决方案适合您,那么这可能是您应该使用的,因为它非常简单。但是,您还可以做一些其他事情:

  1. 使用计算列并将其映射到DisplayName

    我不确定你正在使用什么数据库引擎,但是如果它支持计算列,那么你实际上可以在数据库中创建一个代表DisplayName的计算列。

    在SQL Server中,例如:

    alter table [Person] add [DisplayName] as [LastName] + N', ' + [FirstName]
    

    这很简单,但从关注点分离的角度来看,让数据库引擎关注特定行的列的显示方式可能是不正确的。

  2. 使用Projection

    不幸的是,Projections.Concat不会随意Projections,因此您必须使用Projections.SqlFunctionProjections.Concat无论如何使用)。你最终会得到这样的东西:

    var orderByProjection = 
        Projections.SqlFunction(
            "concat",
            NHibernateUtil.String,
            Projections.Property<Person>(p => p.LastName),
            Projections.Constant(", "),
            Projections.Property<Person>(p => p.FirstName));
    
    var people = session.QueryOver<Person>()
        .OrderBy(orderByProjection).Asc
        .List<Person>();
    
  3. 告诉QueryOver在SQL中访问DisplayName属性的含义

    这非常复杂,但是如果你想在QueryOver查询中使用DisplayName,你实际上可以告诉QueryOver访问该属性应该转换成什么。

    我实际上不会推荐这个,因为它非常复杂并且它重复了逻辑(现在有两个地方定义了DisplayName)。也就是说,它可能对处于类似情况的其他人有用。

    无论如何,如果你很好奇(或者更可能是对QueryOver惩罚的贪婪),这就是这样:

    public static class PersonExtensions
    {
        /// <summary>Builds correct property access for use inside of 
        /// a projection.
        /// </summary>
        private static string BuildPropertyName(string alias, string property)
        {
            if (!string.IsNullOrEmpty(alias))
            {
                return string.Format("{0}.{1}", alias, property);
            }
    
            return property;
        }
    
        /// <summary>
        /// Instructs QueryOver how to process the `DisplayName` property access
        /// into valid SQL.
        /// </summary>
        public static IProjection ProcessDisplayName(
            System.Linq.Expressions.Expression expression)
        {
            Expression<Func<Person, string>> firstName = p => p.FirstName;
            Expression<Func<Person, string>> lastName = p => p.LastName;
    
            string aliasName = ExpressionProcessor.FindMemberExpression(expression);
    
            string firstNameName = 
                ExpressionProcessor.FindMemberExpression(firstName.Body);
            string lastNameName = 
                ExpressionProcessor.FindMemberExpression(lastName.Body);
    
            PropertyProjection firstNameProjection = 
                Projections.Property(BuildPropertyName(aliasName, firstNameName));
            PropertyProjection lastNameProjection = 
                Projections.Property(BuildPropertyName(aliasName, lastNameName));
    
            return Projections.SqlFunction(
                "concat",
                NHibernateUtil.String,
                lastNameProjection,
                Projections.Constant(", "),
                firstNameProjection);
        }
    }
    

    然后,您需要使用NHibernate注册处理逻辑,可能就在您的其他配置代码之后:

    ExpressionProcessor.RegisterCustomProjection(
        () => default(Person).DisplayName,
        expr => PersonExtensions.ProcessDisplayName(expr.Expression));
    

    最后,您可以在QueryOver查询中使用您的(未映射)属性:

    var people = session.QueryOver<Person>()
        .OrderBy(p => p.DisplayName).Asc            
        .List<Person>();
    

    生成以下SQL:

    SELECT 
        this_.Id as Id0_0_,
        this_.FirstName as FirstName0_0_,
        this_.LastName as LastName0_0_
    FROM 
        Person this_ 
    ORDER BY 
        (this_.LastName + ', ' + this_.FirstName) asc
    

    您可以找到有关此技术的更多信息here免责声明:这是我个人博客的链接。

  4. 这可能是太多的信息,如果你因为某种原因对你的解决方案不满意,我个人会选择#1然后#2。

答案 1 :(得分:1)

此问题的“快速而肮脏”的解决方案是OrderBy姓氏,然后是名字。

var results = session
    .QueryOver<Person>()
    .Where(p => p.Group.GroupId == groupId)
    .OrderBy(p => p.LastName).Asc()
    .OrderBy(p => p.FirstName).Asc()
    .List().ToList();

我本可以做一个投影,但我觉得它不太可读。无论如何,给出样本人员名单......

John Smith
Aberforth Scrooge
Tim Dumbledore
Giselle Potter
John Bane
Kit-Kat Chunky

...基于我的应用规则的'正确'顺序,以及此代码生成的列表

John Bane
Kit-Kat Chunky
Tim Dumbledore
Giselle Potter
Aberforth Scrooge
John Smith

案件结束......现在。我不怀疑有更好的方法来做到这一点;毕竟,QueryOver语法的新手。