none
corrección a una función UPDATE que controle la concurrencia RRS feed

  • Pregunta

  • Hola todos,

    TEngo un problema en esta función update que pretende controlar los problemas de concurrencia. 

    Trabajo con vb.net 2010 y MySql

    Este es el código que uso en la capa DataAcess:

    Public Shared Function Update(ByVal grupos As GruposEN) As Boolean Dim conn As New MySqlConnection Dim cmd As New MySqlCommand Dim adapter As New MySqlDataAdapter '-- Dim objconn As New Conexion 'Try 'Abrimos la conexion conn = objconn.open ' Creacion del grupo adapter = New MySqlDataAdapter( _ "SELECT f_modifica FROM grupos WHERE id_grupo ?id_grupo", conn) ' el comando Update chequea las violaciones para la concurrencia optimista ' en la cláusula WHERE. Dim sql As String = "UPDATE grupos SET nombre = ?nombre, id_user_modifica = ?id_user_modifica " & "WHERE id_grupo = ?id_grupo " & " and f_modifica = ?f_modifica " adapter.UpdateCommand = New MySqlCommand(sql, conn) 'Pasamos los valores originales de los parámetros de la cláusula WHERE. adapter.UpdateCommand.Parameters.Add("?id_grupo", MySqlDbType.VarChar, 10) adapter.UpdateCommand.Parameters.Add("?nombre", MySqlDbType.VarChar, 20) adapter.UpdateCommand.Parameters.Add("?id_user_modifica", MySqlDbType.VarChar, 10) adapter.UpdateCommand.Parameters.Add("?f_modifica", MySqlDbType.Timestamp) 'Agregar el controlador de eventos RowUpdated. AddHandler adapter.RowUpdated, New MySqlRowUpdatedEventHandler( _ AddressOf OnRowUpdated) Dim dataSet As DataSet = New DataSet() adapter.Fill(dataSet, "grupos") 'Modificar el contenido de DataSet. adapter.Update(dataSet, "grupos") Dim dataRow As DataRow For Each dataRow In dataSet.Tables("grupos").Rows If dataRow.HasErrors Then Console.WriteLine(dataRow(0) & vbCrLf & dataRow.RowError) End If Next conn.Close() 'Si se ejecuta hasta aqui, entonces la grabación fue exitosa Return True Catch db As DBConcurrencyException MsgBox("Atención! otro usuario ha modificado el registro antes que usted", vbInformation) Return False Catch ex As Exception MsgBox("Error: " & vbCr & ex.Message & vbCr & ". Consulte al administrador del sistema", vbCritical) Return False End Try End Function

    Private Shared Sub OnRowUpdated(ByVal sender As Object, ByVal args As MySqlRowUpdatedEventArgs)
            If args.RecordsAffected = 0 Then
                args.Row.RowError = "Atención! otro usuario modificó este registro antes que usted."
                args.Status = UpdateStatus.SkipCurrentRow
            End If
        End Sub

    en la tabla mysql llamada grupos el campo f_modifica está definido como timestamp

    En mi capa de Entities

    en la clase para esa tabla, defino el campo f_modifica así:

        Private v_f_modifica As TimeSpan
        Public Property f_modifica() As TimeSpan
            Get
                Return v_f_modifica
            End Get
            Set(ByVal value As TimeSpan)
                v_f_modifica = value
            End Set
        End Property

    Cuando ejecuto me sale este error:

    Que estará fallando y les pregunto ¿es esta la mejor manera de manejar la concurrencia?

    Les agradecería su valiosa colaboración...

    si hubiese otra forma más eficiente o práctica de realizar el control a concurrencia les agradecería.


    Saludos, Solph.

    viernes, 30 de noviembre de 2012 22:27

