无法使用QueryOver解析复合属性 - c#

在我正在从事的项目中,我在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为什么无法解析我的复合属性?

参考方案

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

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

但是,根据您的情况,可能有一些变通办法。

如果您的解决方案对您有用,那么这很可能就是您应该采用的解决方案,因为它是如此简单。但是,您还可以执行其他一些操作:

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

我不确定您使用的是哪种数据库引擎,但是如果它支持计算列,那么您实际上可以在数据库中创建表示DisplayName的计算列。

以SQL Server为例:

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

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

不幸的是,Projections.Concat不能采用任意的Projections,因此您必须使用Projections.SqlFunction(无论如何Projections.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>();

告诉QueryOver在SQL中访问DisplayName属性意味着什么

这很复杂,但是如果您想在QueryOver查询中使用DisplayName,则实际上可以告诉QueryOver该属性应转换为什么访问权限。

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

无论如何,如果您感到好奇(或者更可能是QueryOver惩罚的glut嘴),那么情况将是这样:

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。免责声明:这是指向我的个人博客的链接。

这可能是太多的信息,如果您出于某种原因对解决方案不满意,我个人将排名第一,然后排名第二。

为什么要使用Func <string>而不是string? - c#

为什么要使用Func<string>而不是string?我的问题特别是关于this回购。有问题的行是22: private static Func<string> getToken = () => Environment.GetEnvironmentVariable("GitHubToken", Enviro…

无法从ArrayList <String>转换为List <Comparable> - java

当我写下面的代码时,编译器说 无法从ArrayList<String>转换为List<Comparable>private List<Comparable> get(){ return new ArrayList<String>(); } 但是当我用通配符编写返回类型时,代码会编译。private List&l…

Div单击与单选按钮相同吗? - php

有没有一种方法可以使div上的click事件与表单环境中的单选按钮相同?我只希望下面的div提交值,单选按钮很丑代码输出如下:<input id="radio-2011-06-08" value="2011-06-08" type="radio" name="radio_date&#…

将谓词<T>转换为Func <T,bool> - c#

我有一个包含成员Predicate的类,希望在Linq表达式中使用该类:using System.Linq; class MyClass { public bool DoAllHaveSomeProperty() { return m_instrumentList.All(m_filterExpression); } private IEnumerable&…

故障排除“警告:session_start():无法发送会话高速缓存限制器-标头已发送” - php

我收到警告:session_start()[function.session-start]:无法发送会话缓存限制器-标头已发送(错误输出开始如果我将表单数据提交到其他文件进行处理,则可以正常工作。但是,如果我将表单数据提交到同一页面,则会出现此错误。请建议<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0…