NHibernate 是一个基于.Net 的针对关系型数据库的对象持久化类库.Nhibernate 来源于非常优秀的基于Java的Hibernate 关系型持久化工具.
NHibernate源码分析之开篇: 计划和安排
只从使用NHibernate以来请被其强大的功能和使用的简洁所吸引.
为了进一步研究NHibernate决定分析其源代码如有感兴趣者欢迎一起研究.
这里列出了将要分析的部分:
官方源码下载地址:
http://sourceforge.net/projects/nhibernate/
NHibernate配置和持久对象映射文件
NHibernate配置
有三种方式来存放nhibernate的配置
1 作为单独的一节放在相应程序的配置文件中对于执行文件或类库为文件名称后加.config对于asp.net则是放在web.config中.这种方式必须在配置文件的configSetions中声明nhibernate的配置节
配置内容由Cfg.Environment类来读取该类所有成员均为静态的另外它还定义了配置中key值的常数.
2. 放在一个单独的配置文件中默认为hibernate.cfg.xm l使用时必须调用Cfg.Configuration.Config().如不是默认的配置文件名还必须指明配置文件名称.这种方式最适合多数据库的情况可以为每个数据库建立一个配置文件.
3. 手工在程序中加入配置内容最后将加入到Cfg.Configuration.Properties属性中此属性为一IDictionary对象并且为public其余的就不用多话了吧.
下面对几个重要的key值说明一下:
hibernate.connection.provider
连接提供者取值必须是实现了IConnectionProvider接口的类的全名当前版本只能取值NHibernate.Connection.DriverConnectionProvider;
hibernate.connection.driver_class
数据驱动类取值必须是实现了IDriver接口的类的全名常用的选择有NHibernate.Driver.SqlClientDriver NHibernate.Driver.OleDbDriver等;
hibernate.dialect
数据库方言取值必须是继承之Dialect的类的全名最常用的就是NHibernate.Dialect.MsSql2000Dialect了 其它的没用过不清楚能不能正常使用;
hibernate.connection.connection_string
连接字符串取值与driver_class对应即可;
hibernate.show_sql
指明是否在log4net日志中显示sql语句主要用于调试取值为true或false;
完整的配置key列表请查看Cfg.Environment类中的常数声明.
持久对象映射文件
nhibernate为我们提供了很多方式将持久对象映射文件加入到Cfg.Configuration类下面将其一一列出:
Addxm lFile:加入包括对象映射信息的文件;
Addxm lString:加入包含映射信息的字符串;
AddDocument:加入包含映射信息的xm ldocument;
AddInputStream:加入包含映射信息的输入流;
Addxm lReader:加入包含映射信息的xm lReader;
AddResource:加入指定程序集的映射信息资源;
AddClass:加入以类名指定的映射信息资源映射文件必须为classname.hbm.xm l;
AddAssembly:加入指定程序集名称的映射信息资源
注意:如果映射信息为文件形式包括加入到程序集资源的文件那么文件名必须以.hbm.xm l结尾.
NHibernate架构分析uml图
从图中可以看到Session和SessionFactory是NHibernate的核心部分.
SessionFactory维护到持久机制(数据库)的连接并对它们进行管理同时还保存着所有持久对象的映射信息.
SessionFactory由Configuration.BuildSessionFactory创建这个对象一般使用Singleton模式.
Session用于将对象持久化支持数据库事务另外Session还提供了强大的数据加载功能.
Session由SessionFactory创建.
其它对象说明:
IConnectionProvider: 连接提供者接口负责与数据进行连接;
Dialect: 数据库方言;
CollectionPersister: 集合持久化类;
IClassPersister: 类持久化接口定义了基本的CRUD操作;
TransactionFactory: 数据库事务工厂;
IInterceptor: 拦截器接口用于在操作执行时进行一些处理典型的就是记录操作日志;
NHibernate源码分析之一: 配置信息
配置信息用于指定NH以何种方式访问数据库 根据这些配置信息 NH动态的创建数据访问对象并与数据库进行交互. 除了.net类库自带的Odbc OleDb OracleClient和SqlClient访问方式外 在0.2版中 NH增加了用于访问MySQL和Firebird的访问方式 这两种访问方式由第三方组件提供 mono的用户应该高兴了. :)
NH的配置有两种存放方式
存放在应用程序集的配置文件中 对于Web应用程序则存放在Web.config中. 这种方式必须指定配置节的处理程序(类);
存放在一个单独的xm l文件中 使用这种方式我们必须在程序中显式的加载配置文件 本文后面有详细说明. 此方式有一个优点 就是在多数据库的情况下 可以用不同的配置文件与各个数据库进行对应.
配置内容
先来看看配置内容 下列是一个简单的配置例子:
连接提供者取值必须是实现了IConnectionProvider接口的类的全名当前版本只能取值NHibernate.Connection.DriverConnectionProvider.
数据库方言取值必须是继承之Dialect的类的全名最常用的就是NHibernate.Dialect.MsSql2000Dialect了吧 谁让它是M$的了.
数据驱动类取值必须是实现了IDriver接口的类的全名常用的选择有NHibernate.Driver.SqlClientDriver NHibernate.Driver.OleDbDriver等 不过现在又多了ByteFXDataDriver(访问MySQL).
连接字符串取值要与driver_class指定的数据驱动类对应.
配置节处理程序
因为NH的配置信息为自定义配置节 所以必须指定配置节处理程序 NH的配置内容采用key/value形式这和预定义配置节appSettings是一样的我们只要用.net内置的配置节处理程序就可以处理NH的配置内容了 这个处理key/value形式的类就是NameValueSetionHandler.
nhibernate配置节的声明如下:
注意Version的值对于不同的.net fr amework版本取值也可能不一样.
在nh中 Environment类用于读取配置信息 代码如下
//*** Environment.cs - 65行 ***
static Environment()
{
NameValueCollection props = System.Configuration.ConfigurationSettings.GetConfig("nhibernate") as NameValueCollection;
if (props==null)
{
return;
}
foreach(string key in props.Keys)
{
properties[key] = props[key];
}
}
这是一个静态构造函数 在静态成员首次调用时执行.
配置信息放在properties集合中.
//*** Environment.cs - 90行 ***
public static IDictionary Properties
{
get
{
IDictionary copy = new Hashtable(properties.Count);
foreach(DictionaryEntry de in properties)
{
copy[de.Key] = de.Value;
}
return copy;
}
}
Properties属性用于访问配置信息 注意这里并没有直接返回properties 而是复制了一个集合用于返回.
曾有网友问为什么不是直接返回properties 可能的原因是如果返回properties(即引用)话 那么配置信息将是共享的 如果在程序中修改了properties 那么将影响到其它地方.
另外Environment类中还定义了一些属性名称常数.
在程序中操作配置信息
除了在配置文件中指定nh的配置信息外 nh还允许我们在程序中操作配置信息 这对于一些敏感的数据如数据库连接串提供了一种安全的操作方法(可以在程序中加入连接串属性而不用将其存储在配置文件中).
Configuration类提供提供两个方法和一个属性用于操作配置信息.
//*** Configuration.cs - 637行 ***
public Configuration AddProperties(IDictionary properties)
{
foreach(DictionaryEntry de in properties)
{
this.properties.Add(de.Key de.Value);
}
return this;
}
将一个数据字典对象加入到配置属性中.
//*** Configuration.cs - 646行 ***
public Configuration SetProperty(string name string value)
{
properties[name] = value;
return this;
}
设置指定的属性的值 name应使用Environment类中定义的那些属性名称常数.
//*** Configuration.cs - 625行 ***
public IDictionary Properties
{
get { return properties; }
set { this.properties = value; }
}
这个就不用多说的吧 用dotNet的人都知道.有了Properties 想干啥就干啥吧 :-)
NHibernate源码分析之一续: 对象映射
1. 持久对象映射文件
关于持久对象映射文件这里就不多说了可参考nhibernate的例子和文档.
在nhibernate源代码的根目录里有一个nhibernate-mapping-2.0.xsd文档这个文档是nhibernate用来对映射文件进行验证的我们也可以借助相关软件用这个文档来验证映射文件的有效性.
2. 映射信息的读取
通过Configuration类可以用多种方式读取映射信息一些以Add开头的方法就是用来加入映射信息的这些方法最终将调用Add(xm lDocument doc).
//** Configuration.cs **
private Hashtable classes = new Hashtable();
classes集合用于存放所有的持久对象映射信息
它的Key为持久类的类型;Value为PermissionClass类的子类.
private void Add(xm lDocument doc)
{
try
{
Binder.dialect = Dialect.Dialect.GetDialect(properties);
Binder.BindRoot( doc CreateMappings());
}
catch (MappingException me)
{
log.Error("Could not compile the mapping document" me);
throw me;
} // end try/catch
}
AddDocument方法调用Binder的静态方法BindRoot来绑定持久类映射信息.CreateMappings返回一个Mappings对象此对象是一个简单封装了所有映射信息集合的类.
3. 建立对象映射信息
Binder类的BindRoot用于绑定映射信息中的所有映射内容.
//** Binder.cs **
public static void BindRoot(xm lDocument doc Mappings model)
{
// ...
foreach(xm lNode n in hmNode.SelectNodes(nsPrefix + ":class" nsmgr) )
{
RootClass rootclass = new RootClass();
Binder.BindRootClass(n rootclass model);
model.AddClass(rootclass);
}
// ...
}
遍历所有的类映射节点然后调用BindRootClass来绑定类映射信息最后将类映射信息加到集合中.
其中RootClass为PermissionClass的子类.
public static void BindRootClass(xm lNode node RootClass model Mappings mappings)
{
BindClass(node model mappings);
//TABLENAME
xm lAttribute tableNameNode = node.Attributes["table"];
string tableName = (tableNameNode==null)
StringHelper.Unqualify( model.PersistentClazz.Name )
: tableNameNode.Value;
xm lAttribute schemaNode = node.Attributes["schema"];
string schema = schemaNode==null mappings.SchemaName : schemaNode.Value;
Table table = mappings.AddTable(schema tableName);
model.Table = table;
// ...
PropertiesFromxm l(node model mappings);
}
BindRootClass首先调用BindClass绑定持久类映射信息然后调用PropertiesFromxm l来绑定类属性.
public static void BindClass(xm lNode node PersistentClass model Mappings mapping)
{
string className = node.Attributes["name"] == null null : node.Attributes["name"].Value;
// class
try
{
model.PersistentClazz = ReflectHelper.ClassForName(className);
}
catch ( Exception cnfe )
{
throw new MappingException( "persistent class not found" cnfe);
}
// ...
}
BindClass通过反射来取得持久对象的类型.
protected static void PropertiesFromxm l(xm lNode node PersistentClass model Mappings mappings)
{
string path = model.Name;
Table table = model.Table;
foreach(xm lNode subnode in node.ChildNodes)
{
CollectionType collectType = CollectionType.CollectionTypeFromString(name);
Value value = null;
if (collectType!=null)
{
value = new Value(table);
BindValue(subnode value true);
}
else if ( "many-to-one".Equals(name) )
{
value = new ManyToOne(table);
BindManyToOne(subnode (ManyToOne) value propertyName true);
}
else if ( "any".Equals(name) )
{
value = new Any(table);
BindAny(subnode (Any) value true);
}
else if ( "one-to-one".Equals(name) )
{
value = new OneToOne(table model.Identifier );
BindOneToOne(subnode (OneToOne) value true);
}
else if ( "property".Equals(name) )
{
value = new Value(table);
BindValue(subnode value true propertyName);
}
else if ( "component".Equals(name) )
{
value = new Component(model);
BindComponent(subnode (Component) value reflectedClass subpath true mappings);
}
else if ( "subclass".Equals(name) )
{
Subclass subclass = new Subclass(model);
BindSubclass( subnode subclass mappings );
}
else if ( "joined-subclass".Equals(name) )
{
Subclass subclass = new Subclass(model);
BindJoinedSubclass( subnode subclass mappings);
}
if ( value!=null)
{
Property prop = new Property(value);
BindProperty(subnode prop mappings);
}
}
}
遍历所有子节点然后根据节点类型对进行绑定.(注: 部分内容已删除)
关于属性的映射以后有空再详细研究只需要知道属性已加入到RootClass的Properties属性就行了.
NHibernate源码分析之二: 会话工厂
会话工厂是NHibernate中的关键类它与数据库连接数据库事务等进行交互还存储着与所有持久对象类型关联的持久化对象持久化类是持久化的关键它实现基本的CRUD操作.
当用户需要持久操作时由会话工厂创建一个会话供用户进行持久操作.
1. 会话工厂的创建
会话工厂由ISessionFactory接口实现由Configuration的BuildSessionFactory方法创建会话工厂应该使用Singleton模式.
如果要访问多个数据库应建立多个会话工厂.
//*** Configuration.cs ***
public ISessionFactory BuildSessionFactory()
{
// ...
Hashtable copy = new Hashtable();
foreach(DictionaryEntry de in properties)
{
copy.Add(de.Key de.Value);
}
return new SessionFactoryImpl(this copy interceptor);
}
其中SessionFactoryImpl为实现ISessionFactory的类这个类的修饰为Internal.
2. 持久化类的创建
持久化类用于对持久对象进行持久化操作每一个持久对象类型都有一个与之关联的持久化对象.
持久化类继承自IClassPersister接口这个接口定义了用于持久对象的CRUD操作.
//*** SessionFactoryImpl ***
private IDictionary classPersisters;
持久化对象集合Key为持久对象的类型;
private IDictionary classPersistersByName;
持久化对象集合Key为持久对象的类名;
public SessionFactoryImpl(Configuration cfg IDictionary properties IInterceptor interceptor)
{
// ...
foreach(PersistentClass model in cfg.ClassMappings)
{
System.Type persisterClass = model.Persister;
IClassPersister cp;
//TODO: H2.0.3 created a PersisterFactory
if (persisterClass==null || persisterClass==typeof(EntityPersister))
{
cp = new EntityPersister(model this);
}
else if (persisterClass==typeof(NormalizedEntityPersister))
{
cp = new NormalizedEntityPersister(model this);
}
else
{
cp = InstantiatePersister(persisterClass model);
}
classPersisters[model.PersistentClazz] = cp;
classPersistersByName[model.Name] = cp ;
}
// ...
}
在构造函数中遍历所有持久类映射信息然后根据持久类的持久类型建立一个持久化对象并将此对象加入到集合中.
InstantiatePersister用于创建一个自定义的持久化对象类名称由映射文件中Class节点的Persister属性指定自定义持久化类必须实现IClassPersister接口.
3. 连接提供者
连接提供者由IConnectionProvider接口实现会话工厂通过连接提供者与持久机制(数据库等)进行交互例如取得数据库连接等.
//*** SessionFactoryImpl.cs ***
public SessionFactoryImpl(Configuration cfg IDictionary properties IInterceptor interceptor)
{
// ...
connectionProvider = ConnectionProviderFactory.NewConnectionProvider(properties);
// ...
}
还是在构造函数中连接提供者由连接提供者工厂根据配置属性来创建.
//*** ConnectionProviderFactory ***
public static IConnectionProvider NewConnectionProvider(IDictionary settings)
{
IConnectionProvider connections = null;
string providerClass = settings[Cfg.Environment.ConnectionProvider] as string;
if (providerClass != null)
{
try
{
connections = (IConnectionProvider) Activator.CreateInstance(System.Type.GetType(providerClass));
}
catch (Exception e)
{
throw new HibernateException("Could not instantiate connection provider: " + providerClass);
}
}
else
{
throw new NotImplementedException("We have not implemented user supplied connections yet.");
}
connections.Configure(settings);
return connections;
}
NewConnectionProvider方法通过配置中ConnectionProvider的值来创建连接提供者.当前版本(v0.0.5)唯一可用的提供者只有DriverConnectionProvider类.
// 部分内容等有空再补充.
NHibernate源码分析之三: 会话与持久化操作
会话是nhibernate中的主要接口也是我们进行持久化操作和数据加载的主要接口ISession在IClassPersisterITransactionICriteria和IQuery之间起着协调者的作用.
会话对象通过调用会话工厂的OpenSession方法获得OpenSession方法有一个参数interceptor这是一个拦截器由实现了IInterceptor接口的对象来完成比较典型的是对会话的操作进行日志记录.
1. 持久对象的状态
持久对象的状态由EntityEntry类来维护.
sealed internal class EntityEntry
{
private LockMode _lockMode;
private Status _status;
private ob ject _id;
private ob ject[] _loadedState;
private ob ject[] _deletedState;
private bool _existsInDataba se;
private ob ject _version;
// for convenience to save some lookups
[NonSerialized] private IClassPersister _persister;
private string _className;
// ...
}
private IDictionary entitiesByKey; //key=Key value=ob ject
entitiesByKey集合保存当前会话中的所有持久对象.
[NonSerialized] private IdentityMap entries;//key=ob ject value=Entry
entries集合维护当前会话中所有持久对象的状态entries中的项目和entitiesByKey中的项目是一一对应的.
2. 持久化操作
当执行持久化操作时(Save/Update/Delete)除了少数情况外持久化操作并没有立即执行(更新数据源)而是被记录下来直到会话Flush时才会实际更新到数据源这样做的原因很容易理解就是为了避免频烦的数据库连接操作.如果没有调用Flush而关闭了会话当前会话中的持久对象将不会持久化!
SessionImpl.cs中有三个集合用来记录要持久化的计划对象:
[NonSerialized] private ArrayList insertions;
记录所有的ScheduledInsertion对象ScheduledInsertion对象是通过要Save的持久对象创建的如果对象的标识必须从数据库获得(如Identity标识)那么并不会创建ScheduledInsertion对象而是立即执行Save操作原因很简单因为必须取得Identity标识;
[NonSerialized] private ArrayList updates;
记录所有的ScheduledUpdate对象ScheduledUpdate对象是通过要Update的持久对象创建的;
[NonSerialized] private ArrayList deletions;
记录所有的ScheduledDeletion对象ScheduledDeletion对象是通过要Delete的持久对象创建的;
以上三个计划对象都从ScheduledEntityAction对象继承而此对象实现了IExecutable接口IExecutable接口的Execute方法用于执行执久化操作此操作由Flush间接调用.
下面来看看Flush的代码:
public void Flush()
{
if (cascading>0) throw new HibernateException( "..." );
FlushEverything();
Execute();
PostFlush();
}
Execute执行所有的计划对象.
private void Execute()
{
log.Debug("executing flush");
try
{
ExecuteAll( insertions );
insertions.Clear();
ExecuteAll( updates );
updates.Clear();
//...
ExecuteAll( deletions );
deletions.Clear();
}
catch (Exception e)
{
throw new ADOException("..." e);
}
}
分别执行insert/update/delete计划.
private void ExecuteAll(ICollection coll)
{
foreach(IExecutable e in coll)
{
executions.Add(e);
e.Execute();
}
if ( batcher!=null ) batcher.ExecuteBatch();
}
3. Save
ISession有两种保存持久对象的方法区别在于有没有指定对象Id(标识符).
public ob ject Save(ob ject obj)
{
if (obj==null) throw new NullReferenceException("attempted to save null");
if ( !NHibernate.IsInitialized(obj) )
throw new Persistentob jectException("uninitialized proxy passed to save()");
ob ject theObj = UnproxyAndReassociate(obj);
EntityEntry e = GetEntry(theObj);
if ( e!=null )
{
if ( e.Status==Status.Deleted)
{
Flush();
}
else
{
log.Debug( "ob ject already associated with session" );
return e.Id;
}
}
ob ject id;
try
{
id = GetPersister(theObj).IdentifierGenerator.Generate(this theObj);
if( id == (ob ject) IdentifierGeneratorFactory.ShortCircuitIndicator)
return GetIdentifier(theObj); //TODO: yick!
}
catch (Exception ex)
{
throw new ADOException("Could not save ob ject" ex);
}
return DoSave(theObj id);
}
先取得持久对象的状态如为删除则flush;然后取得持久对象的id(标识符)最后调用DoSave方法.
有关持久对象的标识符请参考我的下一篇文章 《持久对象标识符》.
public void Save(ob ject obj ob ject id)
{
if (obj==null) throw new NullReferenceException("attemted to insert null");
if (id==null) throw new NullReferenceException("null identifier passed to insert()");
if ( !NHibernate.IsInitialized(obj) ) throw new Persistentob jectException("uninitialized proxy passed to save()");
ob ject theObj = UnproxyAndReassociate(obj);
EntityEntry e = GetEntry(theObj);
if ( e!=null )
{
if ( e.Status==Status.Deleted )
{
Flush();
}
else
{
if ( !id.Equals(e.Id) )
throw new Persistentob jectException("...");
}
}
DoSave(theObj id);
}
与前一个Save方法不同的是不用取得持久对象的id显然这个方法适用于对象标识符已知的情况这样会提高一些性能.
private ob ject DoSave(ob ject obj ob ject id)
{
IClassPersister persister = GetPersister(obj);
Key key = null;
bool identityCol;
if (id==null)
{
if ( persister.IsIdentifierAssignedByInsert )
{
identityCol = true;
}
else
{
throw new AssertionFailure("null id");
}
}
else
{
identityCol = false;
}
if (!identityCol)
{
// if the id is generated by the db we assign the key later
key = new Key(id persister);
ob ject old = GetEntity(key);
if (old!=null)
{
if ( GetEntry(old).Status==Status.Deleted)
{
Flush();
}
else
{
throw new HibernateException( "...") );
}
}
persister.SetIdentifier(obj id);
}
// ...
// Put a placeholder in entries ...
AddEntry(obj Status.Saving null id null LockMode.Write identityCol persister);
// cascade-save to many-to-one BEFORE the parent is saved
// ...
// set property values ...
if (identityCol)
{
try
{
id = persister.Insert(values obj this);
}
catch (Exception e)
{
throw new ADOException("Could not insert" e);
}
key = new Key(id persister);
if ( GetEntity(key) != null )
throw new HibernateException("...");
persister.SetIdentifier(obj id);
}
AddEntity(key obj);
AddEntry(obj Status.Loaded values id Versioning.GetVersion(values persister) LockMode.Write identityCol persister);
if (!identityCol) insertions.Add( new ScheduledInsertion( id values obj persister this ) );
// cascade-save to collections AFTER the collection owner was saved
// ...
return id;
}
DoSave方法首先判断id是否为赋值的(assign)然后将持久对象加入到当前会话的集合中.
如果id为identity类型的则直接调用持久对象的持久化类来插入数据否则将持久对象加入到insertions集合中直到调用Flush时才插入数据.
4. Update
Update的处理基本上同Create是类似的但值得注意的是Update方法并没有将持久对象加入到updates集合中而是在执行Flush的时候通过判断持久对象的属性来决定持久对象是否需要Update.
5. Delete
Delete的处理比较复杂一些包括处理集合和级联这里就不贴出代码了.关于集合和级联处理我会在后续文章中进行分析.
NHibernate源码分析之三续: 数据持久化
当持久化对象时显然必须存在把记录的值赋值到对象属性和取得对象属性的值用于持久化操作对于更新操作还需要检查对象的值是否已发生变化即是否为Dirty这些操作都是由对象的持久化类来完成的.有关持久化类可参考《会话和持久化操作》一文.
下面对NH的源码进行分析以了解NH中数据加载和更新的过程.
一持久对象加载
先来想像一下对象的加载过程(Load).
1. 根据对象Id从数据库取得记录;
2. 使用默认的构造函数构造一个对象;
3. 把记录的值存储在一个地方用于在保存时进行比较;
4. 把记录的值赋值给对象的属性.
在本文的前篇中已经分析了对象加载过程的前半部分这里仅对后半部分进行分析.
//*** EntityLoader.cs 34行 ***
public ob ject Load(ISessionImplementor session ob ject id ob ject obj)