当设计一个使用 MVCC(多版本并发控制)的数据库时,你创建的表要么是一个布尔字段,如 "IsLatest",要么是一个整数 "VersionId",你永远不会做任何更新,你只当事情发生变化时插入新记录。
MVCC 为您提供对需要详细历史记录的应用程序的自动审核,它还减轻了数据库在更新锁方面的压力。缺点是它会使您的数据大小更大并减慢选择速度,因为获得最新版本需要额外的子句。它也使外键更加复杂。
(请注意,我不是在谈论 RDBMS 中的本机 MVCC 支持,例如 SQL Server 的快照隔离级别)
这已在 Stack Overflow 上的其他帖子中讨论过。 [待办事项 - 链接]
我想知道,哪些流行的实体/ORM 框架(Linq to Sql、ADO.NET EF、Hibernate 等)可以干净地支持这种类型的设计?这是对典型 ActiveRecord 设计模式的重大改变,所以我不确定现有的大多数工具是否可以帮助那些决定使用他们的数据模型走这条路的人。我对如何处理外键特别感兴趣,因为我什至不确定对它们进行数据建模以支持 MVCC 的最佳方法。
我设计了一个类似的数据库(只有 INSERT — 没有 UPDATE,没有 DELETE)。
我几乎所有的 SELECT 查询都是针对每个表的当前行的视图(最高修订号)。
视图看起来像这样……
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| SELECT
dbo.tblBook.BookId,
dbo.tblBook.RevisionId,
dbo.tblBook.Title,
dbo.tblBook.AuthorId,
dbo.tblBook.Price,
dbo.tblBook.Deleted
FROM
dbo.tblBook INNER JOIN
(
SELECT
BookId,
MAX(RevisionId) AS RevisionId
FROM
dbo.tblBook
GROUP BY
BookId
) AS CurrentBookRevision ON
dbo.tblBook.BookId = CurrentBookRevision.BookId AND
dbo.tblBook.RevisionId = CurrentBookRevision.RevisionId
WHERE
dbo.tblBook.Deleted = 0 |
我的插入(以及更新和删除)都由存储过程(每个表一个)处理。
存储过程看起来像这样……
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| ALTER procedure [dbo].[sp_Book_CreateUpdateDelete]
@BookId uniqueidentifier,
@RevisionId bigint,
@Title varchar(256),
@AuthorId uniqueidentifier,
@Price smallmoney,
@Deleted bit
as
insert into tblBook
(
BookId,
RevisionId,
Title,
AuthorId,
Price,
Deleted
)
values
(
@BookId,
@RevisionId,
@Title,
@AuthorId,
@Price,
@Deleted
) |
在 Visual Basic 代码中按事务处理修订号...
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38
| Shared Sub Save(ByVal UserId As Guid, ByVal Explanation As String, ByVal Commands As Collections.Generic.Queue(Of SqlCommand))
Dim Connection As SqlConnection = New SqlConnection(System.Configuration.ConfigurationManager.ConnectionStrings("Connection").ConnectionString)
Connection.Open()
Dim Transaction As SqlTransaction = Connection.BeginTransaction
Try
Dim RevisionId As Integer = Nothing
Dim RevisionCommand As SqlCommand = New SqlCommand("sp_Revision_Create", Connection)
RevisionCommand.CommandType = CommandType.StoredProcedure
RevisionCommand.Parameters.AddWithValue("@RevisionId", 0)
RevisionCommand.Parameters(0).SqlDbType = SqlDbType.BigInt
RevisionCommand.Parameters(0).Direction = ParameterDirection.Output
RevisionCommand.Parameters.AddWithValue("@UserId", UserId)
RevisionCommand.Parameters.AddWithValue("@Explanation", Explanation)
RevisionCommand.Transaction = Transaction
LogDatabaseActivity(RevisionCommand)
If RevisionCommand.ExecuteNonQuery() = 1 Then 'rows inserted
RevisionId = CInt(RevisionCommand.Parameters(0).Value) 'generated key
Else
Throw New Exception("Zero rows affected.")
End If
For Each Command As SqlCommand In Commands
Command.Connection = Connection
Command.Transaction = Transaction
Command.CommandType = CommandType.StoredProcedure
Command.Parameters.AddWithValue("@RevisionId", RevisionId)
LogDatabaseActivity(Command)
If Command.ExecuteNonQuery() 1 Then 'rows inserted
Throw New Exception("Zero rows affected.")
End If
Next
Transaction.Commit()
Catch ex As Exception
Transaction.Rollback()
Throw New Exception("Rolled back transaction", ex)
Finally
Connection.Close()
End Try
End Sub |
我为每个表创建了一个对象,每个表都有构造函数、实例属性和方法、create-update-delete 命令、一堆查找函数和 IComparable 排序函数。这是大量的代码。
一对一的 DB 表到 VB 对象...
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74
| Public Class Book
Implements iComparable
#Region" Constructors"
Private _BookId As Guid
Private _RevisionId As Integer
Private _Title As String
Private _AuthorId As Guid
Private _Price As Decimal
Private _Deleted As Boolean
...
Sub New(ByVal BookRow As DataRow)
Try
_BookId = New Guid(BookRow("BookId").ToString)
_RevisionId = CInt(BookRow("RevisionId"))
_Title = CStr(BookRow("Title"))
_AuthorId = New Guid(BookRow("AuthorId").ToString)
_Price = CDec(BookRow("Price"))
Catch ex As Exception
'TO DO: log exception
Throw New Exception("DataRow does not contain valid Book data.", ex)
End Try
End Sub
#End Region
...
#Region" Create, Update & Delete"
Function Save() As SqlCommand
If _BookId = Guid.Empty Then
_BookId = Guid.NewGuid()
End If
Dim Command As SqlCommand = New SqlCommand("sp_Book_CreateUpdateDelete")
Command.Parameters.AddWithValue("@BookId", _BookId)
Command.Parameters.AddWithValue("@Title", _Title)
Command.Parameters.AddWithValue("@AuthorId", _AuthorId)
Command.Parameters.AddWithValue("@Price", _Price)
Command.Parameters.AddWithValue("@Deleted", _Deleted)
Return Command
End Function
Shared Function Delete(ByVal BookId As Guid) As SqlCommand
Dim Doomed As Book = FindByBookId(BookId)
Doomed.Deleted = True
Return Doomed.Save()
End Function
...
#End Region
...
#Region" Finders"
Shared Function FindByBookId(ByVal BookId As Guid, Optional ByVal TryDeleted As Boolean = False) As Book
Dim Command As SqlCommand
If TryDeleted Then
Command = New SqlCommand("sp_Book_FindByBookIdTryDeleted")
Else
Command = New SqlCommand("sp_Book_FindByBookId")
End If
Command.Parameters.AddWithValue("@BookId", BookId)
If Database.Find(Command).Rows.Count 0 Then
Return New Book(Database.Find(Command).Rows(0))
Else
Return Nothing
End If
End Function |
这样的系统会保留每一行的所有过去版本,但管理起来确实很麻烦。
优点:
缺点:
-
依赖非数据库应用程序来保证数据完整性
-
需要编写大量代码
-
没有在数据库中管理外键(再见自动 Linq-to-SQL 样式的对象生成)
-
我仍然没有想出一个好的用户界面来检索所有保留的过去版本控制。
结论:
-
如果没有一些易于使用的开箱即用 ORM 解决方案,我不会在新项目上遇到这样的麻烦。
我很好奇 Microsoft Entity Framework 是否可以很好地处理此类数据库设计。
Jeff 和 Stack Overflow 团队的其他成员在开发 Stack Overflow 时必须处理类似的问题:已编辑问题和答案的过去修订被保存和检索。
我相信 Jeff 说过他的团队使用 Linq to SQL 和 MS SQL Server。
我想知道他们是如何处理这些问题的。
我可能会考虑纯粹在数据库中实现 MVCC 层,使用存储的过程和视图来处理我的数据操作。然后,您可以为任何能够映射到存储过程和从存储过程映射的 ORM 提供一个合理的 API,并且您可以让数据库处理数据完整性问题(因为它几乎是为此而构建的)。如果你走这条路,你可能想看看像 IBatis 或 IBatis.net 这样更纯粹的 Mapping 解决方案。
查看 Envers 项目 - 与 JPA/Hibernate 应用程序配合得很好,基本上可以为您做到这一点 - 跟踪另一个表中每个实体的不同版本,并为您提供类似 SVN 的可能性("给我 Person 的版本使用 2008-11-05...")
http://www.jboss.org/envers/
/詹斯
据我所知,ORM 框架会希望为您生成 CRUD 代码,因此必须明确设计它们以实现 MVCC 选项;我不知道有什么开箱即用的。
从实体框架的angular来看,CSLA 根本不会为您实现持久性——它只是定义了一个"数据适配器"接口,您可以使用它来实现您需要的任何持久性。因此,您可以设置代码生成(CodeSmith 等)模板来为您的 CSLA 实体自动生成与 MVCC 数据库架构一起使用的 CRUD 逻辑。
这种方法适用于任何实体框架,很可能不仅仅是 CSLA,但它会是 CSLA 中非常"干净"的实现。
我们所做的只是使用普通的 ORM (hibernate) 并使用视图而不是触发器来处理 MVCC。
所以,有一个 v_emp 视图,它看起来就像一个普通的表,你可以在其中插入和更新,但是当你这样做时,触发器实际上会处理将正确的数据插入到基表中。
不.. 我讨厌这种方法 :) 我会按照 Tim 的建议使用存储过程 API。
我一直认为您会在更新和删除时使用 db 触发器将这些行推送到 TableName_Audit 表中。
这将与 ORM 一起使用,为您提供您的历史记录,并且不会削弱该表上的选择性能。这是个好主意还是我错过了什么?