CSLA .NET数据访问与对象持久化详解
在开发应用程序时,数据访问和对象持久化是至关重要的环节。合理的设计可以提高应用程序的性能、可维护性和可扩展性。本文将深入探讨CSLA .NET中数据访问层的设计和实现,以及如何使用数据门户来实现对象的持久化。
1. 数据访问层设计原则
在设计数据访问层时,需要遵循一些重要原则,以确保代码的清晰性和可维护性。
1.1 逻辑分层
应用程序应采用逻辑分层的方式组织代码,避免将UI逻辑和业务逻辑、业务逻辑和数据访问逻辑混合在一起。业务逻辑应独立于数据存储和数据访问的概念,这样即使数据存储技术或数据访问技术发生变化,业务逻辑也不需要改变。
1.2 层间分离
通过将应用程序进行逻辑分层,使业务层和数据访问层分离。这样,任何一层的变化(在合理范围内)都不会影响到另一层。
2. CSLA .NET数据访问模型
CSLA .NET数据门户支持两种业务对象持久化模型,每种模型都有不同的实现方式。
2.1 使用DataPortal_XYZ方法
数据门户调用业务对象的DataPortal_XYZ方法,这些方法再调用数据访问层。这种方式使业务对象负责自身的持久化,可以直接封装数据访问代码,也可以调用外部数据访问对象。该模型简单强大,适用于多种数据访问技术,如ADO.NET、LINQ to SQL、ADO.NET Entity Framework等。
2.1.1 数据访问代码嵌入对象
将数据访问代码直接嵌入业务对象的DataPortal_XYZ方法中。这种方式性能较高,但会模糊业务层和数据访问层的界限。
Private Sub DataPortal_Fetch( _
ByVal criteria As SingleCriteria(Of CustomerEdit, Integer))
Using ctx = ConnectionManager(Of SqlConnection).GetManager("MyDb")
Using cm = ctx.Connection.CreateCommand()
cm.CommandType = CommandTypes.Text
cm.CommandText = "SELECT Id, Name FROM Customer WHERE id=@id"
Using dr = New SafeDataReader(cm.ExecuteReader())
dr.Read()
LoadProperty(IdProperty, dr.GetInt32("Id"))
LoadProperty(NameProperty, dr.GetString("Name"))
End Using
End Using
End Using
End Sub
2.1.2 数据访问代码在单独程序集
将数据访问代码放在单独的程序集中,由DataPortal_XYZ方法中的代码调用。这种方式提供了更好的层间分离,但会增加一定的复杂性。
Private Sub DataPortal_Fetch( _
ByVal criteria As SingleCriteria(Of CustomerEdit, Integer))
Using dal = New CustomerDal()
Using dr As SafeDataReader = dal.GetCustomer(criteria.Value)
dr.Read()
LoadProperty(IdProperty, dr.GetInt32("Id"))
LoadProperty(NameProperty, dr.GetString("Name"))
End Using
End Using
End Sub
2.2 使用对象工厂
业务对象可以使用ObjectFactory属性进行修饰,数据门户会创建工厂对象的实例,并调用工厂对象的方法,而不是业务对象的方法。这种方式将对象的创建、数据的获取和设置以及对象状态的管理责任交给了工厂对象。
2.2.1 数据访问在工厂中
将数据访问代码直接放在工厂对象中。这种方式是使用对象工厂模型的最简单方法,但会限制灵活性。
Public Class CustomerFactory
Inherits ObjectFactory
Public Function Fetch( _
ByVal criteria As SingleCriteria(Of Customer, Integer)) As Object
Using ctx = ConnectionManager(Of SqlConnection).GetManager("MyDb")
Using cm = ctx.Connection.CreateCommand()
cm.CommandType = CommandTypes.Text
cm.CommandText = "SELECT Id, Name FROM Customer WHERE id=@id"
cm.Parameters.AddWithValue("@id", criteria.Value)
Using dr = New SafeDataReader(cm.ExecuteReader())
dr.Read()
Dim result = DirectCast(Activator.CreateInstance( _
GetType(BusinessLibrary.CustomerEdit), True), _
BusinessLibrary.CustomerEdit)
MarkNew(result)
result.LoadData(dr)
Return result
End Using
End Using
End Using
End Function
End Class
2.2.2 对象工厂调用单独的数据访问
设计工厂对象与单独的数据访问层进行交互。这种方式更灵活、强大,工厂对象可以在业务程序集、自己的程序集或数据访问程序集中。
Public Class CustomerFactory
Inherits ObjectFactory
Public Function Fetch( _
ByVal criteria As SingleCriteria(Of CustomerEdit, Integer)) As Object
Using ctx = ObjectContextManager(Of MyDbEntities).GetManager("MyDb")
Dim data = From r In ctx.ObjectContext.Customer _
Where r.Id = criteria.Value _
Select r
Dim result = DirectCast(Activator.CreateInstance( _
GetType(BusinessLibrary.CustomerEdit), True), _
BusinessLibrary.CustomerEdit)
MarkNew(result)
result.LoadData(data.[Single]())
Return result
End Using
End Function
End Class
3. 设计权衡
在设计数据访问层时,需要平衡多个重要概念。
3.1 性能
数据访问的决策会对应用程序的整体性能产生重大影响。不同的数据访问技术和模型在性能上有所差异,需要根据实际需求进行选择。
3.2 封装
业务对象应遵循封装原则,其他对象不应直接操作业务对象封装的数据。但数据访问层需要访问业务对象的数据,这就需要在不破坏封装的前提下进行数据的传递。
3.3 分层
保持业务层和数据访问层的清晰分离,有助于提高应用程序的可维护性和降低成本。
3.4 复杂性
避免设计过于复杂的解决方案,因为这会降低可维护性并增加软件成本。
3.5 封装的保留与破坏
为了保留封装,数据访问层应在不直接操作业务对象的情况下,向业务对象提供或接收数据。例如,通过返回DTO(数据传输对象)的方式,让业务对象自己加载数据。
Private Sub DataPortal_Fetch( _
ByVal criteria As SingleCriteria(Of CustomerEdit, Integer))
Using dal = New CustomerDal()
Dim data As CustomerData = dal.GetCustomer(criteria.Value)
LoadProperty(IdProperty, data.Id)
LoadProperty(NameProperty, data.Name)
End Using
End Sub
如果选择破坏封装,允许数据访问层直接操作业务对象的数据,虽然一些ORM技术专门设计用于操作对象的内部状态,但这会增加耦合性,降低可维护性。可以使用反射、动态方法调用或创建接口等技术来实现,但需要注意避免其他代码也破坏封装。
4. 示例应用:ProjectTracker
以ProjectTracker应用为例,使用LINQ to SQL创建数据访问层。
4.1 使用LINQ to SQL
LINQ to SQL和ADO.NET Entity Framework是.NET中较新的数据访问技术。选择LINQ to SQL是因为它发布较早,且支持存储过程,Visual Studio LINQ to SQL设计器可以自动将存储过程包装成强类型的.NET方法。
4.2 ProjectTracker.DalLinq项目
数据访问代码位于ProjectTracker.DalLinq项目中,使用Visual Studio LINQ to SQL设计器创建数据访问层,只手动编写了一个简单的类。
Public Class Database
Public Const PTracker As String = "PTracker"
Public Const Security As String = "Security"
End Class
4.3 业务类实现
以Project类为例,介绍业务对象的实现。
4.3.1 工厂方法
通过工厂方法创建、检索和删除项目对象。
Public Shared Function NewProject() As Project
Return DataPortal.Create(Of Project)()
End Function
Public Shared Function GetProject(ByVal id As Guid) As Project
Return DataPortal.Fetch(Of Project)(New SingleCriteria(Of Project, Guid)(id))
End Function
Public Shared Sub DeleteProject(ByVal id As Guid)
DataPortal.Delete(New SingleCriteria(Of Project, Guid)(id))
End Sub
4.3.2 非公共构造函数
确保客户端代码必须使用工厂方法来创建或检索项目对象。
Private Sub New()
' require use of factory methods
End Sub
4.3.3 数据访问
实现DataPortal_XYZ方法来支持项目对象的数据创建、检索、插入、更新和删除。
Private Sub DataPortal_Fetch(ByVal criteria As SingleCriteria(Of Project, Guid))
Using ctx = ContextManager(Of ProjectTracker.DalLinq.PTrackerDataContext) _
.GetManager(ProjectTracker.DalLinq.Database.PTracker)
' get project data
Dim data = (From p In ctx.DataContext.Projects _
Where p.Id = criteria.Value _
Select p).Single()
LoadProperty(IdProperty, data.Id)
LoadProperty(NameProperty, data.Name)
LoadPropertyConvert(Of SmartDate, System.DateTime?)( _
StartedProperty, data.Started)
LoadPropertyConvert(Of SmartDate, System.DateTime?)( _
EndedProperty, data.Ended)
LoadProperty(DescriptionProperty, data.Description)
_timestamp = data.LastChanged.ToArray()
' get child data
LoadProperty(ResourcesProperty, _
ProjectResources.GetProjectResources(data.Assignments.ToArray()))
End Using
End Sub
4.4 其他类的实现
还介绍了ProjectResources、ProjectResource、RoleList、ProjectList、ResourceList、Roles等类的实现,每个类都有其特定的功能和数据访问方式。
4.5 实现Exists方法
许多对象可以通过实现Exists命令,让UI快速确定对象的数据是否存在于数据库中,而无需完全实例化对象。
Public Shared Function Exists(ByVal id As Guid) As Boolean
Return ExistsCommand.Exists(id)
End Function
4.6 总结
通过ProjectTracker应用的示例,展示了如何使用CSLA .NET数据门户实现数据访问和对象持久化。该应用的业务库支持多种接口项目,如WPF、Windows Forms、Web Forms等,体现了CSLA .NET的强大功能。
4.7 流程图
graph LR
A[数据访问请求] --> B{数据访问模型}
B -->|DataPortal_XYZ方法| C[业务对象调用数据访问层]
B -->|对象工厂| D[工厂对象处理数据访问]
C --> E[数据访问代码嵌入对象或单独程序集]
D --> F[数据访问在工厂或调用单独数据访问层]
E --> G[执行数据操作]
F --> G
G --> H[返回数据]
4.8 表格
| 数据门户模式 | 数据访问模型 | 描述 |
|---|---|---|
| DataPortal_XYZ | Embedded in object | 数据访问代码直接嵌入业务对象的DataPortal_XYZ方法中 |
| DataPortal_XYZ | In separate assembly | 数据访问代码在单独的程序集中,由DataPortal_XYZ方法中的代码调用 |
| ObjectFactory | Data access in factory | 数据门户调用工厂对象,工厂对象即为数据访问层 |
| ObjectFactory | Data access in separate assembly | 数据门户调用工厂对象,工厂对象再调用单独程序集中的数据访问层 |
通过以上介绍,你应该对CSLA .NET中的数据访问和对象持久化有了更深入的理解,能够根据实际需求选择合适的数据访问模型和技术。
5. 详细业务类分析
5.1 ProjectResources类
Project对象包含一个名为ProjectResources的子列表,该列表包含ProjectResource子对象。
5.1.1 工厂方法
Friend Shared Function NewProjectResources() As ProjectResources
Return DataPortal.CreateChild(Of ProjectResources)()
End Function
Friend Shared Function GetProjectResources( _
ByVal data As ProjectTracker.DalLinq.Assignment()) As ProjectResources
Return DataPortal.FetchChild(Of ProjectResources)(data)
End Function
Private Sub New()
' require use of factory methods
End Sub
这两个工厂方法分别用于创建和检索集合对象,通过调用数据门户的CreateChild()和FetchChild()方法,自动管理对象状态。
5.1.2 数据访问
Private Sub Child_Fetch(ByVal data As ProjectTracker.DalLinq.Assignment())
Me.RaiseListChangedEvents = False
For Each value In data
Me.Add(ProjectResource.GetResource(value))
Next
Me.RaiseListChangedEvents = True
End Sub
Child_Fetch()方法接收一个Assignment对象数组,循环遍历该数组,为每个项添加一个子对象到集合中。
5.2 ProjectResource类
ProjectResources集合包含ProjectResource对象,每个ProjectResource对象代表分配给项目的一个资源。
5.2.1 工厂方法
Friend Shared Function NewProjectResource( _
ByVal resourceId As Integer) As ProjectResource
Return DataPortal.CreateChild(Of ProjectResource)( _
resourceId, RoleList.DefaultRole())
End Function
Friend Shared Function GetResource( _
ByVal data As ProjectTracker.DalLinq.Assignment) As ProjectResource
Return DataPortal.FetchChild(Of ProjectResource)(data)
End Function
Private Sub New()
' require use of factory methods
End Sub
工厂方法用于创建和检索ProjectResource对象,通过数据门户的CreateChild()和FetchChild()方法自动设置子对象的IsChild、IsNew和IsDirty属性。
5.2.2 数据访问
- 创建新对象
Private Sub Child_Create(ByVal resourceId As Integer, ByVal role As Integer)
Dim res = Resource.GetResource(resourceId)
LoadProperty(ResourceIdProperty, res.Id)
LoadProperty(LastNameProperty, res.LastName)
LoadProperty(FirstNameProperty, res.FirstName)
LoadProperty(AssignedProperty, Assignment.GetDefaultAssignedDate())
LoadProperty(RoleProperty, role)
End Sub
Child_Create()方法初始化新对象,从数据库中检索资源信息并加载到对象属性中。
-
加载现有对象
Private Sub Child_Fetch(ByVal data As ProjectTracker.DalLinq.Assignment)
LoadProperty(ResourceIdProperty, data.ResourceId)
LoadProperty(LastNameProperty, data.Resource.LastName)
LoadProperty(FirstNameProperty, data.Resource.FirstName)
LoadProperty(AssignedProperty, data.Assigned)
LoadProperty(RoleProperty, data.Role)
_timestamp = data.LastChanged.ToArray()
End Sub
Child_Fetch()方法从数据访问对象中加载对象字段,并设置时间戳以实现乐观并发控制。
-
插入数据
Private Sub Child_Insert(ByVal project As Project)
_timestamp = Assignment.AddAssignment( _
project.Id, _
ReadProperty(ResourceIdProperty), _
ReadProperty(AssignedProperty), _
ReadProperty(RoleProperty))
End Sub
Child_Insert()方法将插入操作委托给Assignment类的AddAssignment()方法。
Public Shared Function AddAssignment( _
ByVal projectId As Guid, _
ByVal resourceId As Integer, _
ByVal assigned As SmartDate, _
ByVal role As Integer) As Byte()
Using ctx = ContextManager(Of ProjectTracker.DalLinq.PTrackerDataContext). _
GetManager(ProjectTracker.DalLinq.Database.PTracker)
Dim lastChanged As System.Data.Linq.Binary = Nothing
ctx.DataContext.addAssignment( _
projectId, resourceId, assigned, role, lastChanged)
Return lastChanged.ToArray()
End Using
End Function
AddAssignment()方法使用LINQ to SQL调用addAssignment存储过程,并返回时间戳。
-
更新和删除数据
更新和删除数据的方法与插入数据类似,都将操作委托给Assignment类的相应方法。
5.3 RoleList类
RoleList对象是一个基于Roles表的名称/值列表,用于项目、项目资源、项目资源分配和角色分配。
5.3.1 工厂方法
Private Shared _list As RoleList
Public Shared Function GetList() As RoleList
If _list Is Nothing Then
_list = DataPortal.Fetch(Of RoleList)()
End If
Return _list
End Function
Public Shared Sub InvalidateCache()
_list = Nothing
End Sub
GetList()方法使用共享字段实现缓存,只有在缓存为空时才从数据库中加载数据。InvalidateCache()方法用于清空缓存。
5.3.2 数据访问
Private Sub DataPortal_Fetch()
Me.RaiseListChangedEvents = False
Using ctx = ContextManager(Of ProjectTracker.DalLinq.PTrackerDataContext). _
GetManager(ProjectTracker.DalLinq.Database.PTracker)
Dim data = From role In ctx.DataContext.Roles _
Select New NameValuePair(role.Id, role.Name)
IsReadOnly = False
Me.AddRange(data)
IsReadOnly = True
End Using
Me.RaiseListChangedEvents = True
End Sub
DataPortal_Fetch()方法从数据库中检索角色数据,并将其添加到RoleList集合中。
5.4 ProjectList和ResourceList类
ProjectList和ResourceList类是只读数据的只读集合,用于为UI提供高效的项目和资源列表。
5.4.1 以ResourceList类为例
Public Shared Function EmptyList() As ResourceList
Return New ResourceList()
End Function
Public Shared Function GetResourceList() As ResourceList
Return DataPortal.Fetch(Of ResourceList)()
End Function
Private Sub New()
' require use of factory methods
End Sub
工厂方法用于创建空列表和从数据库中检索资源列表。
Private Sub DataPortal_Fetch()
RaiseListChangedEvents = False
Using ctx = ContextManager(Of ProjectTracker.DalLinq.PTrackerDataContext). _
GetManager(ProjectTracker.DalLinq.Database.PTracker)
Dim data = From r In ctx.DataContext.Resources _
Select New ResourceInfo(r.Id, r.LastName, r.FirstName)
IsReadOnly = False
Me.AddRange(data)
IsReadOnly = True
End Using
RaiseListChangedEvents = True
End Sub
DataPortal_Fetch()方法从数据库中检索资源信息,并将其添加到ResourceList集合中。
5.5 Roles类
Roles集合是一个可编辑的根集合,包含可编辑的子Role对象,用于维护角色列表。
5.5.1 工厂方法
Private Sub New()
AddHandler Me.Saved, AddressOf Roles_Saved
Me.AllowNew = True
End Sub
构造函数中设置AllowNew属性为True,允许数据绑定自动添加新的子对象到集合中,并为Saved事件添加处理程序。
5.5.2 数据访问
- 客户端缓存失效
Private Sub Roles_Saved(ByVal sender As Object, _
ByVal e As Csla.Core.SavedEventArgs)
RoleList.InvalidateCache()
End Sub
Roles_Saved()方法在Roles集合保存时,使RoleList的客户端缓存失效。
-
服务器端缓存失效
Protected Overrides Sub DataPortal_OnDataPortalInvokeComplete( _
ByVal e As DataPortalEventArgs)
If ApplicationContext.ExecutionLocation = _
ApplicationContext.ExecutionLocations.Server AndAlso _
e.Operation = DataPortalOperations.Update Then
RoleList.InvalidateCache()
End If
End Sub
DataPortal_OnDataPortalInvokeComplete()方法在服务器端更新操作完成后,使RoleList的服务器端缓存失效。
5.6 Exists方法实现
以Project类的Exists方法为例:
Public Shared Function Exists(ByVal id As Guid) As Boolean
Return ExistsCommand.Exists(id)
End Function
Exists()方法将工作委托给ExistsCommand类的Exists()工厂方法。
<Serializable()> _
Private Class ExistsCommand
Inherits CommandBase
Private _id As Guid
Private _exists As Boolean
Public ReadOnly Property ProjectExists() As Boolean
Get
Return _exists
End Get
End Property
Public Sub New(ByVal id As Guid)
_id = id
End Sub
Public Shared Function Exists(ByVal id As Guid) As Boolean
Dim result As ExistsCommand = DataPortal.Execute(Of ExistsCommand)( _
New ExistsCommand(id))
Return result.ProjectExists
End Function
Protected Overrides Sub DataPortal_Execute()
Using ctx = ContextManager(Of ProjectTracker.DalLinq.PTrackerDataContext). _
GetManager(ProjectTracker.DalLinq.Database.PTracker)
_exists = ((From p In ctx.DataContext.Projects _
Where p.Id = _id _
Select p).Count() > 0)
End Using
End Sub
End Class
ExistsCommand类继承自CommandBase,通过数据门户的Execute()方法执行命令,判断项目数据是否存在于数据库中。
6. 总结与展望
6.1 总结
本文详细介绍了CSLA .NET中数据访问和对象持久化的多种方式,包括使用DataPortal_XYZ方法和对象工厂模型,以及如何在设计数据访问层时平衡性能、封装、分层和复杂性等因素。通过ProjectTracker应用的示例,展示了如何使用LINQ to SQL创建数据访问层,并实现各种业务对象的持久化。
6.2 展望
在实际开发中,可以根据项目的具体需求和特点,选择合适的数据访问模型和技术。同时,可以进一步优化数据访问代码,提高应用程序的性能和可维护性。例如,可以使用更高效的缓存策略,减少数据库访问次数;或者使用异步编程模型,提高应用程序的响应性能。
6.3 表格
| 类名 | 功能 | 工厂方法 | 数据访问特点 |
|---|---|---|---|
| Project | 可编辑的根对象,代表项目 | NewProject()、GetProject()、DeleteProject() | 实现DataPortal_XYZ方法,支持事务处理 |
| ProjectResources | 项目资源子列表 | NewProjectResources()、GetProjectResources() | 使用Child_XYZ方法,管理子对象 |
| ProjectResource | 项目资源子对象 | NewProjectResource()、GetResource() | 委托操作给Assignment类 |
| RoleList | 名称/值列表 | GetList()、InvalidateCache() | 使用缓存提高性能 |
| ResourceList | 只读资源列表 | EmptyList()、GetResourceList() | 从数据库检索只读数据 |
| Roles | 可编辑的角色集合 | GetRoles() | 处理客户端和服务器端缓存失效 |
6.4 流程图
graph LR
A[客户端请求] --> B{请求类型}
B -->|创建项目| C[调用Project.NewProject()]
B -->|获取项目| D[调用Project.GetProject()]
B -->|删除项目| E[调用Project.DeleteProject()]
C --> F[数据门户调用DataPortal_Create()]
D --> G[数据门户调用DataPortal_Fetch()]
E --> H[数据门户调用DataPortal_Delete()]
F --> I[初始化项目对象]
G --> J[从数据库加载项目数据]
H --> K[从数据库删除项目数据]
I --> L[返回新的项目对象]
J --> L
K --> M[确认删除成功]
通过以上的分析和示例,希望能帮助你更好地理解和应用CSLA .NET进行数据访问和对象持久化开发。
超级会员免费看
63

被折叠的 条评论
为什么被折叠?



