自动化对象架构迁移






4.52/5 (10投票s)
2004年12月8日
8分钟阅读

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 竟然找不到答案(难以置信)。我找到了很多原因,比如我可能应该使用像 db4o 或 Bamboo 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()
中添加额外的逻辑来处理任何奇怪的字段映射需求。这种方法可能不适合您的应用程序,如果您预期会频繁且复杂的对象模式更改,您应该仔细考虑其他选择。
结论
所有这些关于对象持久化的讨论,却很少提及并发和弹性等重要功能。我确实有一些正在工作的解决方案,可能是一篇后续文章,但我正在等待看看它是否会“着火”后再谈论。在这里,db4o 和 Bamboo Prevalence 提供了强大的解决方案,但我不想处理第三方层,想保持简单,并想自己解决。敬请关注。