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.

¿Fue útil?

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...")

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

/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.

Licenciado bajo: CC-BY-SA con atribución
No afiliado a StackOverflow
scroll top