让Linq To NHibernate支持Sql Server自带的全文检索

最近在使用NHibernate重构一个项目,好几处需要用到全文检索混搭其他一些条件进行的列表搜索。而大家都知道NHibernate本身是不支持全文检索的,网络上能找到的几处文章说来大概有以下几种方式

1. 修改hibernate.cfg.xml来扩展contains, freetext谓词到HQL, 参见:http://nhforge.org/blogs/nhibernate/archive/2009/03/13/registering-freetext-or-contains-functions-into-a-nhibernate-dialect.aspx

这种方法使得查询只能通过HQL进行,对于熟悉HQL的同学是个不错的选择,对于列表查询,获取列表总数的select count(*)和select *是两条HQL,需要写两次HQL的where条件或拼接HQL,因此个人不是很喜欢。

 

2. 为ICriteria查询添加SQL表达式参见:http://archive.cnblogs.com/a/1289840/

这个方法很棒,也可以通过 criteria.setProjection(Projections.rowCount()).uniqueResult() 来直接获得记录总数,对CreateCriteria不熟悉的同学们要注意文中的Expression.Sql里的Expression不是System.Linq.Expressions.Expression, 而是 NHibernate.Criterion.Expression

 

3. 基于Lucene的第三方扩展 NHibernate.Search : http://www.cnblogs.com/lonely7345/archive/2009/03/17/1413836.html
这个对项目的改动太大,而且手上的项目Sql Server自身的搜索就够用了,还没有复杂到要引入Lucene,因此也没有去具体了解。

 

4. 由于大量使用了Linq,因此自然希望全文搜索功能能够和Linq表达式无缝连接,因此这里介绍一个我自己折腾的Linq方法
先看下最终的调用方式:

var customers = Hibernate.Session.Query<Ares.Model.Sales.Customer>()
    .Where
    (
        u => u.Company.Name.ContainsSearch("公司", u.Company.Phone)
    );

这里的含义是在Company.Name和Company.Phone中查找指定的关键词,ContainsSearch方法最后的参数是params的,因此可以在多个字段中搜索

 

下面来看看实现过程:

首先参见了园子里YJingLee的一篇文章:NHibernate3剖析:Query篇之NHibernate.Linq自定义扩展

有了这个知识基础,我们现在只需要自定义一个Linq扩展方法,然后让NHibernate把这个扩展方法映射成contains
或者freetext就可以了,首先定义一个Linq扩展方法:

public static class MyHibernateLinqExtensions
{
    /// <summary>
    /// 对字段进行模式搜索
    /// </summary>
    /// <param name="source"></param>
    /// <param name="keyword">关键字</param>
    /// <param name="otherProperties">其他用于搜索的字段</param>
    /// <returns></returns>
    public static bool ContainsSearch(this string source, string keyword, params string[] otherProperties)
    {
        throw new NotSupportedException("仅用于数据库搜索");
    }
}

 

查看HqlTreeBuilder,其中有个BooleanMethodCall可以帮忙:(以下是NHibernate的官方源码片段)

public HqlBooleanMethodCall BooleanMethodCall(string methodName, IEnumerable<HqlExpression> parameters)
{
    return new HqlBooleanMethodCall(_factory, methodName, parameters);
}

我们看一下Sql Server中contains谓词的调用语法:

select * from customer
where contains((company, phone, address), '熊猫手机')

因此contains谓词的调用正是一个返回boolean类型的方法调用,参数 IEnumerable<HqlExpression> parameters是这个函数的参数的表达式形式列表

要注意的是contains和freetext其实只有两个参数,第一个参数的实现方式可以看成是一个方法调用的结果,只是这个方法的methodName = string.Empty

下面搬上整个扩展类

View Code
 1 using System;
 2 using System.Collections.Generic;
 3 using System.Linq;
 4 using System.Linq.Expressions;
 5 using System.Text;
 6 using NHibernate;
 7 using NHibernate.Linq;
 8 using NHibernate.Hql;
 9 using NHibernate.Hql.Ast;
