jueves, 24 de marzo de 2016

Autonuméricos, EntityFramework Sql y Oracle




El uso de Entity Framework, cada vez está más en nuestro día a día, con una cantidad de versiones que avalan a este OMR de Microsoft.  

Normalmente suele estar muy ligado a Sql Server, y la parte para Oracle es mucho menos conocida, aunque no por ello no menos práctica. 

Esta entrada trata de explicar de forma bastante extensa el uso para EF de Autonuméricos, difiniendo este término por las propiedades Identity o Secuencias en Oracle, que para este ORM no tiene ningún tratamiento especial y todo hay, hay que hacerlo un poco de 0. 

El contenido lo creé para elaborar un artículo en CodeProyect por allí por el 2013, pero si contamos con que no tengo mucho control con la lengua de Shakespeare y que traspapelé su versión, pues éste se quedó en el olvido. No hace mucho que he vuelto a encontrarlo en castellano, y al estar ejecutado sobre la versión de EF 5.0, completamente compatible con la actual 6.2, he creído que podría ser de utilidad.

Dentro de la versión de Entity Framework Core/7.0, tendremos una nueva característica que nos permitirá hacer una comunión con grupos de claves autonuméricas entre el cliente y la BD, que añadirá otra opción más a todas las que aparecen bajo estas líneas. Por supuesto todavía queda bastante para la versión final de Sql Server y que decir para la de Oracle, aunque últimamente se están poniendo las pilas y liberan sus versiones de ODP.Net para EntityFramework con fechas muy cercanas a las salidas de Visual Studio y el Net. Framework.

No es que me guste demasiado meter así post de otras tecnologías, soy más de empezar algo desde el principio como con LinQ, pero así esto no se quedará en el olvido. Si el tiempo y la entrega me lo permiten me gustaría atacar WPF, que hay un vacío en español inmensurable y después EntityFramework. Por mi cabeza también pasa tratar la TPL (Task Parallel Library), TDD, Unit Tests, y un largo etc, pero no se si el tiempo me lo permitirá. por ahora ahí queda eso.





Índice
1.       Introducción
2.       Identity-Secuences Vs GUIDS (Globally Unique Identifiers)
3.       Sql Server
       Scripts
       Identity
       Guid
4.       Oracle
       Scripts
       Secuence

       Guid



Introducción
El objetivo de este artículo, es unificar y mostrar con ejemplos descriptivos las diferentes formas de usar los campos Identity de Sql Server y las secuencias de Oracle dentro del ámbito de Entity Framework. Si bien los primeros son francamente sencillos de utilizar, intentaremos ampliar los casos de uso y destacar los puntos clave. En el caso de la secuencias, su uso es mucho más restringido y el soporte dentro de Entity Framework es prácticamente nulo, por lo que habrá que utilizar caminos paralelos para conseguir que su uso sea lo más llevadero posible.

El segundo punto clave del artículo se basa en la elección de los cada vez más utilizados campos GUID y las virtudes y los defectos que tienen si los enfrentamos a los Identity-Sequences. 




Identity-Secuences Vs GUIDS (Globally Unique Identifiers)
Dejar claro que existen otros tipos de GUIDS (ordenados, etc) y que no se comentará en este artículo.

Bondades de cada uno de los tipos:
GUIDS
1.       16 bytes de tamaño
2.       No tienen que viajar a la BD para calcular su valor (trabajo offline)
3.       Son fácilmente calculables en código, en la parte cliente.
4.       Aumenta la seguridad ya que no se puede predecir su siguiente valor.
5.       Sencillez de traspaso de datos entre entornos ya que no tienen que guardar un orden.
6.       Fácil Implementación de la capa de datos DAL
7.       Genera una clave única en toda la base de datos
8.       Permite fácilmente mezclar registros de diferentes tablas

Identity-Secuences
1.       4 bytes de tamaño
2.       Se pueden utilizar como índice para ordenar los datos
3.       Fácil compresión del campo ID para depurar y realizar búsquedas.
4.       Tipo de datos simples para la columnas de guardado (int, double, decimal)
5.       Menor tamaño de guardado.
6.       Mayor rendimiento para Qrys extremas (Joins)





Sql Server

Propiedades de las PKs para ambos casos y para ambos tipos:



















Scripts

-- Con PK Identity
USE [PruebasBD]
GO

SET ANSI_NULLS ON
GO

SET QUOTED_IDENTIFIER ON
GO

