database - que - ¿Qué marco ORM puede manejar mejor un diseño de base de datos MVCC?
entity framework que es (6)
A mi leal saber y entender, los marcos ORM van a querer generar el código CRUD para usted, por lo que deberían estar explícitamente diseñados para implementar una opción MVCC; No conozco ninguno que lo haga de la caja.
Desde el punto de vista de un marco de Entity, CSLA no implementa la persistencia para usted en absoluto; solo define una interfaz de "Adaptador de datos" que utiliza para implementar cualquier persistencia que necesite. De modo que podría configurar plantillas de generación de código (CodeSmith, etc.) para generar automáticamente la lógica CRUD para sus entidades CSLA que vayan junto con una arquitectura de base de datos MVCC.
Este enfoque funcionaría con cualquier marco de entidad, muy probablemente, no solo CSLA, sino que sería una implementación muy "limpia" en CSLA.
Al diseñar una base de datos para usar MVCC (Control de Concurrencia de Varias Versiones), usted crea tablas con un campo booleano como "IsLatest" o un entero "VersionId", y nunca hace ninguna actualización, solo inserta nuevos registros cuando cambian las cosas.
MVCC le ofrece 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 hace que su tamaño de datos sea mucho más grande 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.
(Tenga en cuenta que no estoy hablando de la compatibilidad con MVCC nativa 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 de entidad / ORM prevalentes (Linq a Sql, ADO.NET EF, Hibernate, etc.) puede respaldar este tipo de diseño? Este es un cambio importante en el patrón de diseño de ActiveRecord típico, por lo que no estoy seguro de si la mayoría de las herramientas que existen podrían ayudar a alguien que decida ir por esta ruta con su modelo de datos. Estoy particularmente interesado en cómo se manejarán las claves externas, porque ni siquiera estoy seguro de la mejor manera de modelar los datos para admitir MVCC.
Podría considerar implementar el nivel de MVCC puramente en el DB, usando procesos y vistas almacenados para manejar mis operaciones de datos. Luego, podría presentar una API razonable para cualquier ORM que fuera capaz de mapear desde y hacia procs almacenados, y podría dejar que la DB se ocupe de los problemas de integridad de los datos (ya que se trata de una compilación para eso). Si fue por este camino, es posible que desee ver una solución de mapas más pura como IBatis o IBatis.net.
Diseñé una base de datos de manera similar (solo INSERTs - no UPDATEs, no DELETE).
Casi todas mis consultas SELECT estaban en contra de vistas de solo las filas actuales para 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 mis insertos (y actualizaciones y eliminaciones) fueron manejados 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 manejaron 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 uno con constructores, propiedades y métodos de instancia, comandos de crear-actualizar-eliminar, un conjunto de funciones de búsqueda y funciones de clasificación IComparable. Fue una gran cantidad de código.
Tabla de BD uno a uno para el 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
Tal sistema preserva todas las versiones pasadas de cada fila, pero puede ser un verdadero dolor para administrar.
PROS:
- Historia total preservada
- Menos procedimientos almacenados
CONTRAS:
- se basa en la aplicación de bases de datos para la integridad de datos
- gran cantidad de código para ser escrito
- No se administraron claves externas dentro de la base de datos (adiós a la generación automática de objetos estilo Linq-to-SQL)
- Todavía no se me ocurrió una buena interfaz de usuario para recuperar todo lo preservado en el pasado.
CONCLUSIÓN:
- No me tomaría tantas molestias en un nuevo proyecto sin una solución ORM lista para usar fácil de usar.
Tengo curiosidad si Microsoft Entity Framework puede manejar bien esos diseños de bases de datos.
Jeff y el resto del equipo de deben haber tenido que lidiar con problemas similares al desarrollar : las revisiones anteriores de las preguntas y respuestas editadas se guardan y recuperan.
Creo que Jeff ha declarado que su equipo usó Linq para SQL y MS SQL Server.
Me pregunto cómo manejaron estos problemas.
Siempre pensé que utilizarías un desencadenador db en la actualización y eliminaría esas filas en una tabla TableName_Audit.
Eso funcionaría con los ORM, le daría su historial y no reduciría el rendimiento de selección en esa tabla. ¿Es una buena idea o me estoy perdiendo algo?
Echa un vistazo al proyecto Envers: funciona bien con las aplicaciones JPA / Hibernate y básicamente lo hace por ti: realiza un seguimiento de las diferentes versiones de cada Entity en otra tabla y te brinda posibilidades similares a SVN ("Dame la versión de Person que se usa 2008-11 -05 ... ")
/ Jens
Lo que hacemos, es usar un ORM (hibernación) normal y manejar el MVCC con vistas + en lugar de desencadenantes.
Entonces, hay una vista v_emp, que simplemente se ve como una tabla normal, puede insertarla y actualizarla bien, sin embargo, cuando hace esto, los desencadenantes se encargan de insertar los datos correctos en la tabla base.
No ... Odio este método :) Iría con una API de procedimiento almacenado como lo sugiere Tim.