Respuestas

  • Pues le completo un poco más las clases entonces.  La clase Entidad tendría la siguiente función virtual:

    public virtual DAL.IDatosEntidad GetData()
    {
        //Aquí creamos un objeto que implemente la interfase DAL.IDatosEntidad.
        //No he mostrado código para eso, pero le cuento que es un Dictionary
        //que implementa la interfase.  Bien sencillo.  No nos compliquemos con esto.
        MiDTO datos = new MiDTO();
        datos["TimeStamp"] = TimeStamp;
        return datos;
    }

    Todas las clases que deriven de Entidad sobreescriben el método GetData para adicionar los datos específicos de su clase.

    La clase MiDTO sería algo como:

    public class MiDTO : Dictionary<string, object>, DAL.IDatosEntidad
    { }
    //Y la interfase simplemente define la propiedad Index que un Dictionary ya tiene definida, por eso uso Dictionary arriba.
    public interface IDatosEntidad
    {
        object this[string campo] { get; set; }
    }


    Luego entonces ya le puedo mostrar un código para guardar:

    //Esto sería la capa de datos:
    public static class Clientes
    {
        public static DAL.IDatosEntidad Guardar(DAL.IDatosEntidad entidad)
        {
            using (SqlConnection conn = new SqlConnection("<cadena de conexión aquí"))
            {
                using (SqlCommand cmd = new SqlCommand("UPDATE tblClientes Set Nombre = @Nombre Where ID = @ID and TimeStamp = @TimeStamp;", conn))
                {
                    cmd.Parameters.AddWithValue("nombre", entidad["Nombre"]);
                    cmd.Parameters.AddWithValue("ID", entidad["ID"]);
                    cmd.Parameters.AddWithValue("TimeStamp", entidad["TimeStamp"]);
                    int filasModificadas = cmd.ExecuteNonQuery();
                    if (filasModificadas == 0)
                    {
                        //Asumo que el timestamp es distinto y por eso no hubo filas modificadas.
                        throw new RegistroPreviamenteModificadoException();
                    }
                    //El registro fue guardado exitosamente.  Ahora hay que obtener el nuevo valor del timestamp y devolverlo.
                    //De hecho, pueden haber más campos cambiados por triggers y demás.  Por eso siempre es bueno
                    //actualizar todos los valores después de un UPDATE.
                    cmd.CommandText = "Select * From tblClientes Where ID = @ID";
                    cmd.Parameters.Clear();
                    cmd.Parameters.Add("ID", entidad["ID"]);
                    MiDTO datos = new MiDTO();
                    using (SqlDataReader r = cmd.ExecuteReader())
                    {
                        if (r.Read())
                        {
                            datos["TimeStamp"] = r["TimeStamp"];
                            datos["ID"] = r["ID"];
                            datos["Nombre"] = r["Nombre"];
                        }
                    }
                    return datos;
                }
            }
        }
    }

    Algo así.  Como verá, la capa de datos recibe los datos a guardar de la misma exacta forma que los envía:  Usando un DTO (Data Transfer Object).

    Eso es el esquema super básico sin control de errores, sin uso de procedimientos almacenados (que es lo que me gusta usar a mí), sin pensar en concurrencia.

    De hecho le menciono que estas operaciones de grabado deben hacerse sobre una transacción que permita trabajar atómicamente sobre la tabla.  O se usan instrucciones SQL más complejas como MERGE o UPDATE + OUTPUT o se crea una transacción con niveles de aislamiento SERIALIZABLE (en SQL Server al menos, no sé de MySQL).

    En resumen:  Así es como yo trabajo todo el tiempo:  En capas.  Si buscaba código con DataTable's, pues no lo tendrá de mí porque son para principiantes y pues yo ya dejé de serlo hace bastante tiempo. :-P


    Jose R. MCP
    Code Samples

    • Marcado como respuesta Solp lunes, 3 de diciembre de 2012 11:00
    sábado, 1 de diciembre de 2012 22:42

