学习XPO有一段时间了,也用它陆续做过几个项目。遇到过问题走过弯路,这里把DevExpress知识库的几篇文章的内容做了摘录和整理,并且加上了一些自己的注释,方便自己日后备忘也方便其他的朋友。
1. Always define a constructor with a Session parameter in your persistent objects.
This will help you prepare for point 4 (see below) and will also allow you to avoid the exception explained in theSessionCtorAbsentException - what does it mean? article.
[C#]
public class OrderDetail : XPObject {
public OrderDetail(Session session) : base(session) {
}
...
}
.
为每个持久化对象定义一个包含Session参数的构造函数。
实际上使用Dev的模板创建的XPO对象已经默认包含了这个构造函数。而使用中应该明确的使用该构造函数来初始化对象,即显式的传递进一个Session对象来初始化持久类。
2. Use the SetPropertyValue method in persistent property setters.
Here is the recommended code for a persistent property:
[C#]
string fProductName;
public string ProductName {
get { return fProductName; }
set { SetPropertyValue("ProductName", ref fProductName, value); }
}
Q. Why must I define a property rather than just a field (public string ProductName;)? Why should I use a SetPropertyValue rather than simply setting a value to a class field?
A. XPO expects an Object Changed notification, when a persistent property's value is changed. For instance, this notification is used to include the modified object into the UnitOfWork's Objects To Save collection. The SetPropertyValue method sends this notification by calling the object's OnChanged method, while a simple field assignment does not.
Q. Why should I use the SetPropertyValue overloaded method with a property name as the first parameter?
A. If a property name isn't specified, XPO still tries to determine a property name, which is being set, by analyzing the call stack. When the property name is explicitly specified, the SetPropertyValue method is executed much faster!
Q. Why not to use a GetPropertyValue in a property's getter?
A. Code like return fProductName; is faster than a call to the GetPropertyValue method. When it comes to reading persistent properties, the access time is critical, because it's a frequent operation.
Please refer to the XPO Simplified property syntax article to learn more.
在持久类的属性的Set方法里使用SetPropertyValue来替代一个普通的赋值。它的作用是通知UnitOfWork记录下发生了变动的对象,以便在CommitChanges时保存该对象。若不这么做,则UnitOfWork无法跟踪发生变动的对象,亦无法在最后调用CommitChanges时如预期般保存下这些对象了。Get方法则不需要,它只会无谓的增加访问属性时的时间开销。
3. Explicitly set the XpoDefault.DataLayer property in the entry point of your application.
[C#]
[STAThread]
static void Main()
{
string conn = AccessConnectionProvider.GetConnectionString(@"ApplicationData.mdb");
XpoDefault.DataLayer = XpoDefault.GetDataLayer(conn,AutoCreateOption.DatabaseAndSchema);
...
}
Without this code, each new Session or UnitOfWork will create a new data layer for its exclusive use. The data layer creates a new connection to your database. This is not advisable for at least for two reasons:
1. Establishing a connection to a database is a long lasting operation. It may cause performance issues.
2. Your application may exceed of the maximum DB connection limit.
When the XpoDefault.DataLayer is set, all Session and UnitOfWork objects, which were created with their default constructors (without a DataLayer parameter), share a given XpoDefault.DataLayer object.
This will also help you get prepare for Best Practices #4 (see below).
Please note that setting an XpoDefault.DataLayer doesn't prevent you from creating a new connection to your database when you need one (e.g. for emulating a multi-user application for testing purposes): You can create a new SimpleDataLayer instance, pass it to the Session's constructor as a parameter and use this Session as your needs dictate.
在程序的入口点显式的为XpoDefault.DataLayer赋值。
若不这么做,每个Session(UnitOfWork)对象都将自动创建一个新的专用的DataLayer,而这又将创建一个新的数据库连接。频繁的创建数据库连接将会对程序性能带来不利影响,并且可能会超出数据库允许的最大连接数。在为XpoDefault指定了DataLayer以后,则后续的Session(UnitOfWork)都将共享的使用这个DataLayer。(这样做以后我们依然可以在需要的时候创建新的DataLayer,并不会受到任何限制。)
Create a ThreadSafeDataLayer instance in the entry point of your application and assign it to the static XpoDefault.DataLayer property.
It's similar to Windows Forms development where it's recommended to initialize XpoDefault.DataLayer in the Main procedure. The differences include:
A) ThreadSafeDataLayer is to be used in ASP.NET, not in a SimpleDataLayer.
B) The database schema must be up-to-date and contain the XPObjectType table. An XPO connection provider must be created with the SchemaAlreadyExists parameter.
C) ThreadSafeDataLayer requires the XPO dictionary (stores persistent objects' metadata) to be initialized at the moment when ThreadSafeDataLayer is created.
D) The Web application's entry point is the Application_Start procedure in Global.asax.
To sum it up, here is a template for the Application_Start procedure in your application:
[C#]
protected void Application_Start(object sender, EventArgs e) {
string conn = DevExpress.Xpo.DB.MSSqlConnectionProvider.GetConnectionString("(local)","XpoWebTest");
DevExpress.Xpo.Metadata.XPDictionary dict = newDevExpress.Xpo.Metadata.ReflectionDictionary();
DevExpress.Xpo.DB.IDataStore store =DevExpress.Xpo.XpoDefault.GetConnectionProvider(conn,DevExpress.Xpo.DB.AutoCreateOption.SchemaAlreadyExists);
dict.GetDataStoreSchema(typeof(PersistentObjects.Customer).Assembly);
DevExpress.Xpo.XpoDefault.DataLayer = new DevExpress.Xpo.ThreadSafeDataLayer(dict,store);
DevExpress.Xpo.XpoDefault.Session = null;
}
对于ASP.NET项目,特别的,我们应该采用ThreadSafeDataLayer而非SimpleDataLayer(因为ASP.NET项目是多线程的)。
ThreadSafeDataLayer需要一个XPO字典类来保存所有的持久化对象的元数据。上述代码中的dict.GetDataStoreSchema(typeof(PersistentObjects.Customer).Assembly);就是这个作用。注意如果项目中的XPO对象不是集中在一个类库而是分散在多个类库的话,我们需要在这里从每一个类库中都任意抓取一个对象作为参数传递给GetDataStoreSchema方法。
举例来说,我们在CustomerObjects项目下有Customer, Address两个XPO类,又有一个OrderObjects项目下有Order, OrderItem两个XPO类,则GetDataStoreSchema方法应该写成:
GetDataStoreSchema(typeof(CustomerObjects.Customer).Assembly, typeof(OrderObjects.Order).Assembly);
4. Use a new Session / UnitOfWork instance to fully control loading, modifying and saving data.
XPO Session caches objects. By creating a new Session/UnitOfWork instance for data processing, you acquire better control over reloading data and saving changes. We advise that you utilize a separate UnitOfWork instance in all visual modules (Forms and UserControls) of your application.
Please review the attached sample project. It demonstrates how to create a grid form for navigation and a non-modal form for editing XPO data.
See also: Session Management and Caching
建议在每个Form,用户控件后对持久类进行操作时都使用一个新的Session/UnitOfWork类。因为Session会对经它操手的对象进行一定的缓存管理(有关XPO的缓存机制日后会有另文详述)。采用新的独立的Session对象则可以方便我们自如的控制读写。例如由我们指定的废除缓存强制重新从数据库中读取最新的数据。这样可避免对其他数据产生影响。
5. Avoid the use of a default session.
The XPO default Session is accessible via the XpoDefault.Session or Session.Default static property. The default session is used internally when objects or XPCollection instances are created without a Session parameter. This may result in aSessionMixingException and/or make you write cumbersome code for reloading data (see How the XPO cache works). To avoid these problems, please don't use the default session.
1. Set XpoDefault.Session to null (Nothing) in the entry point of your application:
[C#]
XpoDefault.Session = null;
2. Remove default constructors from your persistent classes.
避免使用XpoDefault.Session。
在创建XPO对象或者XPCollection等对象时若没有显式的为他们指定一个Session,则他们会自动调用默认的Session,即XpoDefault.Session,而这会导致一系列的问题。为避免发生这样的情况,建议在程序的入口点就将XpoDefault.Session设为null。并且删除所有XPO对象的默认构造函数(没有传递Session参数的),避免在写代码的时候无意中调用到。
但是在实践中,这样会产生一个问题,即如果我们在Form中使用了XpoDataSource和其他UI控件做绑定(这很常见并且很方便),XpoDataSource会因无法找到适用的Session而无法工作(在没有显式的为其指定Session对象的情况下,它默认使用XpoDefault.Session)。为解决这个问题,我们可以在Form的Page_Init方法中为它指定Session,如:
Session session;
protected void Page_Init(object sender, EventArgs e) {
session = new Session(XpoDefault.DataLayer);
XpoDataSource1.Session = session;
}
这里注意的是上述代码必须被放在Page_Init内,不能放在Page_Load内。 具体请参见ASP.NET页生命周期概述。
6. Use a UnitOfWork rather than Session.
When a Session is used, and its transaction isn't explicitly started, a persistent object is immediately persisted in the data store when its Save method is called. If the BeginTransaction / CommitTransaction methods are explicitly called, then the Session behaves exactly as if it was a UnitOfWork. The Session class is maintained for backward compatibility with XPO 1.x.
Unlike Session, UnitOfWork doesn't persist changes until its CommitChanges method is called. Thus, the UnitOfWork gives you more control over what and when to save.
See also: Transactions and Units of Work
尽可能的使用UnitOfWork代替Session。
在使用Session时,默认情况下它并不会显式的启动一个事务。每当调用Save方法时所有的改动都将被实时的保存进数据库。
UnitOfWork是继承自Session的,即它本身就可以被当做Session使用。然而它可以比Session更好的控制“何时”保存“什么”内容。因为凡是使用该UnitOfWork的对象,在CommitChanges被调用前,所有的改动都由UnitOfWork在内存中管理,不会实时入库。在对多个对象进行较为复杂的修改时,UnitOfWork将比Session节省出很多资源。
当然,若我们使用Session时显式的调用BeginTransaction / CommitTransaction方法,则和UnitOfWork的效果一样了。
7. Create a separate application for database maintenance and schema updates.
For security reasons, you may wish to deny access to system tables and disable modifications to the database schema for the database account used in your end-user application. Please use the AutoCreateOption.SchemaAlreadyExists option when creating a DataLayer in your XPO application. In this case, you can grant fewer privileges to the database user account used in your application.
To create a database schema, please write a separate application, which calls the UpdateSchema and CreateObjectTypeRecords methods:
[C#]
string conn = ...;
IDataLayer dl = XpoDefault.GetDataLayer(conn,DevExpress.Xpo.DB.AutoCreateOption.DatabaseAndSchema);
using(Session session = new Session(dl)) {
System.Reflection.Assembly[] assemblies = new System.Reflection.Assembly[] {
typeof(AnyPersistentObjectFromAssemblyA).Assembly,
typeof(AnyPersistentObjectFromAssemblyB).Assembly
};
session.UpdateSchema(assemblies);
session.CreateObjectTypeRecords(assemblies);
}
创建一个独立的项目进行数据库的维护和结构更新。
XPO可以在项目启动时对数据库进行检查并做出需要的改动。然而这需要较高的数据库帐户权限。在对安全性要求较为严格的项目中,我们可以创建一个独立的项目,专门用以更新数据库结构所用,为这个项目指定一个具有较高权限的账户。而主项目则可以分配给一个权限较低的账户了。
XPO Bonus: Persistent objects etiquette for enterprise applications (Part I)
Here, Etiquette means a simple set of rules, which are not enforced, but if followed, will simplify your life significantly:
• Never use Session.DefaultSession for actual persistent objects.
• Never use an XPBaseObject if not really required. Use XPCustomObject or XPObject as base classes.
• Never produce side effects on a persistent property assignment. At the very least, don't change or check directly or indirectly other persistent properties between OnLoading and OnLoad (while IsLoading == true).
• If you need to enforce your business rules in the properties setters or getters, always do it under:
[C#]
if(!IsLoading) { ... YourBusinessRule... }
or use the following technique:
[C#]
[Persistent("Name")]
private string PersistentName {
get { return name; }
set { SetPropertyValue("PersistentName", ref name, value); }
}
[PersistentAlias("PersistentName")]
public virtual string Name {
get { return PersistentName; }
set {
DoMyBusinessTricksBeforePropertyAssignement();
PersistentName = value;
DoMyBusinessTricksAfterPropertyAssignement();
}
}
• It's a bad idea to throw exceptions inside your persistent properties. At the very least, don't do it between OnLoading and OnLoaded (IsLoading == true).
• Don't use Save whenever possible. Use UnitOfWork whenever possible.
• Share a single DataLayer between all your sessions within same AppDomain whenever possible. Assigning XpoDefault.DataLayer in the Main() function of your program is good style.
• Never do any complex work on persistent object .ctor(Session) and persistent properties assignments (at least between OnLoading and OnLoaded (until IsLoading == true)) -- if you want Xpo to be effective, persistent objects must be created as quickly as possible from their Data Store images.
• If your algorithm requires ten persistent objects -- load them all at once at the beginning of the method
• Never load more objects then actually needed.
• Use Grids and Lookups in ServerMode when appropriate
• Always use IDataStore for remoting scenarios.
• Don't expose your business objects instances remotely, in scenarios where this is possible. Use eXpress Persistent Objects, when available, if you absolutely need to transfer objects over the wire, but think twice: do you really need it?
• Never use XmlWebServices until all your clients have access to assemblies provided by you. Expose IDataStore via WebService, and allow your clients to work with persistent objects.
• If you need to disallow your customers from changing something through remotable IDataStore -- create an IDataStore wrapper, which will throw an InvalidOperationException on each call to ModifyData.
• Use Domain Model whenever possible
• The OnSaving method approaches are valuable in two ways:
1. Custom keys generating
2. Last-chance checking, if you adapt defensive programming, like I do, (this rule does not violate the previous rule! Defense checks must not be violated in the normal workflow -- if such a check fires, it's means something is bad with your program)
• Don't mix the process of objects persisting and business logic (including business rules validation) -- this is a code smell of theTransaction Script pattern
• Don't work with the same persistent object instance from different threads. Create a separate session for each thread, and work with different instances of the same persistent object.
• Don't create a class structure which will result in loading half of the database on accessing a single object/property. For instance if you have a class Gender with two instances, Gender("Male") and Gender("Female") it's a bad idea to create a collection of all persons of a specific sex in the Gender class. If you need to do it for building criteria -- make the collection property private or protected, and return null from it (undocumented and unsupported but a working and useful feature).
• If your class structure is highly coupled and it's actually possible to raise a complete database accessing the single object/property, break this net using delayed loading.
• Don't use Delayed loading if not really needed.
原文这部分内容比较杂和细,有一些在上文里已经说过了,其他的挑我认为比较常见的说了。
尽量不要在持久化对象内部包含业务逻辑,而只应该包含数据存取本身需要的逻辑。这些代码应该尽量被放在属性的Get和Set方法里。并且尽量避免在对象被读取/保存的时候进行复杂的操作或运算。
如果程序里需要对多个对象进行操作,则最好一次性读入这些对象,完成后再一次性写入(善用UnitOfWork)。然而不应该读取任何不需要的内容。
设计对象时应该尽量避免在访问单个对象/属性时可能会导致加载大量数据的结构。例如有一个Gender类,只保存男女,在访问其属性时则可能导致数据库读取整库一半的Person对象。如果确实无法避免这类结构,可以使用XPO的延迟加载特性(为属性加上Delayed Loading标签)。这样仅当关联数据确实被访问时XPO才会再去数据库中进行查询。但该特性依然不应该被滥用,因为它会导致额外的数据库访问开销。
DevExpress的原文链接:
http://www.devexpress.com/Support/Center/KB/p/A2944.aspx
http://www.devexpress.com/Support/Center/kb/p/K18061.aspx