XPO 最佳实践(XPO Best Practices)

来源:https://www.devexpress.com/Support/Center/Question/Details/A2944

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 the 
A751 article.

[C#]

public class OrderDetail : XPObject {
   public OrderDetail(Session session) : base(session) { 
   }
   // ...
}

[VB.NET]

Public Class OrderDetail
   Inherits XPObject
   Public Sub New(ByVal session As Session)
       MyBase.New(session)
   End Sub
   ' ...
End Class

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); }
}

[VB.NET]

Private fProductName As String
Public Property ProductName() As String
   Get
       Return fProductName
   End Get
   Set(ByVal value As String)
       SetPropertyValue("ProductName", fProductName, Value)
   End Set
End Property

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 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.
Q. Should I use the GetPropertyValue/SetPropertyValue methods for delayed properties? 
A. You must use the GetDelayedPropertyValue/SetDelayedPropertyValue or GetPropertyValue/SetPropertyValue methods to correctly handle delayed loading.
Q. The GetDelayedPropertyValue/SetDelayedPropertyValue methods does not provide overloaded method to specify the property holder. How to implement the extended variant of the delayed property, and include different delayed properties in one group? 
A. In this scenario, the following construction can be used:

[C#]

private XPDelayedProperty fPicture = new XPDelayedProperty();
[Delayed("fPicture", "GroupAttributes")]
public byte[] Picture {
    get { return (byte[])fPicture.Value; }
    set { fPicture.Value = value; }
}

[VB.NET]

Private fPicture As New XPDelayedProperty()
<Delayed("fPicture", "GroupAttributes")> _
Public Property Picture() As Byte()
    Get
        Return CType(fPicture.Value, Byte())
    End Get
    Set(ByVal value As Byte())
        fPicture.Value = value
    End Set
End Property

 

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);
...      
}

 

[VB.NET]

<STAThread()> _
Shared Sub Main()
   Dim conn As String = AccessConnectionProvider.GetConnectionString("ApplicationData.mdb")
   XpoDefault.DataLayer = XpoDefault.GetDataLayer(conn, AutoCreateOption.DatabaseAndSchema)
...
End Sub

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.

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. See an example here: How to view persistent objects in the XtraGrid and edit them in a separate window.

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 a SessionMixingExceptionand/or make you write cumbersome code for reloading data (see How XPO reloads objects and collections). 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;

[VB.NET]

XpoDefault.Session = Nothing

2. Remove default constructors from your persistent classes.

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 upon the Save method call. 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: Unit of Work.

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);
}

[VB.NET]

Dim conn As String = ...
Dim dl As IDataLayer = XpoDefault.GetDataLayer(conn, DevExpress.Xpo.DB.AutoCreateOption.DatabaseAndSchema)
Using session As Session = New Session(dl)
   Dim assemblies As System.Reflection.Assembly() = New System.Reflection.Assembly() _
       { GetType(AnyPersistentObjectFromAssemblyA).Assembly, GetType(AnyPersistentObjectFromAssemblyB).Assembly }
   session.UpdateSchema(assemblies)
   session.CreateObjectTypeRecords(assemblies)
End Using

8. Creating criteria

Refer to the Creating Criteria and all related topics to master this fundamental element in the DevExpress components and XAF/XPO frameworks world. Take special note that the same criteria can be represented by many ways. For example, if I want to filter only objects, which have a value equal to or greater than 20 in their “UnitPrice” field I can use the following criteria:

[C#]

CriteriaOperator criteria = new BinaryOperator("UnitPrice", 20, BinaryOperatorType.GreaterOrEqual);
CriteriaOperator criteria = new OperandProperty("UnitPrice") >= new OperandValue(20);
CriteriaOperator criteria = CriteriaOperator.Parse("UnitPrice >= ?", 20);
CriteriaOperator criteria = CriteriaOperator.Parse("UnitPrice >= 20");
CriteriaOperator criteria = CriteriaOperator.Parse(string.Format("UnitPrice >= {0}", 20));

The first three approaches are the best here because they lead to less errors. From them the first two are even strongly-typed which is yet better. To learn more about this, check out the Simplified Criteria Syntax help topic and this video. The third one utilizing positional parameters (they are specified via the ? character) is also safe enough. The last two approaches are also correct but not so safe. We don’t recommend using them widely. Normally, you can use them only if you know exactly what you are doing and you are an expert in the criteria language syntax. This is because, for example, you should be attentive about the correct syntax when specifying values of various types as literals and so lead to errors. See the Literals section in the previous help topic for more information.

Bonus: Persistent objects etiquette for enterprise applications
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 check the 
IsLoading value:

[C#]

private string name;
public string Name {
    get { return name; }
    set {
        bool changed = SetPropertyValue("Name", ref name, value);
        if (!IsLoading && changed) {
            ... YourBusinessRule...
        }
    }
}

[VB.NET]

Private _name As String
Public Property Name() As String
    Get
        Return _name
    End Get
    Set(ByVal value As String)
        Dim changed As Boolean = SetPropertyValue("Name", _name, value)
        If (Not IsLoading) AndAlso changed Then
            ...YourBusinessRule...
        End If
    End Set
End Property

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();
   }
}

[VB.NET]

<Persistent("Name")> _
Private Property PersistentName() As String
   Get
       Return name
   End Get
   Set(ByVal value As String)
       SetPropertyValue("PersistentName", name, value)
   End Set
End Property
<PersistentAlias("PersistentName")> _
Public Overridable Property Name() As String
   Get
       Return PersistentName
   End Get
   Set(ByVal value As String)
       DoMyBusinessTricksBeforePropertyAssignement()
       PersistentName = value
       DoMyBusinessTricksAfterPropertyAssignement()
   End Set
End Property

• 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 the Session’s Save method whenever possible. Use the UnitOfWork’s CommitChanges 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 than actually needed.
• Use Grids and Lookups in ServerMode when appropriate.
• Always use IDataStore for remote 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 and OnDeleting methods of your persistent objects are valuable in two ways:
1. Custom keys generating
2. Last-chance checking.
Defense checks must not be violated in the normal workflow – if such a check fires, it means something is bad with your program.
Please note that if you still decide to use these methods to execute your business logic, take into account that in a general case, they may be called multiple times (e.g., when an object is first saved in a nested Session and then committed to the database in the root Session or in a middle-tier scenario). This may lead to incorrect results, unless you handle this appropriately in your business logic or if multiple execution is irrelevant for it. For instance, you can check whether a value was already assigned to a property or not. Refer to the 
How to generate a sequential and user-friendly identifier field within an XPO business class article for some example code. Alternatively, you can move your logic directly into dependent property setters. Refer to the Task-Based Help > How to: Calculate a Property Value Based on Values from a Detail Collection article for some example code as well.
• Don’t mix the process of objects saving/persisting and business logic (including business rules validation) — this is a 
code smell of the Transaction Script pattern. The examples of defensive validation described in the previous point are exceptions, rather than good rules. You should not use this in your applications often. The best practice is to validate your objects before the saving process is started. Usually, it should be done either right after the changes are applied (for example, property setters), or later, with the help of a specific validator class that has knowledge of the contexts, in which a business object exists and is validated.
• 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.
• Don’t make your code dependent on the order of records returned by the XPCollection/XPView/XPCursor objects, unless you explicitly 
sorted them. By default, if the XPCollection/XPView/XPCursor is NOT sorted, records will be returned in an arbitrary order. This mimics the behavior of the SQL SELECT statement in the same circumstances.

See Also:
XPO Fundamentals
K18061
XPO Worst Practices