19、CSLA .NET数据访问与对象持久化详解

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进行数据访问和对象持久化开发。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值