[NHibernate]用一个实体类对应多个数据库表(三)
书接上回,解决目前最后一个问题:根据模版mapping自动生成各个可能的XXX专用mapping。
上回简单的谈了一下思路
但是,在什么时机呢,由谁来做呢?可以有很多的选择。比如说可以在编译之前手动执行一个tool做这件事,然后把新得到的那些mapping都编译进去。但是这意味着每次客户追加一个XXX的可能取值,我们都需要重新编译我们的dll。这无疑是很麻烦的。
鉴于目前还不会实现完全动态的加载新的mapping,我给自己设定的目标是:手动改一处配置,然后重启一下进程。另外,为了获得更好的性能,希望已经创建好的mapping在下一次不需要重新copy-replace。
于是思路变成在BuildSessionFactory之前,读配置文件中的一个list(包含XXX的可能取值),看对应的assembly是否已经存在。若不存在emit一个出来并存在硬盘上,使下一次不用再次创建。
代码示例如下
private readonly XmlSerializer m_serializer = new XmlSerializer(typeof(HbmMapping));
private void EmbeddedHbmIntoMoudle(ModuleBuilder modBuilder, string replaceFrom, string replaceTo)
{
var templateAsm = Assembly.Load(TemplateAssemblyName);
foreach (var resourceName in templateAsm.GetManifestResourceNames())
{
if (resourceName.StartsWith(ReplacableResourceNamePrefix))
{
var mapping = m_serializer.Deserialize(templateAsm.GetManifestResourceStream(resourceName)) as HbmMapping;
foreach (var item in mapping.Items)
{
if (item is HbmClass)
{
ReplaceClassEntityName(item as HbmClass, replaceFrom, replaceTo);
}
else if (item is HbmJoinedSubclass)
{
ReplaceJoinSubClassEntityName(item as HbmJoinedSubclass, replaceFrom, replaceTo);
}
else
{
//ignore
}
}
var stream = new MemoryStream();
m_serializer.Serialize(stream, mapping);
modBuilder.DefineManifestResource(resourceName, stream, ResourceAttributes.Public);
}
else
{
//ignore
}
}
}
其中只处理了HbmClass和HbmJoinedSubclass(因为项目中只用到了这两种。。。),内容就是替换其entity-name,table,many-to-one的entity-name等一切包含了XXX的地方。啊,对了,不要忘记HbmJoinedSubclass的extends(我就给忘了,结果UT跑不过)。
关于调用的时机么,恩,这里项目出现了一些变化,客户终于允许我们使用Spring,算是帮了大忙。毕竟对于我本人来说,定制Spring的行为比定制Nhibernate的行为熟悉多了。具体到这个问题,就是继承一下Spring实现的LocalSessionFactoryObject,在某个时点插入一点自己的代码就好了。具体什么时点呢?我们来看一下AfterPropertiesSet的代码(这是Spring做完注入之后调用的一个方法)
{
// Create Configuration instance.
Configuration config = NewConfiguration();
if (this.dbProvider != null)
{
config.SetProperty(Environment.ConnectionString, dbProvider.ConnectionString);
config.SetProperty(Environment.ConnectionProvider, typeof(DbProviderWrapper).AssemblyQualifiedName);
configTimeDbProvider = this.dbProvider;
}
if (ExposeTransactionAwareSessionFactory)
{
// Set ICurrentSessionContext implementation,
// providing the Spring-managed ISession s current Session.
// Can be overridden by a custom value for the corresponding Hibernate property
config.SetProperty(Environment.CurrentSessionContextClass, typeof(SpringSessionContext).AssemblyQualifiedName);
}
if (this.entityInterceptor != null)
{
// Set given entity interceptor at SessionFactory level.
config.SetInterceptor(this.entityInterceptor);
}
if (this.namingStrategy != null)
{
// Pass given naming strategy to Hibernate Configuration.
config.SetNamingStrategy(this.namingStrategy);
}
#if NH_2_1
if (this.typeDefinitions != null)
{
// Register specified Hibernate type definitions.
IDictionary<string, string> typedProperties = new Dictionary<string, string>();
foreach (DictionaryEntry entry in hibernateProperties)
{
typedProperties.Add((string) entry.Key, (string) entry.Value);
}
Dialect dialect = Dialect.GetDialect(typedProperties);
Mappings mappings = config.CreateMappings(dialect);
for (int i = 0; i < this.typeDefinitions.Length; i++)
{
IObjectDefinition typeDef = this.typeDefinitions[i];
Dictionary<string, string> typedParamMap = new Dictionary<string, string>();
foreach (DictionaryEntry entry in typeDef.PropertyValues)
{
typedParamMap.Add((string) entry.Key, (string) entry.Value);
}
mappings.AddTypeDef(typeDef.ObjectTypeName, typeDef.ObjectTypeName, typedParamMap);
}
}
#endif
if (this.filterDefinitions != null)
{
// Register specified NHibernate FilterDefinitions.
for (int i = 0; i < this.filterDefinitions.Length; i++)
{
config.AddFilterDefinition(this.filterDefinitions[i]);
}
}
#if NH_2_1
// check whether proxy factory has been initialized
if (config.GetProperty(Environment.ProxyFactoryFactoryClass) == null
&& (hibernateProperties == null || !hibernateProperties.Contains(Environment.ProxyFactoryFactoryClass)))
{
// nothing set by user, lets use Spring.NET's proxy factory factory
#region Logging
if (log.IsInfoEnabled)
{
log.Info("Setting proxy factory to Spring provided one as user did not specify any");
}
#endregion
config.Properties.Add(
Environment.ProxyFactoryFactoryClass, typeof(Bytecode.ProxyFactoryFactory).AssemblyQualifiedName);
}
#endif
if (this.hibernateProperties != null)
{
if (config.GetProperty(Environment.ConnectionProvider) != null &&
hibernateProperties.Contains(Environment.ConnectionProvider))
{
#region Logging
if (log.IsInfoEnabled)
{
log.Info("Overriding use of Spring's Hibernate Connection Provider with [" +
hibernateProperties[Environment.ConnectionProvider] + "]");
}
#endregion
config.Properties.Remove(Environment.ConnectionProvider);
}
Dictionary<string, string> genericHibernateProperties = new Dictionary<string, string>();
foreach (DictionaryEntry entry in hibernateProperties)
{
genericHibernateProperties.Add((string) entry.Key, (string) entry.Value);
}
config.AddProperties(genericHibernateProperties);
}
if (this.mappingAssemblies != null)
{
foreach (string assemblyName in mappingAssemblies)
{
config.AddAssembly(assemblyName);
}
}
if (this.mappingResources != null)
{
IResourceLoader loader = this.ResourceLoader;
if (loader == null)
{
loader = this.applicationContext;
}
foreach (string resourceName in mappingResources)
{
config.AddInputStream(loader.GetResource(resourceName).InputStream);
}
}
if (configFilenames != null)
{
foreach (string configFilename in configFilenames)
{
config.Configure(configFilename);
}
}
#if NH_2_1
// Tell Hibernate to eagerly compile the mappings that we registered,
// for availability of the mapping information in further processing.
PostProcessMappings(config);
config.BuildMappings();
if (this.entityCacheStrategies != null)
{
// Register cache strategies for mapped entities.
foreach (string className in this.entityCacheStrategies.Keys)
{
String[] strategyAndRegion = StringUtils.CommaDelimitedListToStringArray(this.entityCacheStrategies.GetProperty(className));
if (strategyAndRegion.Length > 1)
{
config.SetCacheConcurrencyStrategy(className, strategyAndRegion[0], strategyAndRegion[1]);
}
else if (strategyAndRegion.Length > 0)
{
config.SetCacheConcurrencyStrategy(className, strategyAndRegion[0]);
}
}
}
if (this.collectionCacheStrategies != null)
{
// Register cache strategies for mapped collections.
foreach (string collRole in collectionCacheStrategies.Keys)
{
string[] strategyAndRegion = StringUtils.CommaDelimitedListToStringArray(this.collectionCacheStrategies.GetProperty(collRole));
if (strategyAndRegion.Length > 1)
{
throw new Exception("Collection cache concurrency strategy region definition not supported yet");
//config.SetCollectionCacheConcurrencyStrategy(collRole, strategyAndRegion[0], strategyAndRegion[1]);
}
else if (strategyAndRegion.Length > 0)
{
config.SetCollectionCacheConcurrencyStrategy(collRole, strategyAndRegion[0]);
}
}
}
#endif
if (this.eventListeners != null)
{
// Register specified NHibernate event listeners.
foreach (DictionaryEntry entry in eventListeners)
{
ListenerType listenerType;
try
{
listenerType = (ListenerType) Enum.Parse(typeof (ListenerType), (string) entry.Key);
}
catch
{
throw new ArgumentException(string.Format("Unable to parse string '{0}' as valid {1}", entry.Key, typeof (ListenerType)));
}
object listenerObject = entry.Value;
if (listenerObject is ICollection)
{
ICollection listeners = (ICollection) listenerObject;
EventListeners listenerRegistry = config.EventListeners;
// create the array and check that types are valid at the same time
ArrayList items = new ArrayList(listeners);
object[] listenerArray = (object[])items.ToArray(listenerRegistry.GetListenerClassFor(listenerType));
config.SetListeners(listenerType, listenerArray);
}
else
{
config.SetListener(listenerType, listenerObject);
}
}
}
// Perform custom post-processing in subclasses.
PostProcessConfiguration(config);
#if NH_2_1
if (BytecodeProvider != null)
{
// set custom IBytecodeProvider
Environment.BytecodeProvider = BytecodeProvider;
}
else
{
// use Spring's as default
// Environment.BytecodeProvider = new Bytecode.BytecodeProvider(this.applicationContext);
}
#endif
// Build SessionFactory instance.
log.Info("Building new Hibernate SessionFactory");
this.configuration = config;
this.sessionFactory = NewSessionFactory(config);
AfterSessionFactoryCreation();
// set config time DB provider back to null
configTimeDbProvider = null;
}
有点长,是不是?简单说来,这个方法做了如下的事情
{
// Create Configuration instance.
Configuration config = NewConfiguration();
// set一些基本的东西,跟Mapping息息相关
// ...
// Tell Hibernate to eagerly compile the mappings that we registered,
// for availability of the mapping information in further processing.
PostProcessMappings(config);
config.BuildMappings();
// set跟Mapping没啥关系的东西
PostProcessConfiguration(config);
// set另一些我们不关系的东西
// Build SessionFactory instance.
log.Info("Building new Hibernate SessionFactory");
this.configuration = config;
this.sessionFactory = NewSessionFactory(config);
AfterSessionFactoryCreation();
// set config time DB provider back to null
configTimeDbProvider = null;
}
由此,我们就可以看出来,在config.BuildMappings()之前,我们必须完成我们的工作。我选择了override PostProcessMappings这个方法。
{
public IMappingAssemblyGenerator MappingAssemblyGenerator { get; set; }
protected override void PostProcessMappings(Configuration config)
{
base.PostProcessMappings(config);
foreach (var assembly in MappingAssemblyGenerator.Generate())
{
config.AddAssembly(assembly);
}
}
}
其中MappingAssemblyGenerator是一个用于copy-replace的类,核心的代码就是上面贴过的EmbeddedHbmIntoMoudle方法。
再贴一下调用EmbeddedHbmIntoMoudle并用emit创建dll并且保存至磁盘的方法
{
var rv = new List<Assembly>();
var templateDllPath = new StringBuilder(DllPath).Append(TemplateAssemblyName).Append(".dll").ToString();
var templateAssemblyFileLastWriteTime = GetTemplateAssemblyFileLastWriteTime(templateDllPath);
foreach (var replaceTo in ReplaceToList)
{
var sb = new StringBuilder(TargetAssemblyName).Append(".").Append(replaceTo);
var asmName = sb.ToString();
var dllName = sb.Append(".dll").ToString();
var dllPath = sb.Insert(0, DllPath).ToString();
if (NeedGenerate(dllPath, templateAssemblyFileLastWriteTime))
{
var asmBuilder = AppDomain.CurrentDomain.DefineDynamicAssembly(new AssemblyName(asmName), AssemblyBuilderAccess.RunAndSave, DllPath);
var modBuilder = asmBuilder.DefineDynamicModule(asmName, dllName);
EmbeddedHbmIntoMoudle(modBuilder, ReplaceFrom, replaceTo);
asmBuilder.Save(dllName);
}
rv.Add(Assembly.Load(File.ReadAllBytes(dllPath)));
}
return rv;
}
这个系列到这里就算完结了,虽然没有能够动态的实现创建mapping并加载,但还算是大体达到了客户的要求,也达到了我对自己的一个要求(手动改一处配置,然后重启一下进程就可以完成增加mapping),还算满意吧。
最后说一下,这个系列建立在NH2.1和Spring1.3的基础上。