Welches ORM-Framework kann am besten mit einem MVCC-Datenbankdesign umgehen?
-
09-06-2019 - |
Frage
Wenn Sie eine Datenbank für die Verwendung von MVCC (Multi-Version Concurrency Control) entwerfen, erstellen Sie Tabellen entweder mit einem booleschen Feld wie „IsLatest“ oder einer Ganzzahl „VersionId“ und führen nie Aktualisierungen durch, sondern fügen neue Datensätze nur ein, wenn sich etwas ändert.
MVCC bietet Ihnen eine automatische Prüfung für Anwendungen, die einen detaillierten Verlauf erfordern, und entlastet außerdem die Datenbank im Hinblick auf Update-Sperren.Die Nachteile bestehen darin, dass die Datenmenge viel größer wird und die Auswahl verlangsamt wird, da eine zusätzliche Klausel erforderlich ist, um die neueste Version zu erhalten.Außerdem werden Fremdschlüssel dadurch komplizierter.
(Beachten Sie, dass ich nicht wir reden über die native MVCC-Unterstützung in RDBMS wie der Snapshot-Isolationsstufe von SQL Server)
Dies wurde in anderen Beiträgen hier auf Stack Overflow besprochen.[Alles – Links]
Ich frage mich, welche der vorherrschenden Entitäts-/ORM-Frameworks (Linq to Sql, ADO.NET EF, Hibernate usw.) diese Art von Design sauber unterstützen können?Dies ist eine wesentliche Änderung des typischen ActiveRecord-Entwurfsmusters, daher bin ich mir nicht sicher, ob die meisten verfügbaren Tools jemandem helfen könnten, der sich für diesen Weg mit seinem Datenmodell entscheidet.Mich interessiert vor allem, wie mit Fremdschlüsseln umgegangen wird, da ich nicht einmal sicher bin, wie ich sie am besten datenmodellieren kann, um MVCC zu unterstützen.
Lösung
Ich könnte darüber nachdenken, die MVCC-Ebene rein in der Datenbank zu implementieren und gespeicherte Prozesse und Ansichten für die Verarbeitung meiner Datenoperationen zu verwenden.Dann könnten Sie jedem ORM, der in der Lage ist, auf und von gespeicherten Prozessen zuzuordnen, eine vernünftige API bereitstellen und die Datenbank mit den Datenintegritätsproblemen befassen (da sie eigentlich dafür gebaut ist).Wenn Sie diesen Weg gegangen sind, sollten Sie sich vielleicht eine reinere Mapping-Lösung wie IBatis oder IBatis.net ansehen.
Andere Tipps
Ich habe eine Datenbank auf ähnliche Weise entworfen (nur INSERTs – keine UPDATEs, keine DELETEs).
Fast alle meiner SELECT-Abfragen betrafen Ansichten nur der aktuellen Zeilen für jede Tabelle (höchste Revisionsnummer).
Die Ansichten sahen so aus ...
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
Und meine Einfügungen (sowie Aktualisierungen und Löschungen) wurden alle von gespeicherten Prozeduren (eine pro Tabelle) verarbeitet.
Die gespeicherten Prozeduren sahen so aus ...
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
)
Revisionsnummern wurden im Visual Basic-Code pro Transaktion verarbeitet …
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
Ich habe für jede Tabelle ein Objekt erstellt, jedes mit Konstruktoren, Instanzeigenschaften und -methoden, Befehlen zum Erstellen, Aktualisieren und Löschen, einer Reihe von Suchfunktionen und IComparable-Sortierfunktionen.Es war eine riesige Menge Code.
Eins-zu-eins-DB-Tabelle zu VB-Objekt ...
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
Ein solches System behält alle früheren Versionen jeder Zeile bei, kann jedoch sehr mühsam zu verwalten sein.
VORTEILE:
- Gesamte Geschichte erhalten
- Weniger gespeicherte Prozeduren
NACHTEILE:
- verlässt sich für die Datenintegrität auf datenbankunabhängige Anwendungen
- riesige Menge an Code, der geschrieben werden muss
- Keine Fremdschlüssel in der Datenbank verwaltet (Auf Wiedersehen, automatische Objektgenerierung im Linq-to-SQL-Stil)
- Ich habe immer noch keine gute Benutzeroberfläche entwickelt, um die gesamte alte Versionierung abzurufen.
ABSCHLUSS:
- Ohne eine benutzerfreundliche, sofort einsatzbereite ORM-Lösung würde ich mir bei einem neuen Projekt nicht so viel Mühe machen.
Ich bin gespannt, ob das Microsoft Entity Framework mit solchen Datenbankdesigns gut zurechtkommt.
Jeff und der Rest des Stack Overflow-Teams mussten sich bei der Entwicklung von Stack Overflow mit ähnlichen Problemen auseinandersetzen:Frühere Überarbeitungen bearbeiteter Fragen und Antworten werden gespeichert und sind abrufbar.
Ich glaube, Jeff hat angegeben, dass sein Team Linq to SQL und MS SQL Server verwendet hat.
Ich frage mich, wie sie mit diesen Problemen umgegangen sind.
Soweit ich weiß, möchten ORM-Frameworks den CRUD-Code für Sie generieren, daher müssten sie explizit für die Implementierung einer MVCC-Option konzipiert sein;Ich kenne keinen, der das sofort macht.
Aus Sicht des Entity-Frameworks implementiert CSLA überhaupt keine Persistenz für Sie – es definiert lediglich eine „Datenadapter“-Schnittstelle, die Sie verwenden, um die benötigte Persistenz zu implementieren.So können Sie Codegenerierungsvorlagen (CodeSmith usw.) einrichten, um automatisch CRUD-Logik für Ihre CSLA-Entitäten zu generieren, die mit einer MVCC-Datenbankarchitektur einhergehen.
Dieser Ansatz würde höchstwahrscheinlich mit jedem Entity-Framework funktionieren, nicht nur mit CSLA, aber es wäre eine sehr „saubere“ Implementierung in CSLA.
Schauen Sie sich das Envers-Projekt an – funktioniert gut mit JPA/Hibernate-Anwendungen und erledigt das im Grunde genommen für Sie – verfolgt verschiedene Versionen jeder Entität in einer anderen Tabelle und bietet Ihnen SVN-ähnliche Möglichkeiten („Gib mir die Version von Person, die 2008–11 verwendet wird“) -05...")
/Jens
Ich dachte immer, Sie würden beim Aktualisieren und Löschen einen Datenbanktrigger verwenden, um diese Zeilen in eine TableName_Audit-Tabelle zu verschieben.
Das würde mit ORMs funktionieren, Ihnen Ihren Verlauf liefern und die ausgewählte Leistung in dieser Tabelle nicht beeinträchtigen.Ist das eine gute Idee oder übersehe ich etwas?
Wir verwenden einfach ein normales ORM (Ruhezustand) und verarbeiten den MVCC mit Ansichten + anstelle von Triggern.
Es gibt also eine v_emp-Ansicht, die einfach wie eine normale Tabelle aussieht. Sie können sie problemlos einfügen und aktualisieren. Wenn Sie dies tun, übernehmen die Trigger jedoch das tatsächliche Einfügen der richtigen Daten in die Basistabelle.
Nicht..Ich hasse diese Methode :) Ich würde eine API für gespeicherte Prozeduren verwenden, wie von Tim vorgeschlagen.