CREATE TABLE [dbo].[CustomersV1](
       [ID] [int] IDENTITY(1,1) NOT NULL,
       [CompantyName] [nvarchar](50) NOT NULL,
       [City] [nvarchar](50) NOT NULL,
       [EntryDate] [date] NOT NULL,
 CONSTRAINT [PK_CustomersV1] PRIMARY KEY CLUSTERED  (
       [ID] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON,
ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY]


GO




-- Con PK Guid
USE [PruebasBD]
GO

SET ANSI_NULLS ON
GO

SET QUOTED_IDENTIFIER ON
GO

CREATE TABLE [dbo].[CustomersV2](
       [ID] [uniqueidentifier] NOT NULL,
       [CompantyName] [nvarchar](50) NOT NULL,
       [City] [nvarchar](50) NOT NULL,
       [EntryDate] [date] NOT NULL,
 CONSTRAINT [PK_CustomersV2] PRIMARY KEY CLUSTERED 
(
       [ID] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON,
ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY]

GO

ALTER TABLE [dbo].[CustomersV2] ADD  CONSTRAINT [DF_CustomersV2_ID]  DEFAULT (newid()) FOR [ID]

GO



Identity


El uso en Entity Framework de este tipo de pk es sencillísimo, simplemente nos olvidaremos de la PK, ya que el motor no lo tendrá en cuenta y utilizará el siguiente valor calculado de la columna Identity. 


using (var context = new PruebasBDEntities()) 
{ 
    /// without ID 
    var cv1 = new CustomersV1 { CompantyName = "Company 1", City = "Madrid", EntryDate = DateTime.Today };
    /// with wrong ID 
    var cv2 = new CustomersV1 { ID = 0000, CompantyName = "Company 2", City = "Rome", EntryDate = DateTime.Today };
    context.CustomersV1.Add(cv1);     
    context.CustomersV1.Add(cv2); 
    int changes = context.SaveChanges();
}

Como podemos observar en la creación del objeto cv2, aunque le indiquemos un ID, este será omitido a la hora de ejecutar su consulta insert en la BD. 





Una de las limitaciones que presenta el uso de las claves Identity, es que no tenemos forma de conocer el valor de la nueva clave, hasta que no llamemos al método SaveChanges(), hasta ese momento si realizamos una inspección al objeto recién creado, el valor de su clave ID siempre será 0.


Aunque no es una forma 100% fiable, nos podríamos acercar lo máximo posible a este valor realizando un método dentro de nuestra clase parcial DbContext: 


public partial class PruebasBDEntities 
{ 
    public decimal GetNextIdentityVal(string tableName) 
    { 
        string qry = string.Format("SELECT IDENT_CURRENT('{0}') + IDENT_INCR('{0}') from {0}", tableName); 
  
        decimal result = -1; 
                 try         { 
            result = this.Database.SqlQuery<decimal>(qry).First(); 
        } 
        catch (Exception ex) 
        { 
            throw  new Exception("Wrong TableName", ex);         } 
  
        return result; 
    } 
} 



*** Esta consulta simplemente nos devolverá el siguiente valor del Identity, pero no moverá su secuencia.

Este dato solo sería fiable a la hora de realizar la consulta y tendría básicamente las mismas carencias que utilizar:

Select max(ID) + 1 from Table


Por lo que nuestro consejo es no utilizar en bases de datos con mucha concurrencia y solo utilizar como valor de referencia. 



Guid


Al contrario que con los campos Identity, con los Guid tenemos 2 opciones de generar nuestras pks:

1.       Generar la pk en el lado del servidor, dejando al motor de base de datos que realice el trabajo.

2.       Generar la pk en el lado del cliente, generándola directamente en código. 



BASE DE DATOS

La forma de uso es muy similar a la utilizada por los campos Identity, pero a diferencia de estos, para los Guid tenemos que realizar una serie de configuraciones, tanto en el servidor como en el cliente.

Configuración en Sql Server:





El campo PK, deberá configurarse de tipo ‘uniqueidentifier’ y el valor por defecto, será el resultado de la llamada a la función newid().


Configuración en Visual Studio: 




En segundo lugar configuraremos dentro de nuestro EDM, la propiedad StoreGeneratedPattern de la PK Guid de nuestra entidad a Identity.

Vamos a verlo en funcionamiento:


        using (var context = new PruebasBDEntities()) 
        { 
            /// without ID 
            var cv1 = new CustomersV2 { CompantyName = "Company 1", City = "Madrid", EntryDate = DateTim e.Today };
            /// with wrong ID 
            var cv2 = new CustomersV2 { ID = Guid.NewGuid(), CompantyName = "Company 2", City = "Rome", EntryDate = DateTim e.Today };

            Console.WriteLine(cv2.ID); 
            context.CustomersV2.Add(cv1);     
            context.CustomersV2.Add(cv2); 
  
            int changes = context.SaveChanges();
        }


Resultado: 






Como podemos apreciar en los resultados, al igual que en el caso del Identity, aunque le proporcionemos un valor válido para la columna ID (cv2), este lo omitirá y generará uno nuevo en el servidor. 



CLIENTE
Esta es una de las ventajas que atesora este tipo de datos, y que subsana la limitación que tenían los Identity, ya que de esta manera tenemos disponible desde el mismo momento de la creación el valor correspondiente a nuestra columna ID, y además no tenemos que realizar un viaje al servidor para conocer su valor.

Lo primero que haremos será devolver el valor de la propiedad StoreGeneratedPattern de la PK de tipo Identity a None.

Con esto ya nos bastaría, aunque para dejarlo más correcto podríamos también limpiar el valor por defecto del servidor de BD (newid()). La diferencia de hacer o no este último paso, es que en caso de realizar algún tipo de inserción en la tabla sin indicar de manera explícita un valor para nuestro ID, Sql Server genera uno por defecto.

00000000-0000-0000-0000-000000000000


La forma de uso sería la misma que la de la segunda inserción del ejemplo anterior, facilitando explícitamente el valor del ID. 


var cv2 = new CustomersV2 { ID  = Guid.NewGuid(), CompantyName = "Company 2", City = "Rome"  , EntryDate = DateTimeTo day };  


Con el fin de hacer más sencillo el uso, y minimizar el código, podríamos hacer una clase parcial de la entidad y en ella hacer un nuevo constructor sin parámetros:  


public partial class CustomersV2
{
    public CustomersV2()
    {
        this.ID = Guid.NewGuid();
    }

    public CUSTOMERSV2(string companyName, string city, DateTime entryDate) : this() 
    {
        this.CompanyName = companyName; this.City = city; this.EntryDate = entryDate;
    }

}




Oracle

Propiedades de las PKs para ambos casos y para ambos tipos: 






























Scripts


CREATE TABLE SYSTEM.CUSTOMERSV1
(
  ID           NUMBER                           NOT NULL,
  COMPANYNAME  NVARCHAR2(50)                    NOT NULL,
  CITY         NVARCHAR2(50)                    NOT NULL,   ENTRYDATE    DATE                             NOT NULL
)
TABLESPACE SYSTEM PCTUSED    40
PCTFREE    10
INITRANS   1
MAXTRANS   255
STORAGE    (
            INITIAL          64K
            NEXT             1M
            MINEXTENTS       1
            MAXEXTENTS       UNLIMITED
            PCTINCREASE      0
            FREELISTS        1
            FREELIST GROUPS  1
            BUFFER_POOL      DEFAULT
           )
LOGGING 
NOCOMPRESS 
NOCACHE
NOPARALLEL
MONITORING;


CREATE UNIQUE INDEX SYSTEM.CUSTOMERSV1_PK ON SYSTEM.CUSTOMERSV1
(ID)
LOGGING
TABLESPACE SYSTEM
PCTFREE    10
INITRANS   2
MAXTRANS   255
STORAGE    (
            INITIAL          64K
            NEXT             1M
            MINEXTENTS       1
            MAXEXTENTS       UNLIMITED
            PCTINCREASE      0
            FREELISTS        1
            FREELIST GROUPS  1
            BUFFER_POOL      DEFAULT
           )
NOPARALLEL;


ALTER TABLE SYSTEM.CUSTOMERSV1 ADD (
  CONSTRAINT CUSTOMERSV1_PK
 PRIMARY KEY
 (ID)
    USING INDEX 
    TABLESPACE SYSTEM
    PCTFREE    10
    INITRANS   2
    MAXTRANS   255
    STORAGE    (
                INITIAL          64K
                NEXT             1M
                MINEXTENTS       1
                MAXEXTENTS       UNLIMITED
                PCTINCREASE      0
                FREELISTS        1
                FREELIST GROUPS  1
               ));


CREATE TABLE SYSTEM.CUSTOMERSV2
(
  ID           RAW(16)                          DEFAULT sys_guid()            NOT NULL,
  COMPANYNAME  NVARCHAR2(50)                    NOT NULL,
  CITY         NVARCHAR2(50)                    NOT NULL,   ENTRYDATE    DATE                             NOT NULL )
TABLESPACE SYSTEM PCTUSED    40
PCTFREE    10
INITRANS   1
MAXTRANS   255
STORAGE    (
            INITIAL          64K
            NEXT             1M             MINEXTENTS       1             MAXEXTENTS       UNLIMITED
            PCTINCREASE      0
            FREELISTS        1
            FREELIST GROUPS  1
            BUFFER_POOL      DEFAULT
           )
LOGGING 
NOCOMPRESS 
NOCACHE
NOPARALLEL
MONITORING;


CREATE UNIQUE INDEX SYSTEM.CUSTOMERSV2_PK ON SYSTEM.CUSTOMERSV2
(ID)
LOGGING
TABLESPACE SYSTEM
PCTFREE    10
INITRANS   2
MAXTRANS   255
STORAGE    (
            INITIAL          64K
            NEXT             1M
            MINEXTENTS       1
            MAXEXTENTS       UNLIMITED
            PCTINCREASE      0
            FREELISTS        1
            FREELIST GROUPS  1
            BUFFER_POOL      DEFAULT
           )
NOPARALLEL;


ALTER TABLE SYSTEM.CUSTOMERSV2 ADD (
  CONSTRAINT CUSTOMERSV2_PK
 PRIMARY KEY
 (ID)
    USING INDEX 
    TABLESPACE SYSTEM
    PCTFREE    10
    INITRANS   2
    MAXTRANS   255
    STORAGE    (
                INITIAL          64K
                NEXT             1M
                MINEXTENTS       1
                MAXEXTENTS       UNLIMITED
                PCTINCREASE      0
                FREELISTS        1
                FREELIST GROUPS  1
               ));

CREATE SEQUENCE SYSTEM.SEQCUSTOMERSV1
  START WITH 2
  MAXVALUE 9999999999999999999999999999
  MINVALUE 1
  NOCYCLE
  NOCACHE

  NOORDER;



Sequence (Secuencia)

ODP.NET y Entity Framework para Oracle, no contiene ningún tipo de soporte para secuencias, por lo que nos lo tenemos que trabajar nosotros. Si tenemos libre acceso a la BD  y queremos dejar que el trabajo se realice desde el servidor, la manera más sencilla de proceder es mediante un trigger (before insert):



CREATE OR REPLACE TRIGGER CUSTOMERSV1_BEF_INS
   BEFORE INSERT
   ON CUSTOMERSV1
   FOR EACH ROW
BEGIN
   SELECT SEQCUSTOMERSV1.NEXTVAL
     INTO :NEW.ID
     FROM DUAL;
END;




De esta manera procederemos igual que lo hacíamos con los Identity en Sql Server, simplemente nos olvidaremos de facilitar el dato correspondiente a la columna ID y él hará el resto: 


using (var context = new OracleEntities()) 
{ 
    CUSTOMERSV1 cv1 = new CUSTOMERSV1 { COMPANYNAME = "COMPANY 1", CITY = "BARCELONA", ENTRYDATE = DateTime.Now };

    context.CUSTOMERSV1.Add(cv1); 
  
    int changes = context.SaveChanges();
}  


Resultado:





Si tuviéramos problemas con el uso de triggers, ya que no están permitidos por política de empresa, o simplemente no son de nuestro agrado, o al igual que sucedía con los Identity, necesitáramos conocer el valor del Campo ID antes de llamar al método SaveChanges(), algo que puede suceder cuando pasa un tiempo prudencial desde la creación del objeto hasta la grabación de los datos (esto solo lo recomendamos hacer solo en casos indispensables) podemos optar por la siguiente solución: 


Lo primero será deshabilitar/eliminar el trigger que hemos creado en el paso anterior.


Crearemos un método que se encargue de consultar el siguiente valor de la secuencia dentro de la clase parcial de nuestro contexto: 


public partial class OracleEntities
{
    public decimal SequenceNextVal(string sequenceName)
    {
        string sql = string.Format("SELECT {0}.NEXTVAL FROM DUAL", sequenceName);

        decimal result = 0;
        try
        {
            result = this.Database.SqlQuery<decimal>(sql).First();
        }
        catch (Exception ex)
        {
            throw new Exception("Problems with sequence.nexval, view InnerException", ex);
        }
        return result;
    }

}


Como podemos apreciar, este método de nuestra clase parcial del contexto, simplemente consulta el siguiente valor para una secuencia facilitada por parámetros.


También será necesario añadir una clase parcial para nuestra entidad y generar en ella 2 nuevos constructores que hagan transparente todo el trabajo de asignación del nuevo ID:

public partial class CUSTOMERSV1
{
    public CUSTOMERSV1()
    {
        using (OracleEntities context = new OracleEntities())
        {
            this.ID = context.SequenceNextVal("SEQCUSTOMERSV1");
        }
    }
    public CUSTOMERSV1(string companyName, string city, DateTime entryDate) : this()
    {
        this.COMPANYNAME = companyName; this.CITY = city; this.ENTRYDATE = entryDate;
    }
}


Probamos con la nueva fórmula: 


using (var context = new OracleEntities())
{
    CUSTOMERSV1 cv1 = new CUSTOMERSV1(companyName: "COMPANY 2", city: "SEVILLA", entryDate: DateTime.Now);

    context.CUSTOMERSV1.Add(cv1);

    int changes = context.SaveChanges();
}


Con el mismo resultado que cuando utilizábamos el trigger: 





Nota
Un punto a destacar, es que cuando utilizamos este tipo de generación de secuencia, tenemos que recalcar que cada vez que generamos un nuevo objeto (new), estamos aumentando la secuencia, por lo que si nunca se realiza el SaveChanges(), estos nuevos IDs generados nunca se podrán volver a reutilizar.

Importante
Tanto con la solución del trigger como con la solución de las clases parciales, cada vez que queramos conocer el valor de nuestro nuevo ID por medio de la secuencia, tenemos que realizar un viaje al servidor de Base de datos. Esto lo logramos de las formas que hemos visto, insertando los datos y generando el ID automáticamente mediante el PL-SQL del trigger,  y realizando una consulta anterior para conocer el siguiente valor de nuestra secuencia.





Guid


Para los campos Guid, al igual que en Sql Server tenemos las mismas 2 opciones de generación, tanto en cliente como en servidor, pero con diferencias.


Si nos fijamos en la generación del tipo de ID Guid en Oracle, este tiene el valor de una función (sys_guid()), como valor por defecto: 






Este valor por defecto, solo es válido cuando lanzamos directamente nuestras sentencias Insert, si lo hacemos desde Entity Framework, este comportamiento no se produce, ni aunque configuremos la propiedad StoreGeneratedPattern a Identity, ya que se produce un error de conversión de tipo de datos en tiempo de ejecución. 



BASE DE DATOS

Como hemos comentado anteriormente, la imposibilidad de ejecutar ‘sys_guid()’, dentro de los valores por defecto de la tabla en Entity Framework, nos obliga a realizar esta acción de manera muy similar a como lo hacíamos con las secuencias, con un trigger: 


CREATE OR REPLACE TRIGGER CUSTOMERSV2_BEF_INS_GUID
   BEFORE INSERT
   ON CUSTOMERSV2
   FOR EACH ROW
BEGIN
   SELECT sys_guid()
     INTO :NEW.ID
     FROM DUAL;

END;


El ejemplo de ejecución en Entity Framework, es exactamente el mismo que con Sequences: 







CLIENTE
En cliente tenemos prácticamente las mismas opciones que teníamos en Sql Server, y son las siguientes:


Generar el ID Guid en la misma sentencia de construcción del objeto mediante los constructores automáticos: 


public partial class CUSTOMERSV2
{
    public CUSTOMERSV2()
    {
        this.ID = Guid.NewGuid();
    }

    public CUSTOMERSV2(string companyName, string city, DateTime entryDate) : this()
    {
        this.COMPANYNAME = companyName; this.CITY = city; this.ENTRYDATE = entryDate;
    }
}


Lo primero será deshabilitar/eliminar el trigger que hemos creado en el paso anterior.


Realizamos la llamada: 


using (var context = new OracleEntities())
{
    CUSTOMERSV2 cv2 = new CUSTOMERSV2 { COMPANYNAME = "COMPANY 2", CITY = "MY TOWN", ENTRYDATE = DateTime.Now };

    context.CUSTOMERSV2.Add(cv2);

    int changes = context.SaveChanges();
}


Como podemos observar no hemos incluido la llamada al nuevo Guid, ya que este lo está haciendo nuestro constructor desde la nueva clase parcial. Resultado:

 






No hay comentarios :

Publicar un comentario