65.9K
CodeProject 正在变化。 阅读更多。
Home

自动化对象架构迁移

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.52/5 (10投票s)

2004年12月8日

8分钟阅读

viewsIcon

48930

使用 .NET 中的反射和序列化代理来自动化基本的对象模式迁移。

引言

本文演示了一种使用序列化代理和反射来自动化对象模式迁移的简单方法。换句话说,当 .NET 出现“可能的版本不匹配。类型 [某个类型] 有 x 个成员,反序列化的成员数为 y。”时该如何处理。

背景

在编写了多年的自定义 ORM 类解决方案,或者满足于使用 ADO 作为业务实体之后,我终于达到了极限,决定是时候进入对象持久化的神秘世界了。由此开始了我在 .NET 的 Serialization 命名空间 中的第一次探索。

我惊喜地发现,我可以轻松地将我的业务对象直接序列化到磁盘或任何持久化存储(其他地方已经有 大量文档)。一切都阳光明媚。我用几个小时的序列化实现工作,取代了可能需要几天时间来设置表以及在表和对象之间映射字段的工作,如下所示:

[ MyClass.vb ]
<Serializable()> Public Class MyClass
    . create some members
    .
    .
    Public Sub Save()
        Dim util As New CouldBeAnotherClass
        Dim bytes As Byte() = util.SerializeObject(Me)
        ' now save it to disk, database, cache, whatever
    End Sub
End Class

[ CouldBeAnotherClass.vb ]
Imports System.Runtime.Serialization
Imports System.Runtime.Serialization.Formatters.Binary

Public Class CouldBeAnotherClass
    Public Function SerializeObject(obj As Object) As Byte()
        Dim stream As New MemoryStream ' or file stream, any stream
        Dim bf As New BinaryFormatter
        Dim bytes As Byte()

        bf.Serialize(stream, obj)
        stream.Seek(0, SeekOrigin.Begin)
        bytes = stream.ToArray
        stream.Close()
        Return bytes
    End Function
End Class

问题

然后,风雨就来了,我的游行队伍也散了。我在我的一个对象中添加了一个字段。你知道接下来发生了什么:那个可怕的“可能的版本不匹配。类型 [某个类型] 有 x 个成员,反序列化的成员数为 y。”。糟糕。

更糟糕的是,Google 竟然找不到答案(难以置信)。我找到了很多原因,比如我可能应该使用像 db4oBamboo Prevalence 这样的现有产品,但那些产品虽然比 ORM 解决方案简单,但对我来说仍然是过度设计。而且,我想自己解决这个问题!

我的搜索引导我实现了根对象的 ISerializable 接口,以此来创建序列化字段与对象字段之间的自定义映射,包括自定义构造函数和 GetObjectData()。然而,我沮丧地发现,我花费时间创建的这些映射,恰恰是我试图避免的。如果我要编写所有那些繁琐的代码,为什么不直接设置传统的数据库表,然后从那里映射字段呢?我觉得我绕了一圈。我现在浪费时间做这样的事情:

Imports System.Security.Permissions
Imports System.Runtime.Serialization

