Which ORM framework can best handle an MVCC database design?

前端 未结 6 929
北海茫月
北海茫月 2021-02-06 02:30

When designing a database to use MVCC (Multi-Version Concurrency Control), you create tables with either a boolean field like \"IsLatest\" or an integer \"VersionId\", and you n

6条回答
  •  抹茶落季
    2021-02-06 03:31

    I designed a database similarly (only INSERTs — no UPDATEs, no DELETEs).

    Almost all of my SELECT queries were against views of only the current rows for each table (highest revision number).

    The views looked like this…

    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
    

    And my inserts (and updates and deletes) were all handled by stored procedures (one per table).

    The stored procedures looked like this…

    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
            )
    

    Revision numbers were handled per-transaction in the Visual Basic code…

    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
    

    I created an object for each table, each with constructors, instance properties and methods, create-update-delete commands, a bunch of finder functions, and IComparable sorting functions. It was a huge amount of code.

    One-to-one DB table to VB object...

    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
    

    Such a system preserves all past versions of each row, but can be a real pain to manage.

    PROS:

    • Total history preserved
    • Fewer stored procedures

    CONS:

    • relies on non-database application for data integrity
    • huge amount of code to be written
    • No foreign keys managed within database (goodbye automatic Linq-to-SQL-style object generation)
    • I still haven't come up with a good user interface to retrieve all that preserved past versioning.

    CONCLUSION:

    • I wouldn't go to such trouble on a new project without some easy-to-use out-of-the-box ORM solution.

    I'm curious if the Microsoft Entity Framework can handle such database designs well.

    Jeff and the rest of that Stack Overflow team must have had to deal with similar issues while developing Stack Overflow: Past revisions of edited questions and answers are saved and retrievable.

    I believe Jeff has stated that his team used Linq to SQL and MS SQL Server.

    I wonder how they handled these issues.

提交回复
热议问题