让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
下面搬上整个扩展类
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方法一起混合调用了。