最近遇到了一个让人抓狂的性能问题。生产环境里有一张表的数据量目前达到了 70 万条。结果发现无论是匹配主键的查询还是更新,执行一条语句居然需要 3.5 秒!如果把 NH Prof 中截获的 SQL 语句拿到 PL/SQL Developer 里执行,就只需几十毫秒。一开始还以为是NH的问题,后来发现其实另有隐情。
介绍一下环境先。数据库使用 Oracle10g,所有字符类型的字段都是 varchar2 [1]。所有的主键都使用 Guid,在数据库里是 varchar2(36) 类型,相应的,实体的 Id 属性的类型是 string。ORM 使用的是 NHibernate 2.1.0 和 FluentNHibernate1.1。
经过一番排查之后发现,问题的根源是 NH 将 SQL 语句传递给 Oracle 时,所有字符型的参数都是 nvarchar2 类型,而数据库里对应的字段却是 varchar2 类型,这将导致 Oracle 无法使用索引,终于造成全表扫描,所以数据量稍大就慢得不行。
第一种解决方法是,把数据库中所有的字符型字段的类型由 varchar2 更改为 nvarchar2,出于种种原因我们不希望这么做。
第二种解决方法是,让 NH 把 varchar2 作为参数类型传递给 Oracle。
事实上,NH 默认把 .net 的 string 映射为 DbType.String [2],把 DbType.String 映射为 nvarchar2 [3]。把 DbType.AnsiString 映射为 varchar2 [4]。
所以对于查询比较简单,只要把 HQL 的参数类型指定为 AnsiString 就行了。
var query = Session.CreateQuery( @"select t from Region as t where t.Id = :Id" ) .SetAnsiString( "Id" , id); |
var query = Session.CreateQuery( @"select t from Region as t where t.Id in (:Ids)" ) .SetParameterList( "Ids" , ids.ToList(), NHibernateUtil.AnsiString); |
但是如何设置 Update 和 Delete 语句的参数类型呢?这里有个小小的秘技,把映射文件里的属性类型指定为“AnsiString”即可。
public class RegionMap : TreeNodeMap<Region> { public RegionMap() { Table( "INFRA_REGION" ); Id(t => t.Id, "REGION_ID" ).CustomType( "AnsiString" ); ... } } |
注意 一定要使用 CustomType() 而不是 CustomSqlType()。
当然了,要是把每一个配置文件都改一遍实在很烦,好像项目使用了 Fluent NHibernate,只要添加一个 IdConvention 就行了。
public class IdConvention : FluentNHibernate.Conventions.IIdConvention { public void Apply(FluentNHibernate.Conventions.Instances.IIdentityInstance instance) { instance.CustomType( "AnsiString" ); } } |
想要彻底一点的话,可以再加一个 string 类型的 property 的 convention。
public class StringPropertyConvention : IPropertyConvention, IPropertyConventionAcceptance { public void Accept(IAcceptanceCriteria<IPropertyInspector> criteria) { criteria.Expect(x => x.Property.PropertyType == typeof ( string )); } public void Apply(IPropertyInstance instance) { instance.CustomType( "AnsiString" ); } } |
把这两个 Convention 加到配置里面:
Session[ "SessionFactory" ] = Fluently.Configure() .Database(OracleClientConfiguration.Oracle10 .Dialect<Oracle10gDialect>() .ConnectionString( "User ID=iBlast;Password=不可说;Data Source=Moki" ) .QuerySubstitutions( "true 1, false 0, yes 'Y', no 'N'" ) .UseOuterJoin() .ProxyFactoryFactory<ProxyFactoryFactory>() .AdoNetBatchSize(1000) .Driver<OracleClientDriver>()) .Mappings(m => { m.HbmMappings.AddFromAssembly(Assembly.Load( "Infrastructure.Repositories" )); m.FluentMappings.AddFromAssembly(Assembly.Load( "Infrastructure.Repositories" )) .Conventions.Add<EnumConvention>() .Conventions.Add<HasManyConvention>() .Conventions.Add<HasManyToManyConvention>() .Conventions.Add<StringPropertyConvention>() .Conventions.Add<IdConvention>() .ExportTo( @"F:\temp\" ); }) .BuildSessionFactory(); |
注意倒数第二行的 .ExportTo(@"F:\temp\") 是为了测试一下生成的映射文件对不对而把映射文件输出到了 “F:\temp\”,映射文件应该像这个样子:
<hibernate-mapping xmlns= "urn:nhibernate-mapping-2.2" default -access= "property" auto-import= "true" default -cascade= "none" default -lazy= "true" > < class xmlns= "urn:nhibernate-mapping-2.2" dynamic-insert= "true" dynamic-update= "true" mutable= "true" where = "IsDelete=0" name= "Dawn.HIS.Infrastructure.Core.Data.Region, Infrastructure.Core, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" table= "INFRA_REGION" > <id name= "Id" type= "AnsiString" > <column name= "REGION_ID" /> <generator class = "assigned" /> </id> <version name= "Version" type= "System.Int32, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" > <column name= "Version" /> </version> <property name= "CreateTime" type= "System.DateTime, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" > <column name= "CREATETIME" /> </property> <property name= "Name" type= "AnsiString" > <column name= "NAME" /> </property> ... </ class > </hibernate-mapping> |
[1] 之所以使用 varchar2 而不是 nvarchar2,除了考虑 varchar2 可以节省空间之外,主要是为了避免 nvarchar2 排序时的性能问题。
[2] 见 NHibernate-2.1.0.GA-src\src\NHibernate\Type\TypeFactory.cs 第 197 行。
[3] 见 NHibernate-2.1.0.GA-src\src\NHibernate\Dialect\Oracle8iDialect.cs 第 92 行。
[4] 见 NHibernate-2.1.0.GA-src\src\NHibernate\Dialect\Oracle8iDialect.cs 第 88 行。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· 10年+ .NET Coder 心语 ── 封装的思维:从隐藏、稳定开始理解其本质意义
· 地球OL攻略 —— 某应届生求职总结
· 提示词工程——AI应用必不可少的技术
· 字符编码:从基础到乱码解决
· Open-Sora 2.0 重磅开源!