10 using NHibernate.Hql.Ast.ANTLR;
11 
12 namespace Ares.Service.Linq
13 {
14     public static class MyHibernateLinqExtensions
15     {
16         /// <summary>
17         /// 对字段进行模式搜索
18         /// </summary>
19         /// <param name="source"></param>
20         /// <param name="keyword">关键字</param>
21         /// <param name="otherProperties">其他用于搜索的字段</param>
22         /// <returns></returns>
23         public static bool ContainsSearch(this string source, string keyword, params string[] otherProperties)
24         {
25             throw new NotSupportedException("仅用于数据库搜索");
26         }
27 
28         /// <summary>
29         /// 对字段进行开放式搜索
30         /// </summary>
31         /// <param name="source"></param>
32         /// <param name="keyword">关键字</param>
33         /// <param name="otherFields">其他用于搜索的字段</param>
34         /// <returns></returns>
35         public static bool FreeSearch(this string source, string keyword, params string[] otherFields)
36         {
37             throw new NotSupportedException("仅用于数据库搜索");
38         }
39     }
40 
41     class ContainsSearchGenerator : NHibernate.Linq.Functions.BaseHqlGeneratorForMethod
42     {
43         public ContainsSearchGenerator()
44         {
45             SupportedMethods = new[]
46             {
47                 ReflectionHelper.GetMethodDefinition
48                 (
49                     () => MyHibernateLinqExtensions.ContainsSearch(null, null, null)
50                 )
51             };
52         }
53 
54         public override NHibernate.Hql.Ast.HqlTreeNode BuildHql(System.Reflection.MethodInfo method, Expression targetObject, System.Collections.ObjectModel.ReadOnlyCollection<Expression> arguments, NHibernate.Hql.Ast.HqlTreeBuilder treeBuilder, NHibernate.Linq.Visitors.IHqlExpressionVisitor visitor)
55         {
56             var pms = arguments.Select(p => visitor.Visit(p).AsExpression()).ToArray();
57 
58             var fields = treeBuilder.MethodCall("", pms.Where(p => p != pms[1]).ToArray());
59 
60             return treeBuilder.BooleanMethodCall("contains", new HqlExpression[] { fields, pms[1] });
61         }
62     }
63 
64     class FreeSearchGenerator : NHibernate.Linq.Functions.BaseHqlGeneratorForMethod
65     {
66         public FreeSearchGenerator()
67         {
68             SupportedMethods = new[]
69             {
70                 ReflectionHelper.GetMethodDefinition
71                 (
72                     () => MyHibernateLinqExtensions.FreeSearch(null, null, null)
73                 )
74             };
75         }
76 
77         public override NHibernate.Hql.Ast.HqlTreeNode BuildHql(System.Reflection.MethodInfo method, Expression targetObject, System.Collections.ObjectModel.ReadOnlyCollection<Expression> arguments, NHibernate.Hql.Ast.HqlTreeBuilder treeBuilder, NHibernate.Linq.Visitors.IHqlExpressionVisitor visitor)
78         {
79             var pms = arguments.Select(p => visitor.Visit(p).AsExpression()).ToArray();
80 
81             var fields = treeBuilder.MethodCall("", pms.Where(p => p != pms[1]).ToArray());
82 
83             return treeBuilder.BooleanMethodCall("freetext", new HqlExpression[] { fields, pms[1] });
84         }
85     }
86 
87     class MyLinqToHqlGeneratorRegistry : NHibernate.Linq.Functions.DefaultLinqToHqlGeneratorsRegistry
88     {
89         public MyLinqToHqlGeneratorRegistry()
90         {
91             RegisterGenerator(ReflectionHelper.GetMethodDefinition(() => MyHibernateLinqExtensions.ContainsSearch(null, null, null)), new ContainsSearchGenerator());
92             RegisterGenerator(ReflectionHelper.GetMethodDefinition(() => MyHibernateLinqExtensions.FreeSearch(null, null, null)), new FreeSearchGenerator());
93         }
94     }
95 }

注册这个扩展的方法(在实现Factory方法的地方):

private static ISessionFactory BuildFactory()
{
    var cfg = new Configuration();
//大家实现SessionFactory的方法可能会略有区别,重要的是在BuildSessionFactory()之前,调用下面的方法将扩展类注册到你的Configuration对象即可
            cfg.LinqToHqlGeneratorsRegistry<Ares.Service.Linq.MyLinqToHqlGeneratorRegistry>();

    cfg = cfg.Configure();
    return cfg.BuildSessionFactory();
}

 

现在我们就可以将全文搜索和其他各种Linq方法一起混合调用了。

posted @ 2012-06-02 13:14  疯子阿飞  阅读(2692)  评论(0编辑  收藏  举报