Todas las respuestas

  • Pues creo que necesita de expertos en MySQL.  Yo uso timestamp en SQL Server y el dato en .net se trata como un arreglo de 8 bytes.  No sé si eso es factible en MySQL.  Si esto fuera SQL Server supongo que yo hubiera podido ayudarle más.

    También veo que la excepción es muy general.  ¿No existe una excepción anidada en InnerException?  Tal vez contenga más información.

    En general le recomiendo hacer esta pregunta en un foro de MySQL.  Creo que tendría más suerte.


    Jose R. MCP
    Code Samples

    viernes, 30 de noviembre de 2012 23:30
  • Hola Solp,

    ¿Has probado a usar DateTime en vez de TimeSpan la propiedad f_modifica? Creo que has escogido mal el tipo

    Espero haberte ayudado


    @XaviPaper
    http://geeks.ms/blogs/xavipaper

    viernes, 30 de noviembre de 2012 23:38
  • Hola WebJose y XaviPaper

    Gracias por su colaboración...

    voy a hacer lo que me dicen... inicialmente lo de DateTime en vez de TimeSpan.

    Pregunto algo:

    para recuperar el valor TimeStamp debo utilizar 

    DataAdapter y adapter.UpdateCommand ?

    cómo lo hacen ustedes sin importar que DB usen, podrían regalarme algún fragmento del código...

    Les agradezco su ayuda


    Saludos, Solph.

    sábado, 1 de diciembre de 2012 1:58
  • Toda la idea del timestamp es prevenir esta situación:  

    • Usuario A consulta registro.
    • Usuario B consulta el mismo registro unos segundos después que Usuario A.
    • Usuario A quiere modificar registro, pero resulta que Usuario B hizo una modificación que actualmente Usuario A desconoce.
    • Usuario A procede a actualizar registro sin saber que Usuario B ya ha hecho un cambio.
    • Resultado:  Los cambios de Usuario B se han perdido porque Usuario A ha grabado una versión del registro ANTERIOR a la que había.

    ¿Cómo se usa el timestamp para prevenir esto?  Sencillo:  El motor de base de datos promete que asignará un valor único y monótonamente creciente a cada registro de la tabla, y que aumentará su valor cada vez que se actualice el registro.  Este valor (el timestamp) es un número, aunque puede ser un valor de fecha.  Supongo que depende del motor de base de datos.  En fin, con esa promesa, los usuarios modifican su comportamiento de esta manera:

    • Usuario A consulta registro, asegurándose de obtener el valor del timestamp.
    • Usuario B consulta el mismo registro unos segundos después que Usuario A, también obteniendo el timestamp.
    • Usuario A quiere modificar registro, pero resulta que Usuario B hizo una modificación que actualmente Usuario A desconoce.
    • Usuario A procede a actualizar registro pero únicamente si el timestamp que él posee es el que actualmente se encuentra en base de datos.
    • Resultado:  Los cambios de Usuario A no modifican ningún registro porque el timestamp no coincide.  La aplicación en este momento puede detectar que hubo cero filas actualizadas y procede a indicarle al usuario que hay una nueva versión del registro que debe ser cargada antes de proceder con los cambios.

    Entonces ¿cómo se usa en la práctica?

    • Todos los SELECT DEBEN incluir el timestamp.
    • Todos los UPDATE DEBEN incluir una cláusula WHERE campoTimestamp = <valor conocido del timestamp>.  Ejemplo:  Update Tabla Set Campo1 = <valor1>, campo2 = <valor2>, ... Where ID = <valor del id> AND campoTimestamp = <valor conocido>.  En SQL Server uno puede usar @@RowCount para saber cuántos registros fueron afectados por el UPDATE.  Si fue cero asumimos que es por un cambio en el timestamp; puede verificarse esto rápidamente con un SELECT por ID si fuera necesario.


    Jose R. MCP
    Code Samples

    sábado, 1 de diciembre de 2012 5:44
  • Gracias WebJose por tus comentarios.

    Sin embargo, lo que necesito es un fragmento de código sin importar el lenguaje que usen o la DB que tengan. Los conceptos los tengo claro. Lo que necesito es qué utilizo: si dataAdapter, etc. 

    Con un fragmento de código que sea eficiente y eficaz que me proporcionen es suficiente. Les agradezco...



    Saludos, Solph.

    sábado, 1 de diciembre de 2012 18:46
  • Pues yo nunca uso DataSet o DataTable y tampoco Data adapters.  Yo siempre uso una capa de negocio con clases.  En un proyecto Windows Forms usaría Data Binding a un List<MiClase> donde MiClase sería el objeto a representar, que tendría una propiedad Timestamp.

    Para ponerle un ejemplo concreto, yo tendría algo como esto en mi capa de negocio:

    //La clase base:
    public class Entidad : INotifyPropertyChanged
    {
        #region Property Changed
        public event PropertyChangedEventHandler PropertyChanged;
        protected void RaisePropertyChanged(string propertyName)
        {
            PropertyChangedEventHandler ev = PropertyChanged;
            if (ev != null)
            {
                ev(this, new PropertyChangedEventArgs(propertyName));
            }
        }
        #endregion
    
        #region Propiedades
        private byte[] m_TimeStamp;
        public byte[] TimeStamp
        {
            get
            {
                return m_TimeStamp;
            }
            protected set
            {
                m_TimeStamp = value;
                RaisePropertyChanged("TimeStamp");
            }
        }
        #endregion
    
        #region Constructores
        //Yo uso el patrón Data Transfer Object.  Yo paso a los constructores de las entidades
        //dicho objeto, representado aquí por una interfase de mi propia creación.
        public Entidad(DAL.IDatosEntidad datos)
        {
            TimeStamp = (byte[])datos["TimeStamp"];
        }
        public Entidad()
        { }
        #endregion
    }
    
    //Y luego todas las demás clases de la capa de negocio heredan de Entidad.  Como ejemplo:
    public class Cliente : Entidad
    {
        #region Propiedades
        private int m_ID;
        public int ID
        {
            get
            {
                return m_ID;
            }
            protected set
            {
                m_ID = value;
                RaisePropertyChanged("ID");
            }
        }
        private string m_Nombre;
        public string Nombre
        {
            get
            {
                return m_Nombre;
            }
            set
            {
                m_Nombre = value;
                RaisePropertyChanged("Nombre");
            }
        }
        #endregion
    
        #region Constructores
        public Cliente(DAL.IDatosEntidad datos)
            : base(datos)
        {
            ID = (int)datos["ID"];
            Nombre = datos["Nombre"].ToString();
        }
        public Cliente()
            : base()
        { }
        #endregion
    
        #region Repositorio.  //No suelo crear repositorios como clases separadas.
        public static List<Cliente> Todos()
        {
            //Obtener de la capa de datos todos los clientes en la forma de objetos DAL.IDatosEntidad:
            List<Cliente> clientes = new List<Cliente();
            foreach (DAL.IDatosEntidad datos in DB.Clientes.Todos())
            {
                clientes.Add(new Cliente(datos));
            }
            return clientes;
        }
        #endregion
    }
    

    Entonces como verá mi capa de negocio me provee todos los clientes como una lista de objetos tipo Cliente que ya vienen populadas con el timestamp.

    En un proyecto Windows Forms tendría esta lista ligada a un DGV, y podría editar los objetos y demás sin problema.  Luego con un botón de Guardar Cambios podría solicitarle a la capa de datos una actualización sobre todos los objetos modificados.  La capa de datos recibiría el timestamp que se obtuvo cuando se creó el objeto Cliente y lo usaría en una consulta UPDATE para asegurarse que no estaría sobreescribiendo otra actualización según lo expliqué anteriormente.

    ¿Le aclara esto el panorama o necesita ver más código de ejemplo?


    Jose R. MCP
    Code Samples

    sábado, 1 de diciembre de 2012 19:06
  • Hola WebJose

    Gracias por estar disponible a colaborar...

    Este código es muy interesante para mí... como te habrás dado cuenta no soy experto sino más bien nuevo en esto de capas y visual studio .net

    tú mencionas "La capa de datos recibiría el timestamp que se obtuvo cuando se creó el objeto Cliente y lo usaría en una consulta UPDATE para asegurarse que no estaría sobreescribiendo otra actualización"

    Si puedes pasarme este parte del código... te agradecería y creo que me quedaría muy claro. Gracias


    Saludos, Solph.

    sábado, 1 de diciembre de 2012 20:12
  • Pues le completo un poco más las clases entonces.  La clase Entidad tendría la siguiente función virtual:

    public virtual DAL.IDatosEntidad GetData()
    {
        //Aquí creamos un objeto que implemente la interfase DAL.IDatosEntidad.
        //No he mostrado código para eso, pero le cuento que es un Dictionary
        //que implementa la interfase.  Bien sencillo.  No nos compliquemos con esto.
        MiDTO datos = new MiDTO();
        datos["TimeStamp"] = TimeStamp;
        return datos;
    }

    Todas las clases que deriven de Entidad sobreescriben el método GetData para adicionar los datos específicos de su clase.

    La clase MiDTO sería algo como:

    public class MiDTO : Dictionary<string, object>, DAL.IDatosEntidad
    { }
    //Y la interfase simplemente define la propiedad Index que un Dictionary ya tiene definida, por eso uso Dictionary arriba.
    public interface IDatosEntidad
    {
        object this[string campo] { get; set; }
    }


    Luego entonces ya le puedo mostrar un código para guardar:

    //Esto sería la capa de datos:
    public static class Clientes
    {
        public static DAL.IDatosEntidad Guardar(DAL.IDatosEntidad entidad)
        {
            using (SqlConnection conn = new SqlConnection("<cadena de conexión aquí"))
            {
                using (SqlCommand cmd = new SqlCommand("UPDATE tblClientes Set Nombre = @Nombre Where ID = @ID and TimeStamp = @TimeStamp;", conn))
                {
                    cmd.Parameters.AddWithValue("nombre", entidad["Nombre"]);
                    cmd.Parameters.AddWithValue("ID", entidad["ID"]);
                    cmd.Parameters.AddWithValue("TimeStamp", entidad["TimeStamp"]);
                    int filasModificadas = cmd.ExecuteNonQuery();
                    if (filasModificadas == 0)
                    {
                        //Asumo que el timestamp es distinto y por eso no hubo filas modificadas.
                        throw new RegistroPreviamenteModificadoException();
                    }
                    //El registro fue guardado exitosamente.  Ahora hay que obtener el nuevo valor del timestamp y devolverlo.
                    //De hecho, pueden haber más campos cambiados por triggers y demás.  Por eso siempre es bueno
                    //actualizar todos los valores después de un UPDATE.
                    cmd.CommandText = "Select * From tblClientes Where ID = @ID";
                    cmd.Parameters.Clear();
                    cmd.Parameters.Add("ID", entidad["ID"]);
                    MiDTO datos = new MiDTO();
                    using (SqlDataReader r = cmd.ExecuteReader())
                    {
                        if (r.Read())
                        {
                            datos["TimeStamp"] = r["TimeStamp"];
                            datos["ID"] = r["ID"];
                            datos["Nombre"] = r["Nombre"];
                        }
                    }
                    return datos;
                }
            }
        }
    }

    Algo así.  Como verá, la capa de datos recibe los datos a guardar de la misma exacta forma que los envía:  Usando un DTO (Data Transfer Object).

    Eso es el esquema super básico sin control de errores, sin uso de procedimientos almacenados (que es lo que me gusta usar a mí), sin pensar en concurrencia.

    De hecho le menciono que estas operaciones de grabado deben hacerse sobre una transacción que permita trabajar atómicamente sobre la tabla.  O se usan instrucciones SQL más complejas como MERGE o UPDATE + OUTPUT o se crea una transacción con niveles de aislamiento SERIALIZABLE (en SQL Server al menos, no sé de MySQL).

    En resumen:  Así es como yo trabajo todo el tiempo:  En capas.  Si buscaba código con DataTable's, pues no lo tendrá de mí porque son para principiantes y pues yo ya dejé de serlo hace bastante tiempo. :-P


    Jose R. MCP
    Code Samples

    • Marcado como respuesta Solp lunes, 3 de diciembre de 2012 11:00
    sábado, 1 de diciembre de 2012 22:42
  • Muchas gracias WebJose

    voy a analizarlo e implementar algo similar...


    Saludos, Solph.

    lunes, 3 de diciembre de 2012 11:00
  • Gracias Xavi Paper,

    por tu observación...


    Saludos, Solph.

    lunes, 3 de diciembre de 2012 11:03
  • Hola,

    Quisiera aclarar como funciona una columna TimeStamp (Marca de tiempo) en MySQL, tal vez eso le simplifique algo de codigo al colega.  

    En MySQL una columna definida como TimeStamp se actualiza automaticamente al insertar o actualizar el registro con la fecha y hora actual cuando no le especifiquemos el valor o incluso cuando le asignemos = NULL.

    Eso quiere decir que la sentencia de insert deberia ser, por ejemplo, asi:

    INSERT INTO CLIENTES (ID,RAZON_SOCIAL) VALUES (1,'Empresa S.A.')

    Para el update:

    UPDATE CLIENTES SET RAZON_SOCIAL='Nueva Empresa S.A.' WHERE ID=1

    Si la tabla contiene un campo timestamp el valor sera actualizado automaticamente con la fecha actual.

    Victor Koch

    martes, 4 de diciembre de 2012 16:30