在我正在开发的项目中,我在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无法解析我的复合属性,尽管事实上两个属性都是存在该属性的结果?
答案 0 :(得分:4)
就像@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惩罚的贪婪),这就是这样:
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。 免责声明:这是我个人博客的链接。
这可能是太多的信息,如果你因为某种原因对你的解决方案不满意,我个人会选择#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
语法的新手。