¿Qué marco ORM puede manejar mejor un diseño de base de datos MVCC?
-
09-06-2019 - |
Pregunta
Al diseñar una base de datos para usar MVCC (Control de concurrencia de versiones múltiples), crea tablas con un campo booleano como "IsLatest" o un número entero "VersionId", y nunca realiza actualizaciones, solo inserta nuevos registros cuando las cosas cambian.
MVCC le brinda auditoría automática para aplicaciones que requieren un historial detallado y también alivia la presión sobre la base de datos con respecto a los bloqueos de actualización.Las desventajas son que aumenta mucho el tamaño de los datos y ralentiza las selecciones, debido a la cláusula adicional necesaria para obtener la última versión.También hace que las claves foráneas sean más complicadas.
(Ten en cuenta que soy no hablando sobre el soporte nativo de MVCC en RDBMS como el nivel de aislamiento de instantáneas de SQL Server)
Esto se ha discutido en otras publicaciones aquí en Stack Overflow.[todo - enlaces]
Me pregunto cuál de los marcos ORM/entidad predominantes (Linq to Sql, ADO.NET EF, Hibernate, etc.) puede admitir limpiamente este tipo de diseño.Este es un cambio importante en el patrón de diseño típico de ActiveRecord, por lo que no estoy seguro de si la mayoría de las herramientas que existen podrían ayudar a alguien que decida seguir este camino con su modelo de datos.Estoy particularmente interesado en cómo se manejarían las claves externas, porque ni siquiera estoy seguro de cuál es la mejor manera de modelarlas con datos para que admitan MVCC.
Solución
Podría considerar implementar el nivel MVCC exclusivamente en la base de datos, utilizando vistas y procesos almacenados para manejar mis operaciones de datos.Luego, podría presentar una API razonable para cualquier ORM que fuera capaz de mapear hacia y desde procesos almacenados, y podría dejar que la base de datos se ocupe de los problemas de integridad de los datos (ya que está prácticamente diseñada para eso).Si siguió este camino, es posible que desee buscar una solución de mapeo más pura como IBatis o IBatis.net.
Otros consejos
Diseñé una base de datos de manera similar (solo INSERTOS, sin ACTUALIZACIONES ni ELIMINACIONES).
Casi todas mis consultas SELECT se realizaron en vistas de solo las filas actuales de cada tabla (número de revisión más alto).
Las vistas se veían así...
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
Y todas mis inserciones (y actualizaciones y eliminaciones) fueron manejadas por procedimientos almacenados (uno por tabla).
Los procedimientos almacenados se veían así...
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
)
Los números de revisión se manejaban por transacción en el código de 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
Creé un objeto para cada tabla, cada una con constructores, propiedades y métodos de instancia, comandos de creación, actualización y eliminación, un montón de funciones de búsqueda y funciones de clasificación IComparable.Era una gran cantidad de código.
Tabla DB uno a uno a objeto 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 de este tipo conserva todas las versiones anteriores de cada fila, pero su gestión puede ser realmente complicada.
VENTAJAS:
- Historia total preservada
- Menos procedimientos almacenados
CONTRAS:
- Se basa en una aplicación que no es de base de datos para la integridad de los datos.
- gran cantidad de código por escribir
- No se administran claves externas dentro de la base de datos (adiós generación automática de objetos estilo Linq a SQL)
- Todavía no he creado una buena interfaz de usuario para recuperar todas las versiones anteriores conservadas.
CONCLUSIÓN:
- No me tomaría tantas molestias en un nuevo proyecto sin una solución ORM lista para usar y fácil de usar.
Tengo curiosidad por saber si Microsoft Entity Framework puede manejar bien estos diseños de bases de datos.
Jeff y el resto del equipo de Stack Overflow deben haber tenido que lidiar con problemas similares mientras desarrollaban Stack Overflow:Las revisiones anteriores de preguntas y respuestas editadas se guardan y se pueden recuperar.
Creo que Jeff ha declarado que su equipo utilizó Linq to SQL y MS SQL Server.
Me pregunto cómo manejaron estos problemas.
Hasta donde yo sé, los marcos ORM querrán generar el código CRUD para usted, por lo que tendrían que diseñarse explícitamente para implementar una opción MVCC;No conozco ninguno que lo haga de forma inmediata.
Desde el punto de vista del marco de Entity, CSLA no implementa la persistencia en absoluto; simplemente define una interfaz de "Adaptador de datos" que utiliza para implementar cualquier persistencia que necesite.Por lo tanto, podría configurar plantillas de generación de código (CodeSmith, etc.) para generar automáticamente lógica CRUD para sus entidades CSLA que van junto con una arquitectura de base de datos MVCC.
Este enfoque funcionaría con cualquier marco de entidad, muy probablemente, no solo con CSLA, pero sería una implementación muy "limpia" en CSLA.
Consulte el proyecto Envers: funciona bien con aplicaciones JPA/Hibernate y básicamente lo hace por usted; realiza un seguimiento de las diferentes versiones de cada entidad en otra tabla y le brinda posibilidades similares a las de SVN ("Dame la versión de Persona que se está utilizando 2008-11 -05...")
/Jens
Siempre pensé que usarías un activador de base de datos al actualizar y eliminar para enviar esas filas a una tabla TableName_Audit.
Eso funcionaría con ORM, le brindaría su historial y no diezmaría el rendimiento seleccionado en esa tabla.¿Es una buena idea o me falta algo?
Lo que hacemos es simplemente usar un ORM normal (hibernar) y manejar el MVCC con vistas + en lugar de activadores.
Entonces, hay una vista v_emp, que parece una tabla normal, puede insertarla y actualizarla bien; sin embargo, cuando hace esto, los activadores se encargan de insertar los datos correctos en la tabla base.
No..Odio este método :) Yo elegiría una API de procedimiento almacenado como lo sugirió Tim.