<Serializable()> Public Class MyClass
    Implements ISerializable
    . create some members
    .
    .
    Public Sub Save()
        Dim util As New CouldBeAnotherClass
        Dim bytes As Byte() = util.SerializeObject(Me)
        ' now save it to disk, database, cache, whatever
    End Sub
    
    Public Sub New()
        ' an empty constructor for standard object initialization
    End Sub
    
    Private Sub New(ByVal info As SerializationInfo, _
                    ByVal context As StreamingContext)
        ' a private constructor used automatically
        ' by the deserialization process
        Me.SomeField = DirectCast(info.GetValue("SomeField", _
                                  Me.SomeField.GetType), [SomeType])
        Me.AnotherField = info.GetString("AnotherField")
        .
        .    and on and on with the possibility of conditional mappings
        .
    End Sub

    <SecurityPermissionAttribute(SecurityAction.Demand, _
     SerializationFormatter:=True)> _
    Public Sub GetObjectData(ByVal info As SerializationInfo, _
        ByVal context As StreamingContext _
        Implements ISerializable.GetObjectData
        
        info.AddValue("SomeField", Me.SomeField)
        info.AddValue("AnotherField", Me.AnotherField)
        .
        .    and on and on with the possibility of conditional mappings
        .
    End Sub
End Class

但是,我举着伞,继续前进,相信乌云总会散去。我在 Bamboo 的源代码中找到了它——一些被该项目测试计划遗漏的方法,是作者早期努力的一部分(现在已被另一种方法取代),但对我来说是个开始。为什么 Google 没找到呢?

解决方案

Bamboo 目前的对象模式迁移方法是读取一个 XML 文件,该文件定义了序列化内容与当前对象之间的对象和字段映射,然后利用这些信息创建适当的初始化程序,这些初始化程序在实现 ISerializationSurrogate.SetObjectData 时被调用。实现 ISerializationSurrogate 的对象只是代替其他对象进行繁琐的字段映射,这样您的业务对象本身就不必实现 ISerializable。这是一个巧妙的解决方案,而 Java 中与 Bamboo 对应的 显然缺少。但它比我需要的要多得多。

我从 Bamboo 源代码中的另一个地方汲取了方向,在一个似乎是在 XML 映射方法之前编写但现在很大程度上被废弃的类中。它看起来大致是这样的(关键方法是 SetObjectData()):

Imports System.Reflection
Imports System.Runtime.Serialization

Public Class MySurrogate
    Implements ISerializationSurrogate
    Implements ISurrogateSelector

    Private _assemblyToMigrate As System.Reflection.Assembly

    Public Sub New(ByVal assemblyToMigrate As System.Reflection.Assembly)
        _assemblyToMigrate = assemblyToMigrate
    End Sub
    
    Function SetObjectData(ByVal obj As Object, ByVal info As SerializationInfo, _
        ByVal context As StreamingContext, _
        ByVal selector As ISurrogateSelector) As Object _
        Implements ISerializationSurrogate.SetObjectData

        Dim entityType As Type = obj.GetType

        For Each entry As SerializationEntry In info
            Dim members As MemberInfo() = _
                entityType.GetMember(fieldName, MemberTypes.Field, _
                BindingFlags.NonPublic Or BindingFlags.Public _
                Or BindingFlags.Instance)

            If members.Length > 0 Then
                Dim newField As FieldInfo = CType(members(0), FieldInfo)
                Dim value As Object = entry.Value
                If Not value Is Nothing Then
                    If Not newField.FieldType.IsInstanceOfType(value) Then
                        value = Convert.ChangeType(value, newField.FieldType)
                    End If
                End If
                newField.SetValue(obj, value)
            End If
        Next
        Return Nothing
    End Function
    
    Sub GetObjectData(ByVal entity As Object, _
        ByVal info As SerializationInfo, _
        ByVal context As StreamingContext) Implements _
        ISerializationSurrogate.GetObjectData

        Throw New NotImplementedException
    End Sub

    Function GetSurrogate(ByVal type As System.Type, _
        ByVal context As StreamingContext, _
        ByRef selector As ISurrogateSelector) As ISerializationSurrogate _
        Implements ISurrogateSelector.GetSurrogate

        If type.Assembly Is _assemblyToMigrate Then
            selector = Me
            Return Me
        Else
            selector = Nothing
            Return Nothing
        End If
    End Function
    
    Function GetNextSelector() As ISurrogateSelector _
             Implements ISurrogateSelector.GetNextSelector
        Return Nothing
    End Function

    Sub ChainSelector(ByVal selector As _
        System.Runtime.Serialization.ISurrogateSelector) _
        Implements ISurrogateSelector.ChainSelector

        Throw New NotImplementedException("ChainSelector not supported")
    End Sub
End Class

您在这里看到的 ISurrogateSelector 实现是在构造 BinaryFormatter(我们稍后会这样做)时需要的,该 BinaryFormatter 用于序列化和反序列化您的业务对象,并且我们希望使用 ISerializationSurrogate,以便我们可以自定义字段映射以避免版本不匹配错误。

如果您的各种业务对象需要不同的序列化格式,可以使用 ISurrogateSelector 在多个 ISerializationSurrogate 实现之间进行选择。但在本例中,我们特别希望创建一个适用于我们所有对象的 ISerializationSurrogate,因此 ISurrogateSelector 的编写是基于一个简单的条件,来返回该对象或不返回。因此,后续代码块将省略 ISurrogateSelector 实现,尽管它是必需的。

不幸的是,虽然那个 MySurrogate 类(为了保护无辜而更改了名称)看起来很有希望,但当我尝试用它反序列化时,它失败了,即使在测试时对象模式实际上没有改变!在我们开始讨论这个问题之前,我将向您展示如何使用代理。上面的代码展示了如何序列化。反序列化同样容易。

Imports System.Runtime.Serialization
Imports System.Runtime.Serialization.Formatters.Binary

Public Class CouldBeAnotherClass

    Public Function DeserializeObject(ByVal type As System.Type) As Object
        Dim stream As FileStream = file.OpenRead
        Dim selector As New MySurrogate(type.Assembly)
        Dim bf As BinaryFormatter(selector, _
                  New StreamingContext(StreamingContextStates.All))
        Dim obj As Object = bf.Deserialize(stream)
        stream.Close
        Return obj
    End Function

    Public Function SerializeObject(obj As Object) As Byte()
        .
        .    as above
        .
    End Function
End Class

在本例中,我正在从文件进行反序列化(并且缺少一些语法),但您可以从内存、数据库字段或各种源进行反序列化。这里,我们尝试使用上面创建的代理。如果我们想在不使用代理的情况下进行反序列化,我们可以省略 selector 的声明,并创建不带参数的 BinaryFormatter。一旦您有了有效的代理,就可以轻松地将其与给定的 BinaryFormatter 一起使用或不使用。

克服新问题

我们的代理(遵循 Bamboo 旧代码模式的那个)的问题在于,它只适用于非常简单的对象。如果我们的业务对象使用了基类的字段,此代理将失败。失败的原因很简单,因为 Type.GetMember() 方法不返回基类的私有成员,尽管标准的 BinaryFormatter 已经成功序列化了这些相同的成员。因此,当代码遍历反序列化信息中的条目时,它在目标对象中找不到匹配项,并且该对象的字段将保持未初始化状态。

我们可以避免此问题的一种方法是使这些基类成员非私有,例如 Protected。事实上,这会奏效。这些成员随后对派生类型的 GetMember() 可见,并将从匹配的序列化条目中获取值。但是,如果您像我一样,通过继承 CollectionBase 创建了一些集合,那么您就没有更改其私有 list 访问权限的选项了。没有收到集合的任何成员就无法恢复序列化的集合,这很令人沮丧。毫无疑问,这适用于您可能继承的许多其他类。那么,该怎么办?

由于我对这个命名空间不太熟悉,我做的第一件事是感到非常沮丧。这些努力的整个目的是找到一种对象持久化模式,以避免繁琐的字段映射。我似乎无法实现。于是,我做了任何一个拥有 Intellisense 的好程序员都会做的事情,开始在 SetObjectData() 的成员上按“.”来查看我有哪些选项。我不详细介绍那些冒险经历,而是(终于!)直接给出解决方案。

Imports System.Reflection
Imports System.Runtime.Serialization

Public Class MySurrogate
    Implements ISerializationSurrogate
    Implements ISurrogateSelector
    
    Function SetObjectData(ByVal obj As Object, _
             ByVal info As SerializationInfo, _
             ByVal context As StreamingContext, _
             ByVal selector As ISurrogateSelector) As Object _
             Implements ISerializationSurrogate.SetObjectData

        Dim fieldName As String = String.Empty
        Dim entityType As Type

        For Each entry As SerializationEntry In info
            ' for each member that was serialized,
            ' get matching member in new type
            fieldName = entry.Name
            If fieldName.IndexOf("+") <> -1 Then
                ' serialized field comes from a base class
                Dim name As String() = fieldName.Split("+".ToCharArray)
                Dim baseType As String = name(0)

                fieldName = name(1)
                entityType = obj.GetType

                ' drill into base classes until type found
                Do While entityType.Name <> baseType
                    entityType = entityType.BaseType
                Loop
            Else
                entityType = obj.GetType
            End If

            Dim members As MemberInfo() = _
                entityType.GetMember(fieldName, MemberTypes.Field, _
                BindingFlags.NonPublic Or BindingFlags.Public _
                Or BindingFlags.Instance)

            If members.Length > 0 Then
                ' entity has a member matching the serialized info
                Dim newField As FieldInfo = CType(members(0), FieldInfo)
                Dim value As Object = entry.Value
                If Not value Is Nothing Then
                    ' don't bother adding serialized members with null values
                    If Not newField.FieldType.IsInstanceOfType(value) Then
                        ' convert type if changed in new member
                        value = Convert.ChangeType(value, newField.FieldType)
                    End If
                End If
                newField.SetValue(entity, value)
            End If
        Next
        Return Nothing
    End Function
    
    ' ISurrogateSelector implementations not shown
End Class

为什么它有效

您可以看到,这与上面的 ISerializationSurrogate 实现相同,但在 SetObjectData() 中新增了这块代码:

fieldName = entry.Name
If fieldName.IndexOf("+") <> -1 Then
    Dim name As String() = fieldName.Split("+".ToCharArray)
    Dim baseType As String = name(0)

    fieldName = name(1)
    entityType = obj.GetType

    Do While entityType.Name <> baseType
        entityType = entityType.BaseType
    Loop
Else
    entityType = obj.GetType
End If

在我多次在调试模式下逐步执行此代码时,我注意到来自基类的私有字段的 .Name 总是 [BaseClass]+[Field] 而不是简单的 [Field]。例如,在反序列化我的 CollectionBase 派生对象时,我会看到 CollectionBase+list 经过...但没有匹配项。然后 Intellisense 向我展示了 Type.BaseType

我得出的方法有点粗暴,但到目前为止对我来说效果很好。如果我遇到一个属于基类的字段,通过存在“+”来指示,我就会将基类型的名称拆出来,并使用该名称通过 Type.BaseType 深入到我的目标对象类型中,直到找到匹配项。然后,这个类型和拆分出来的字段名称将成为 For 循环其余部分字段匹配逻辑使用的类型和名称。

使用代码

尽管没有进行任何性能测试,但我仍然设想这个代理比使用标准的 BinaryFormatter 进行对象反序列化要慢。因此,我只希望在使用标准反序列化抛出“可能的版本不匹配”错误时使用此 ISerializationSurrogate。我按如下方式处理了它:

Imports System.Runtime.Serialization
Imports System.Runtime.Serialization.Formatters.Binary

Public Class MyPersistenceClass

    Public Function Load(ByVal filename As String, ByVal type As System.Type, _
        ByRef schemaChange As Boolean) As Object

        Dim obj As Object
        schemaChange = False

        Dim file As New FileInfo(filename)
        If file.Exists Then
            Dim stream As FileStream = file.OpenRead
            Dim bf As BinaryFormatter

            bf = Me.CreateFormatter()

            Try
                obj = bf.Deserialize(stream)
            Catch ex As SerializationException
                ' standad deserialization
                ' didn't work so attempt schema migration
                stream.Seek(0, SeekOrigin.Begin)
                bf = Me.CreateFormatter(type)
                obj = bf.Deserialize(stream)
                schemaChange = True
            Finally
                stream.Close()
            End Try
        End If

        Return obj
    End Function
    
    Private Function CreateFormatter(ByVal type _
                     As System.Type) As BinaryFormatter
        Dim selector As New MySurrogate(type.Assembly)
        Return New BinaryFormatter(selector, _
            New StreamingContext(StreamingContextStates.All))
    End Function

    Private Function CreateFormatter() As BinaryFormatter
        Dim formatter As New BinaryFormatter
        formatter.Context = _
            New StreamingContext(StreamingContextStates.Persistence)
        Return formatter
    End Function
End Class

您可以看到,这个例子也假设是基于文件的持久化。不过,任何数据存储都可以。当调用 Load() 时,它首先尝试使用标准的 BinaryFormatter(由一个私有方法创建)从指定的文件中反序列化指定的对象类型。对我来说,这将在 99.9% 的情况下,甚至更高,都有效。我开发此项目所使用的业务对象模式每年只会更改两三次。

但是,如果失败了,它会从重载的 CreateFormatter() 加载备用 BinaryFormatter,该方法使用我们的 ISurrogateSelector 来获取我们的 ISerializationSurrogate 实现。然后,将使用此格式化程序进行第二次反序列化尝试。

一个布尔型 schemaChange 变量通过引用传递给 Load(),以便调用方法可以决定在检测到模式更改时该做什么。在我的例子中,调用方法会立即对对象调用一个新的 .Save,以便序列化版本随后与新模式匹配。

注意事项

上面得出的 ISerializationSurrogate 实现将无法处理所有模式更改。Bamboo 方法在这方面更为健壮,它使用自定义初始化程序来处理一系列字段映射。但据我所知,它也需要为许多类型的模式更改编写自定义字段初始化程序。

此实现处理的更改是字段的删除和添加,以及简单的类型更改。对我而言,这意味着它将自动处理我预期的几乎所有模式更改。由于模式更改意味着移动新程序集,因此我可以同时轻松地在 ISerializationSurrogate.SetObjectData() 中添加额外的逻辑来处理任何奇怪的字段映射需求。这种方法可能不适合您的应用程序,如果您预期会频繁且复杂的对象模式更改,您应该仔细考虑其他选择。

结论

所有这些关于对象持久化的讨论,却很少提及并发和弹性等重要功能。我确实有一些正在工作的解决方案,可能是一篇后续文章,但我正在等待看看它是否会“着火”后再谈论。在这里,db4oBamboo Prevalence 提供了强大的解决方案,但我不想处理第三方层,想保持简单,并想自己解决。敬请关注。

© . All rights reserved.