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
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.
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:
*** 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:
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.
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:
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:
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:
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:
Probamos con la nueva fórmula:
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:
Lo primero será deshabilitar/eliminar el trigger que hemos creado en el
paso anterior.
Realizamos la llamada:
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