Domanda

Quando si progetta un database per utilizzare MVCC (Multi-Version Concurrency Control), si creano tabelle con un campo booleano come "IsLatest" o un numero intero "VersionId" e non si eseguono mai aggiornamenti, si inseriscono solo nuovi record quando le cose cambiano.

MVCC offre un controllo automatico per le applicazioni che richiedono una cronologia dettagliata e allevia anche la pressione sul database per quanto riguarda i blocchi degli aggiornamenti.Gli svantaggi sono che rende la dimensione dei dati molto più grande e rallenta le selezioni, a causa della clausola aggiuntiva necessaria per ottenere la versione più recente.Rende anche le chiavi esterne più complicate.

(Nota che lo sono non parlando del supporto MVCC nativo negli RDBMS come il livello di isolamento dello snapshot di SQL Server)

Questo è stato discusso in altri post qui su Stack Overflow.[da fare - link]

Mi chiedo, quale dei framework entità/ORM prevalenti (Linq to Sql, ADO.NET EF, Hibernate, ecc.) può supportare in modo pulito questo tipo di progettazione?Si tratta di un cambiamento importante rispetto al tipico modello di progettazione ActiveRecord, quindi non sono sicuro che la maggior parte degli strumenti disponibili possa aiutare qualcuno che decide di seguire questa strada con il proprio modello dati.Sono particolarmente interessato a come verrebbero gestite le chiavi esterne, perché non sono nemmeno sicuro del modo migliore per modellarle sui dati per supportare MVCC.

È stato utile?

Soluzione

Potrei prendere in considerazione l'implementazione del livello MVCC esclusivamente nel DB, utilizzando procedure e visualizzazioni archiviate per gestire le mie operazioni sui dati.Quindi potresti presentare un'API ragionevole a qualsiasi ORM in grado di mappare da e verso processi memorizzati e potresti lasciare che il DB si occupi dei problemi di integrità dei dati (poiché è praticamente costruito per quello).Se hai seguito questa strada, potresti voler considerare una soluzione di mappatura più pura come IBatis o IBatis.net.

Altri suggerimenti

Ho progettato un database in modo simile (solo INSERT - nessun AGGIORNAMENTO, nessun DELETE).

Quasi tutte le mie query SELECT erano contrarie alla visualizzazione delle sole righe correnti per ciascuna tabella (numero di revisione più alto).

Le visualizzazioni apparivano così...

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

E i miei inserimenti (e aggiornamenti ed eliminazioni) sono stati tutti gestiti da procedure memorizzate (una per tabella).

Le procedure memorizzate apparivano così...

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
        )

I numeri di revisione venivano gestiti per transazione nel codice Visual Basic...

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

Ho creato un oggetto per ogni tabella, ciascuno con costruttori, proprietà e metodi di istanza, comandi di creazione-aggiornamento-eliminazione, una serie di funzioni di ricerca e funzioni di ordinamento IComparable.Era un'enorme quantità di codice.

Tabella DB uno-a-uno su oggetto VB...

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

Un sistema di questo tipo preserva tutte le versioni precedenti di ogni riga, ma può essere davvero complicato da gestire.

PROFESSIONISTI:

  • Storia totale preservata
  • Meno procedure archiviate

CONTRO:

  • si basa su un'applicazione non database per l'integrità dei dati
  • enorme quantità di codice da scrivere
  • Nessuna chiave esterna gestita all'interno del database (addio generazione automatica di oggetti in stile Linq-to-SQL)
  • Non ho ancora trovato una buona interfaccia utente per recuperare tutto il controllo delle versioni passato conservato.

CONCLUSIONE:

  • Non mi prenderei questi problemi per un nuovo progetto senza una soluzione ORM pronta all'uso e facile da usare.

Sono curioso di sapere se Microsoft Entity Framework è in grado di gestire bene tali progetti di database.

Jeff e il resto del team di Stack Overflow devono aver dovuto affrontare problemi simili durante lo sviluppo di Stack Overflow:Le revisioni precedenti delle domande e delle risposte modificate vengono salvate e recuperabili.

Credo che Jeff abbia affermato che il suo team ha utilizzato Linq to SQL e MS SQL Server.

Mi chiedo come abbiano gestito questi problemi.

Per quanto ne so, i framework ORM vorranno generare il codice CRUD per te, quindi dovrebbero essere progettati esplicitamente per implementare un'opzione MVCC;Non conosco nessuno che lo faccia fuori dagli schemi.

Dal punto di vista del framework Entity, CSLA non implementa affatto la persistenza: definisce semplicemente un'interfaccia "Data Adapter" che utilizzi per implementare la persistenza di cui hai bisogno.Quindi potresti impostare modelli di generazione di codice (CodeSmith, ecc.) per generare automaticamente la logica CRUD per le tue entità CSLA che vanno insieme a un'architettura di database MVCC.

Questo approccio funzionerebbe con qualsiasi struttura di entità, molto probabilmente, non solo con CSLA, ma sarebbe un'implementazione molto "pulita" in CSLA.

Dai un'occhiata al progetto Envers - funziona bene con le applicazioni JPA/Hibernate e fondamentalmente lo fa per te - tiene traccia delle diverse versioni di ciascuna Entità in un'altra tabella e ti offre possibilità simili a SVN ("Dammi la versione di Persona utilizzata 2008-11 -05...")

http://www.jboss.org/envers/

/Jens

Ho sempre pensato che avresti utilizzato un trigger db durante l'aggiornamento e l'eliminazione per inserire quelle righe in una tabella TableName_Audit.

Funzionerebbe con gli ORM, ti fornirebbe la tua cronologia e non decimerebbe le prestazioni selezionate su quella tabella.E' una buona idea o mi sto perdendo qualcosa?

Quello che facciamo è semplicemente usare un normale ORM (hibernate) e gestire l'MVCC con visualizzazioni + invece che con trigger.

Quindi, c'è una vista v_emp, che assomiglia a una normale tabella, puoi inserirla e aggiornarla correttamente, quando lo fai, però, i trigger gestiscono effettivamente l'inserimento dei dati corretti nella tabella di base.

Non..Odio questo metodo :) Sceglierei un'API di procedura memorizzata come suggerito da Tim.

Autorizzato sotto: CC-BY-SA insieme a attribuzione
Non affiliato a StackOverflow
scroll top