Grimpi IT Blog

Marzo 26, 2009

Nueva versión de Open DBDiff

Archivado en: Open DbDiff, SQL Server — Etiquetas:, — grimpi @ 1:41 am

Bueno gente, hay una nueva versión de Open DBDiff en la calle (o mejor dicho, en CodePlex).

Que es Open DBDiff?
Es un pequeño programa de sincronización de schemas de bases de datos que funciona en SQL Server 2005/2008.
Hay casi diria decenas de aplicaciones similares en el mercado. La gran mayoría, exceptuando Red Gate y un par más, son bastante limitadas y funcionan mal. No capturan todos los objetos de la base y son incapaces de generar un script que funcione cuando se presentan casos minimamente complejos.

Por otro lado, no existe ningún producto similar, que sea open source.
La idea de Open DbDiff es precisamente llenar este hueco. Hacer una herramienta de comparacion de schemas potente, facil de usar y 100% libre.

A futuro, planeo hacer lo mismo para MySql y a un futuro muy a largo plazo, Oracle.
Es capaz de generar un correcto script de sincronización en situaciones bastante extrañas. Despues de mas de un año de desarrollo, creo que la aplicacion evolucionó lo suficiente para ser probada seriamente en producción (aunque soy conciente de que todavia quedan algunos bugs por resolver).

Este es link a la home en CodePlex para quien le interese probarlo.

Marzo 21, 2009

Diferencias entre TRUNCATE TABLE y DELETE FROM

Archivado en: SQL Server, T-SQL — Etiquetas:, — grimpi @ 1:01 pm

Una duda bastante habitual entre los desarrolladores, es cuál es la diferencia entre un TRUNCATE TABLE y un DELETE FROM TABLE. Este post (y el blog en general) está centrado en SQL Server, sin embargo, la mayoría de las diferencias entre ambas sentencias aplican a cualquier motor de bases de datos (Oracle, MySQL, DB2, etc).
Primero voy a enumerar las diferencias y luego voy a explicar el porqué de dichas diferencias.

TRUNCATE TABLE

DELETE FROM

  • Es una operación DDL.
  • Es una operación DML.
  • No permite el borrado selectivo. TRUNCATE TABLE elimina todo el contenido de la tabla.
  • Permite el borrado selectivo, mediante la clausula WHERE.
  • No se puede ejecutar, si la tabla tiene asociadas, aun si no existiesen registros en la tabla que contiene la FK.
  • Se puede ejecutar si hay FK asociadas a la tabla, pero siempre y cuando no tenga registros asociados o la FK este deshabilitada.
  • Es la forma más rápida de eliminar el contenido de una tabla.
  • Es más lenta.
  • No se activa ningún trigger al ejecutarse (a partir de SQL Server 2005, es posible capturar el evento mediante un DDL trigger, pero a modo de auditoría, no es posible tener acceso a los valores que fueron eliminados).
  • Puede activarse el trigger de ON DELETE y poder determinar que registros están siendo eliminados. siendo eliminados.
  • En caso que la tabla tuviese un campo Identity, se resetea el valor a 1 (o al valor base determinado en el campo).
  • No resetea el valor del campo Identity, en caso que la tabla tuviese uno.
  • TRUNCATE TABLE desasocia (deallocate) las páginas de datos de la tabla.
  • DELETE FROM marca cada registro afectado, como eliminado.
  • El logueo en el transaction log es mínimo. Solo registra el dealloc de las páginas de datos.
  • Loguea cada operación sobre los registros afectados.
  • No puede ser ejecutado si la tabla tiene asociadas vistas indexadas.
  • Se puede ejecutar si la tabla tiene vistas indexadas.

Como podemos ver, TRUNCATE TABLE es bastante más restrictivo que DELETE, al punto que en muchas situaciones, aun si queremos eliminar todo el contenido de la tabla, no podemos hacerlo.

Ahora bien, que es lo que hace TRUNCATE y porque es mas rápido que su “competidor”?
Para eso primero voy a hacer una brevísima introducción a como almacena internamente SQL Server los datos.
En SQL Server, los registros de una tabla, son agrupados en una estructura física de datos, que se llama página. Cada página tiene un tamaño fijo de 8060 bytes, y puede almacenar uno o cientos de registros, dependiendo del tamaño del mismo. Cuando se intenta insertar más registros en una tabla y la página de datos está llena, se crea otra donde se inserta el nuevo registro y así sucesivamente.
El comando TRUNCATE, lo que hace es desasociar (deallocate) las páginas de datos de la tabla, sin alterar los registros en sí mismo, mientras que el DELETE FROM recorre cada uno de los registros y los marca como borrados, por lo tanto, hace muchas más operaciones de I/O, que aumenta exponencialmente en relación con TRUNCATE, a medida que aumenta el tamaño de la tabla.
Otra razón que explica la diferencia de performance, es que TRUNCATE TABLE solo loguea en el transaction log, el deallocate de las paginas con la tabla (por lo tanto, es posible hacer un rollback de un TRUNCATE, cosa que muchos piensan que no), mientras que el DELETE FROM manda al transaction log todos los registros afectados, lo que es obviamente mucho más costoso a nivel recursos de I/O.
Por último, al hacer un TRUNCATE un lockeo sobre la tabla, a diferencia del DELETE FROM que hace un lockeo por pagina o registro, el consumo de memoria para almacenar los objetos lockeados es mucho menor.

Marzo 11, 2009

Como almacenar passwords en una base de datos?

Archivado en: Arquitectura, Seguridad — Etiquetas: — grimpi @ 9:53 pm

Supongamos que tenemos que hacer el típico sistema donde el usuario se autentica contra una tabla de nuestra base de datos.
Entre todas las cuestiones de seguridad que hay que tener en cuenta, surge un problema muy común: De que manera almacenar el password de los usuarios en nuestra base de datos?

Existen 4 maneras clásicas de hacer esto:

  • Guardarlo así como viene, en un campo sin encriptar, como si fuera un campo de texto normal.
  • Guardarlo encriptado.
  • Aplicarle una función hash y guardar el hash del password.
  • Aplicarle una función hash + un salt y guardar el hash del password.

Password plano:

La primer opción, por obvias razones, es la más simple de todas, pero extremadamente insegura y salvo en sistemas donde la seguridad NO IMPORTE EN ABSOLUTO, no debe ser utilizada. Sin embargo no es infrecuente encontrar sistemas donde nadie se haya tomado la molestia al menos, de dificultar mínimamente el acceso al password.
Un argumento que se puede dar a favor de este enfoque, es que si el acceso a la base de datos y la tabla de usuarios, está correctamente configurado y restringido, no debería haber problemas de seguridad. Lo que es una mentira atroz, porque tanto el DBA como los desarrolladores, tendría potencialmente acceso a estos datos.
Y si un hacker termina rompiendo de alguna manera la seguridad de nuestra base de datos, podría llegar a leer sin ningún problema, el password de todos los usuarios.

Password encriptado reversible:

La segunda opción, consiste en encriptar el password. Es mejor que nada, pero tiene sus problemas. El primer inconveniente que surge con este enfoque, es donde guardar la llave de encriptación. En un archivo de configuración? De qué sirve usar el algoritmo de encriptación más complejo y seguro que exista, si luego guardamos la llave de encriptación, en un archivo plano o de fácil acceso?
Y el segundo problema de este enfoque, es que el password es reversible. Que significa esto? Que es posible obtener el password sin apelar a la fuerza bruta. Por lo tanto, una potencial debilidad.
Si se pueden desencriptar, el riesgo de que alguien conozca la llave y tenga acceso a todo el sistema con todos los usuarios es muy grande. En un organización para el programador/DBA resulta extremadamente fácil obtener el password de los usuarios del sistema que mantiene/desarrolla, ya que tiene acceso a las tablas y a la clave de encriptación.

Password encriptado irreversible:

Perfecto, ya vimos que las primeras 2 opciones, son bastante débiles. Qué hacemos?
La tercera manera de almacenar el password, es usar una función hash. Como es esto?
Una función hash, es una función que encripta un determinado texto de forma no reversible, esto significa que no existe forma de “desencriptar” dicho texto. Lo que es perfecto para almacenar password. En la tabla ahora tenemos que guardar el hash del password. Es un método mucho más seguro que los otros 2 anteriores. Al no poder ser “desencriptado” el password, no existe forma de obtenerlo.
Entonces, si no puedo desencriptar una contraseña, ¿Cómo puedo saber entonces si una contraseña entregada es válida? La respuesta es muy simple. Se encripta la contraseña entregada y se compara el resultado de la encriptación con la contraseña previamente almacenada.

CLAVE_HASH = FUNCION_HASH(Password)

Existen principalmente 2 algoritmos para hacer Hash: MD5 y SHA-1 (también existen variantes de este como el SHA-256 y SHA-512).
Sin embargo, existe una debilidad, veamos el siguiente ejemplo de una tabla Usuario:

ID Usuario Password
1 Pepe ADC2-1234….
2 Rosario ADC2-1234….
3 Juan FA3D-BC56….
4 Grimpi FFD3-D045….

Si somos observadores, el hash del password de los usuarios Pepe y Rosario son el mismo. No podemos saber cuál es el password exacto de ambos, pero si podemos saber que son iguales. Supongamos que nuestro sistema tiene 50000 usuarios. Cuál es la posibilidad de que existan passwords repetidos? Muy grande. Si un Hacker por alguna razón, obtiene el password de Pepe, instantáneamente, ya sabría cual es el password del usuario Rosario. Mala idea.
Por otro lado, este enfoque también tiene otro grave inconveniente, que son los diccionarios de claves hash (Rainbow tables). Un diccionario de estas características, no es más que una enorme tabla de 2 campos, donde en el primer campo figura la clave hash y en el segundo campo, el texto plano correspondiente a esa clave.
Los usuarios no suelen poner claves muy complejas a excepción que uno se lo exija, por lo tanto, un diccionario de claves hash muy amplio, seguramente contendrá la mayoría de las password de los usuarios.

Entonces, que hacemos?

Password encriptado irreversible + Salt:

Que es un salt?
Un salt no es ni mas ni menos que texto o un conjunto de caracteres que varían por usuario/registro que se le concadena al texto antes de ser encriptado por una función hash. Y para qué queremos hacer esto? Para evitar los problemas del enfoque anterior. Si a un password se le concadena un texto que varia por usuario, como por ejemplo, el ID del mismo o el login, el hash sería diferente aun si tuviésemos 2 o más usuarios con exactamente el mismo password.
Pero además, dificultamos enormemente el ataque del hacker usando un diccionario de claves. Por supuesto, que a medida que el Salt sea más complejo, más dificultosa será la tarea del hacker.

CLAVE_HASH = FUNCION_HASH(Password + Salt)

Existen básicamente 2 estrategias diferentes para determinar el salt de un password. Recordemos que a la hora de autenticar el password, debemos saber el valor del salt para generar una clave encriptada idéntica a la que tenemos en la base de datos.

La primera estrategia consiste en generar un texto aleatorio (convenientemente con caracteres “raros” para dificultar todavía más la tarea de un ataque por diccionario) y asociárselo al usuario, por lo tanto, sería necesario agregar un nuevo campo a la tabla de Usuarios, que tenga el valor del Salt:

ID Usuario Salt Password
1 Pepe AB_D!#$2 BFC3-8234….
2 Rosario T5&/021? AAC2-1290….
3 Juan ¿+}s34@” EC3D-4C56….
4 Grimpi D*-34r$Q AFC4-A145….

La otra estrategia consiste calcular el salt en función de algún dato del usuario, como por ejemplo el username (que se supone que debe ser único), el ID de la tabla o algún otro campo del mismo. La ventaja de esta estrategia es que no tenemos que agregar un nuevo campo a la tabla, pero debemos asegurarnos que usamos un valor univoco en toda la tabla!!.

Recomiendo ver este link con una interesante discusión que hubo en un foro, con respecto a las distintas maneras en que se puede obtener el valor salt y este paper que explica mejor el metodo recomendado.

Febrero 24, 2009

Lo nuevo de SQL Server 2008: MERGE

Archivado en: SQL Server, SQL Server 2008, T-SQL — Etiquetas: — grimpi @ 11:22 pm

Otra de las características interesantes que Microsoft incorporó en SQL Server 2008 y que al igual que la sentencia GROUPING SETS, ya existía en otros motores de bases de datos, es la clausula MERGE (de la cual habíamos hablado brevemente antes).
Esta cláusula nos va a permitir definir lógica de combinación para operaciones atómicas de inserción, borradas y actualización de datos.
La idea es comparar 2 conjuntos de datos y detectar las diferencias de datos entre las 2 tablas y en función de eso, ejecutar alguna operación de actualización sobre la tabla.
La clausula MERGE nos sirve básicamente para 2 cosas:

1) Sincronizar los datos de 2 tablas. Supongamos que tenemos 2 bases distintas (Producción y Desarrollo por ejemplo) y queremos sincronizar los datos de una tabla para que queden exactamente iguales. Lo que antes hubiese implicado
algunas sentencias mezcladas con INNER JOIN y NOT EXISTS, ahora es posible resumirlo en una operación atómica mucho
más sencilla y eficiente.

2) La otra razón por la cual podríamos usar MERGE, es cuando tenemos nuevos datos que queremos almacenar en una tabla y no sabemos si la primary key de la tabla ya existe o no, por lo tanto, no sabemos si hacer un UPDATE o un INSERT en la tabla. El famoso IF EXISTS… (tema que ya habíamos visto acá y que es un poquito más complicado de lo que parece a simple vista).

Veamos un ejemplo:

MERGE dbo.Tabla1 AS Target
USING (SELECT ID,Campo1,Campo2,Campo3 FROM dbo.Tabla2) AS Source
ON (Target.ID = Source.ID)
WHEN MATCHED THEN

UPDATE
SET Target.Campo1 = Source.Campo1, Target.Campo2 = Source.Campo2

WHEN NOT MATCHED BY TARGET THEN

INSERT (ID,Campo1,Campo2,Campo3)
VALUES (Source.ID,Source.Campo1,Source.Campo2, Source.Campo3)

WHEN NOT MATCHED BY SOURCE THEN

DELETE;

Realmente creo que es una de las cosas más interesantes que incorporó la nueva versión de SQL Server y es increíble que algo tan útil y tan práctico como esto, no hubiese estado antes.
Es importante entender que la ventaja del MERGE no es una simple cuestión sintáctica que nos permite escribir un par de líneas menos de código. La ventaja real y más importante está en que permite hacer sincronizar 1 tabla en función de una consulta, de manera atómica. Esto significa que si una operación de delete/insert/update falla, se hace un rollback de todo el conjunto de datos y más importante aún, se garantiza en escenarios de alta concurrencia, donde puede haber otros procesos escribiendo en la misma tabla en el mismo instante, no existan incoherencias.

Capturar salida:
Algo fantástico que existe a partir de SQL Server 2005, es la clausula OUTPUT que permite capturar todo lo que sucedió dentro de una operación INSERT/DELETE/UPDATE. En SQL Server 2008 el uso conjunto de MERGE + OUTPUT nos sirve saber que registros fueron modificados y que acción se hizo sobre ese registro (INSERT, UPDATE o DELETE).
La nueva función $action indica que operación se realizó, mientras que los atributos deleted e inserted guardan la información sobre el registro afectado (de la misma manera que funcionan con los triggers).

Ejemplo:

MERGE dbo.Tabla1 AS Target
USING (SELECT ID,Campo1,Campo2,Campo3 FROM dbo.Tabla2) AS Source
ON (Target.ID = Source.ID)
WHEN MATCHED THEN

UPDATE SET Target.Campo1 = Source.Campo1, Target.Campo2 = Source.Campo2

WHEN NOT MATCHED BY TARGET THEN

INSERT (ID,Campo1,Campo2,Campo3)
VALUES (Source.ID,Source.Campo1,Source.Campo2, Source.Campo3)

WHEN NOT MATCHED BY SOURCE THEN

DELETE

OUTPUT $action, deleted.*, inserted.*;

Resultado:

Avanzando en las profundidades del MERGE:
Si vemos el plan de ejecución, podemos encontrar cosas muy interesantes. En primer lugar, aparece un nuevo operador lógico en el plan, que se llama “Clustered Index Merge” y es nuevo de SQL Server 2008. Este operador en función de un conjunto de datos, realiza un delete, insert o update a una tabla usando su respectivo índice clustered. Si la tabla no tuviese un índice clustered, entonces se usaría el también nuevo operador “Table Merge”. Esta es una de las razones por la cual MERGE no es una simple encapsulación que simplifica cosas que ya podíamos hacer antes, sino que al ser una operación interna del motor de SQL Server, es más eficiente que cualquier otra alternativa existente en las versiones previas de SQL Server.
Otro punto interesante es que podemos ver es que cuando existen índices y las 2 tablas tienen similares volúmenes de datos, el operador MERGE JOIN resulta el método más eficiente en estas operaciones, ya ambas tablas son escaneadas una sola vez y no hay necesidad de ordenar los datos. Es posible cambiar estableciendo otro join hint, pero en la mayoría de los casos, el MERGE JOIN es lo más óptimo.


Algunas recomendaciones para obtener mejor performance con esta sentencia:

  • Crear un índice clustered sobre las columnas relacionadas en el JOIN, en la tabla destino.
  • Crear un índice único sobre las columnas relacionadas en el JOIN, en la tabla de origen (en caso que hubiese).

Esto garantiza al motor que no tiene que ejecutar ninguna validación adicional ni efectuar ningún sort extra.

Links:
http://technet.microsoft.com/en-us/library/cc879317.aspx
http://technet.microsoft.com/en-us/library/bb522522.aspx

Febrero 12, 2009

Identity en SQL Server

Archivado en: Identitys, SQL Server — Etiquetas:, — grimpi @ 11:25 pm

Quienes trabajaron mínimamente en alguna versión de SQL Server, conocerán los campos Identitys, que son columnas cuyo valor es autoincremental. Hay muchas discusiones sobre las ventajas y desventajas del uso de este tipo de campos, para las columnas que componen la clave primaria (PK), pero es algo que ya voy a tratar en otro artículo.
Por el momento muestro un listado de tips que cualquier programador que trabaje con SQL Server debería saber al respecto de los campos Identitys:

1) Resetear el valor del campo identity:

Mucha gente piensa que no es posible resetear el contador de un identity de una tabla. FALSO. Existen dos formas de hacerlo, una es mediante TRUNCATE TABLE. Cuando hacemos un TRUNCATE de una tabla, además de borrar todo su contenido, reseteamos el valor del campo Identity. Esto es muy obvio, porque un TRUNCATE TABLE no hace nada más y nada menos que borrar la tabla y volver a crearla.
La segunda opción (y la que nos interesa) es usar el comando DBCC CHECKIDENT. Esta instrucción permite setera el valor que queremos del campo Identity.
Su sintaxis es la siguiente:

DBCC CHECKIDENT <NombreDeTabla>,RESEED,NuevoValor

Hay que tener mucho cuidado con el uso de este comando, ya que podemos setear un valor menor al máximo de la tabla actual y crear en algún momento un valor duplicado. Una opción interesante de este comando es asignar al campo el valor máximo de la tabla + 1, y así garantizar que no tendremos problemas de registros duplicados, para eso solo hay que poner:

DBCC CHECKIDENT <NombreDeTabla>

2) Insertar valores explícitos en un campo Identity:

Cuando se inserta un registro en una tabla con un campo Identity, este se incrementa automáticamente y si queremos modificar o insertar este valor manualmente, el SQL nos tira error y nos indica que no es posible.
Sin embargo, algunas veces puede que necesitemos deshabilitar temporalmente la propiedad Identity, y poder ingresar un valor explícito a la tabla. Generalmente queremos hacer algo así cuando hacemos copias o replicaciones de una tabla de una base a otra tabla.
Para esto, tenemos la opción:


SET IDENTITY_INSERT <NombreDeTabla> ON

Con esta opción, podemos insertar valores en un campo Identity como si fuese un campo más. Luego, cuando terminamos de insertar valores, hay que volver habilitar nuevamente la propiedad Identity del campo. Para eso, corremos el mismo comando anterior, pero con el parámetro OFF.

SET IDENTITY_INSERT <NombreDeTabla> OFF

3) Capturar el valor en un INSERT:

Muchas veces cuando tenemos una tabla con un campo Identity, al hacer un INSERT, queremos saber cuál es el valor que insertó en este campo.
Lo mas común en estos casos es usar @@Identity, que es una variable global que indica el valor del último campo identity insertado en cualquier tabla de la base. Esto último es un detalle muy importante. Si la tabla contiene un trigger que inserta registros en otra tabla, o antes de leer la variable @@Identity, se inserta otro registro en cualquier tabla de la base, el valor @@Identity, tendrá un valor diferente al que estábamos esperando.
Para resolver esta situación, tenemos dos funciones muy útiles: SCOPE_IDENTITY e IDENT_CURRENT. La función SCOPE_IDENTIY nos devuelve el último valor generado dentro de un scope, o sea dentro de un entorno, ya sea un Store Procedure, Function o Trigger. En nuestro caso anterior, como el trigger esta fuera del scope, la función SCOPE_IDENTIY nos devolvería el valor que queremos.
Pero por otro lado, tenemos la función, IDENT_CURRENT, donde se le pasa por parámetro el nombre de la tabla y nos devuelve el ultimo valor identity generado para esta tabla, sin importar el scope. Esta opción en mi opinión es la más clara. Su sintaxis es la siguiente:

SELECT IDENT_CURRENT(‘NombreTabla’)

4) Capturar los valores Identity en un INSERT con multiples registros:

Supongamos que insertamos en una sola sentencia INSERT, mas de un registro en una tabla. Como hacemos para capturar el valor de la columna identity de todos los nuevos registros insertados?. Ni la funcion IDENT_CURRENT ni SCOPE_IDENTITY nos sirven, ya que estas solo devuelven el ultimo valor insertado, pero a nosotros nos interesan todos los valores insertados. Veamos este ejemplo:

CREATE TABLE Cliente (ClienteID INT IDENTITY, Nombre VARCHAR(50))

INSERT INTO Cliente
SELECT ‘Esteban’
UNION
SELECT ‘Juan’
UNION
SELECT ‘Ricardo’

SELECT IDENT_CURRENT(‘Cliente’)

Como ya dijimos antes, IDENT_CURRENT solo nos va a devolver el valor del identity del registro ‘Ricardo’, que fue el último registro que se insertó, por lo tanto, el ejemplo anterior no nos sirve.
Pero a partir de SQL Server 2005, existe la interesante clausula OUTPUT, que permite capturar el valor de las operaciones INSERT y DELETE.
Veamos entonces este ejemplo, que usa OUTPUT y almacena los valores de los registros insertados en una variable de tabla.

DECLARE @NuevosIdentitys TABLE (ID INT)

INSERT INTO Cliente
OUTPUT INSERTED.ClienteID INTO @NuevosIdentitys(ID)

SELECT ‘Esteban’
UNION
SELECT ‘Juan’
UNION
SELECT ‘Ricardo’

SELECT * FROM @NuevosIdentitys

5) Detectar si una columna es Identity:

Usando la función COLUMNPROPERTY, podemos obtener esta información.

SELECT COLUMNPROPERTY(OBJECT_ID(‘<NombreDeTabla>’),’<NombreColumna>‘,‘IsIdentity’)

Otra opción, más potente, consiste en usar la vista sys.identity_columns
(solo a partir de SQL Server 2005), que además de determinar si una columna es Identity, nos permite obtener mayor información sobre la columna relacionada con esta propiedad, como por ejemplo el valor inicial, el valor de incremento, si se aplica o no también para casos de replicación (IsNotForReplication), etc.

SELECT
Name,
is_identity,
seed_value,
increment_value,
is_not_for_replication,
last_value
FROM sys.identity_columns

6) Obtener el valor de la columna Identity aun si saber el nombre del campo:

Supongamos que queremos obtener el valor de una columna que sabemos que es identity, pero solo sabemos el nombre de la tabla (en situaciones donde ejecutamos SQL dinámico podría llegar a darse un caso así).
Existe en SQL Server una función que se llama IDENTITYCOL, que precisamente nos devuelve este valor que queremos averiguar.

SELECT IDENTITYCOL FROM <NombreDeTabla>

Febrero 6, 2009

Introducción a Test Driven Development (TDD)

Archivado en: Arquitectura, Patrones, Test Driven Development — Etiquetas: — grimpi @ 10:46 pm

Test Driven Development (TDD) es una metodología de programación que involucra 2 prácticas: Escribir primero las pruebas unitarias, o sea, escribir el test antes de escribir el programa y refactoring.
Yo creo que una de las cosas más difíciles al trabajar de esta manera, es cambiar la mentalidad con la que estamos acostumbrados a desarrollar.
TDD es un proceso interactivo donde se repiten una serie de pasos hasta que se está satisfecho con el código generado. A TDD no le importa demasiado como está estructurado el código ni la arquitectura del software. La manera en que TDD trabaja es mediante requerimientos o casos de usos, que son descompuestos en un conjunto de comportamientos más chicos necesarios para cumplir con el caso de uso dado. Luego por cada comportamiento del sistema, lo primero que se debe hacer es escribir un test unitario para dicha funcionalidad.

Hay muchos conceptos confusos alrededor de esta técnica que me gustaría aclarar:

  • Primero las pruebas unitarias, luego el código de los métodos (Test-First Development). Este concepto es una de las características fundamentales de TDD. Si no respetamos esto, no estamos haciendo TDD (lo que tampoco tiene porque estar mal, pero es necesario hacer la aclaración sobre lo que no es TDD).
  • TDD no es una técnica de testing. TDD apunta a desarrollar software de manera incremental de la forma más simple y robusta posible.
  • Mucha gente tiene cierto rechazo porque piensa que TDD consiste en escribir TODAS las pruebas unitarias antes que el código. Esto es falso y es importante remarcarlo. TDD consiste en hacer un desarrollo cíclico e incremental. Se debe generar inicialmente un conjunto muy limitado de test que corresponden a una clase o caso de uso, para luego ir desarrollando el código. Una vez que toda la funcionalidad correspondiente a las pruebas unitarias fueron desarrolladas, se pasa a otro modulo.
  • A pesar de lo que siempre se dice que en TDD lo primero que hay que hacer es escribir el test unitario antes del código, esto tiene un límite obvio y es que la estructura de la entidad que vamos a probar ya debe estar creada. Por ejemplo, si vamos a desarrollar una clase que administre clientes, conviene primero crear la clase entidad Cliente, con sus respectivas propiedades, luego las pruebas unitarias y por último los métodos de la clase Cliente (que pueden ser insertar, modificar, validar, etc.).

Ciclo del TDD:

1) Escribir un test e intentar compilar, aunque aun no sea posible porque no se desarrollo la funcionalidad correspondiente.

2) Escribir la mínima cantidad de código posible para que la prueba unitaria compile y ejecutarla. Deberá fallar, ya que no tiene la funcionalidad requerida.

3) Escribir el test de tal manera, que al ser ejecutado, este no falle. La idea es que la función escrita no sea lo más elegante y optimo posible, sino que simplemente, permita pasar el test unitario exitosamente.

4) Refactorizar el código de la función, para que sea lo más optimo posible. Es muy posible que existan varias instancias de refactorización del código de la función en distintas etapas del proyecto.

5) Escribir un nuevo test unitario para otra funcionalidad.


Ventajas:

Todo muy lindo lo explicado, pero que ventajas concretas ofrece trabajar con TDD? Porque conviene trabajar con esta metodología?

  • En primer lugar y la ventaja más obvia, es que al tener el test unitario escrito por cada método, podemos detectar regresiones y cambios en una función que rompan otra parte del código.
  • Mayor facilidad para refactorizar y mantener código.
  • Si bien es verdad que TDD significa en un principio escribir más código, la realidad indica que muchas veces ahorra código y tiempo, al disminuir la cantidad de defectos en los métodos.
  • Al desarrollar el método de acceso al método antes que su implementación, ayuda a pensar cómo utilizar el componente y para que debería existir, por lo tanto, nos ayuda al diseño del software.

Recomendaciones:

  • Hacer más de un test unitario por función que contemplen distintas situaciones. Se deben probar la mayor cantidad de casos posibles. Es MUY recomendable hacer un test unitario negativo, o sea, que si no devuelve una excepción o el resultado de llamar al método es distinto de false, es porque hay un error. Por ejemplo, si es una regla de negocio, hacer un test unitario para que contemple la validación exitosa y otro test unitario, que llame al método con parámetros incorrectos para probar que la validación la está haciendo de manera correcta.
  • No meter lógica en los test unitarios. Si queremos probar distintas situaciones, usemos un test unitario por cada prueba.
  • No hacer que un test unitario dependa de otro test. Deben ser lo más independientes posibles uno del otro.

TDD vs TAC:

Uno de los componentes principales de TDD es Test First Development, que como ya dijimos antes, consiste en escribir el test unitario antes que el código. Pero sin embargo, existe otro enfoque más tradicional, que es precisamente el inverso. Escribir el test unitario DESPUES de haber escrito el código y a esto se lo llama Test After Coding (TAC).

Escribir antes el test unitario nos garantiza que todos los métodos y funciones de nuestra aplicación, tengan su respectivo test unitario. En cambio, esto en la práctica, no está garantizado con TAC, ya que siempre se va a estar corriendo con los tiempos.

Por otro lado, TDD nos ayuda a pensar el diseño del software, ya que cuando escribimos el test unitario, debemos pensar en la interfaz del método y como va a interactuar. Si empiezo diseñando la prueba, se hace más claro que tiene que hacer exactamente el método, con lo cual, se podría reducir tiempos y sobre todo, posibilidad de errores.

Otro punto a favor de TDD y muy importante, la primera vez que se ejecuta el test unitario, este debe fallar. Si el test al ser ejecutado, pasa exitosamente, es porque está mal escrito. Sin embargo, si el test fue escrito posteriormente al desarrollo del método, existe la posibilidad de no poder detectar que el test unitario en si mismo este mal escrito, ya que si siempre se ejecuta exitosamente, uno tiende a pensar que el método esta correcto y dificulta la tarea de ver si el test unitario está correcto. Esto es fundamental.

Ejemplo de código:

Pongamos un poco de TDD en acción y veamos un ejemplo simple pero práctico de cómo usar esta metodología de trabajo.

Supongamos que nos piden desarrollar modulo que permita insertar, modificar, listar y eliminar clientes. A su vez también tenemos que programar un método de validación que cumpla con la siguiente regla de negocio del cliente: Debe tener menos de 50 años, el sexo solo puede ser F o M, y el nombre es un campo obligatorio.

Para simplificar, vamos a usar el patrón Active Record (del cual ya habíamos hablado antes), que nos permite en una misma clase poner la lógica de negocio y de acceso a datos.

public class Cliente
{

private string nombre;
private int edad;
private string sexo;

public string Sexo
{

get { return sexo; }
set { sexo = value; }

}

public int Edad
{

get { return edad; }

set { edad = value; }

}

public string Nombre
{

get { return nombre; }
set { nombre = value; }

}

}

Ahora debemos crear la clase ClienteTest y para reducir el ejemplo, vamos a crear solamente 2 pruebas unitarias (Validar e Insertar)

  • 1° Fase

[TestClass()]

public class ClienteTest

{

[TestMethod()]

public void InsertarTest()

{

Cliente nuevo = new Cliente();

nuevo.Edad = 35;

nuevo.Sexo = “F”;

nuevo.Nombre = “Alejandra”;

bool resultado = Cliente.Insert(nuevo);

Assert.AreEqual(resultado, true);

}

[TestMethod()]

public void ValidarTest()

{

Cliente nuevo = new Cliente();

nuevo.Edad = 50;

nuevo.Sexo = “F”;

nuevo.Nombre = “Alejandra”;

bool resultado = nuevo.Validate();

Assert.AreEqual(resultado, true);

}

}

Obviamente este codigo no va a compilar, ya que los metodos de la clase Cliente todavia no fueron siquiera declarados.

  • 2° Fase

Ahora debemos crear en la Clase Cliente, los 2 metodos que vamos a utilizar, pero con el UNICO objetivo que las pruebas unitarias puedan compilar y fallar al ser ejecutadas.


///
<summary>

/// Valida que el el cliente cumpla con las reglas de negocio.

///</summary>

public bool Validate()

{

return false;

}

///<summary>

/// Inserta un nuevo Cliente en la base de datos.

///</summary>

public static bool Insert(Cliente nuevo)

{

return false;

}

  • 3° Fase

Corremos todos los test unitarios desarrollados y estos deben fallar (Red Status)


  • 4° Fase

Desarrollamos los 2 métodos correspondientes a los test unitarios (para el segundo método, no lo desarrolle completo para no hacer largo el código y clarificar lo que se quiere hacer).


public bool Validate()

{

if (edad < 50) return false;

if (String.IsNullOrEmpty(sexo)) return false;

if (!sexo.Equals(“F”) && !sexo.Equals(“M”)) return false;

if (String.IsNullOrEmpty(nombre)) return false;

return true;

}

public static bool Insert(Cliente nuevo)

{

try

{

using (SqlConnection conn = new SqlConnection(“CONNECTION_STRING”))

{

using (SqlCommand command = new SqlCommand(“INSERT INTO CLIENTES …”))

{

command.ExecuteNonQuery();

}

}

return true;

}

catch

{

return false;

}

}

  • 5° Fase

Corremos todos los test unitarios desarrollados y ahora estos deben pasar exitosamente (Green Status)


Links:

Enero 23, 2009

Parameter Sniffing

Archivado en: Engine, SQL Server — Etiquetas:, , — grimpi @ 1:26 am

Parameter Sniffing es una técnica usada por los motores de base de datos (en este caso SQL Server) para optimizar el plan de ejecución de un store procedure dependiendo de los valores de los parámetros del mismo. En general, es una buena técnica que ayuda a mejorar la performance de la ejecución del store procedure, cuando en la mayoría de los escenarios donde se ejecuta, se trabaja con el mismo conjunto de datos.

Sin embargo, existen al menos 2 situaciones, donde se pueden presentar más inconvenientes que beneficios.

  • Parameter Sniffing es óptimo cuando en general, la mayoría de los valores de los parámetros tienen las mismas características (en realidad, el resultado de los valores de filtro en la tabla tienen la densidades similares). Si los valores que se le envían al Store Procedure, varían mucho en tamaño (cantidad de registros), el motor podría estar ejecutando un plan de ejecución que en la mayoría de los casos, no es el adecuado para el valor.
  • Otra razón en la cual puede haber problemas de performance, es cuando la distribución de datos en la tabla no es uniforme, por lo tanto, el plan de ejecución óptimo es muy sensible a ligeras variaciones en el filtrado de datos.


Muchas veces cuando pasa que una query se ejecuta rápido, pero la misma dentro de un store procedure no, es por problemas relacionados con Parameter Sniffing.


Veamos el siguiente ejemplo, para observar el problema que sucede en algunos casos relacionado con Parameters Sniffing.

1) Creo primero el siguiente Store Procedure:

CREATE PROCEDURE dbo.GetReservaCliente @Param2int
AS
BEGIN

SELECT * FROM Reserva R WHERE R.ClienteId = @Param2

END

Hay que recordar que el plan de ejecución de un Store Procedure no se genera cuando es creado el Store, sino cuando se ejecuta por primera vez. Esto significa que la segunda vez que se llama el procedimiento, va a usar el plan de ejecución que tiene cacheado.

2) Vemos la distribución de datos en la tabla Reserva por cada ClienteId:

SELECT ClienteId, Count(*) AS Cantidad FROM Reserva
GROUP BY ClienteId
ORDER BY Count(*)

El mínimo es el ClienteId 63, que tiene 1 solo registro.
El máximo es el ClienteId 1065, que tiene 28656 registros.

3) Buscamos ahora todos los registros de la tabla Reserva cuyo ClienteId sea 1065 usando el Store Procedure previamente creado.

EXEC GetReservaCliente 1065

Veamos el plan de ejecución que el SQL Server armó para la consulta:

2 cosas nos interesan acá: El plan de ejecución que armó y el “Estimated Number of Rows” (que está marcado en rojo). Como podemos ver, este valor es de 28656, exactamente la cantidad de registros existentes en la tabla para el cliente consultado. De donde sacó esta información el motor? En función de las estadísticas internas que almacena.
Ahora ejecutemos el mismo Store Procedure, pero buscando el ClienteId 63, que como vimos antes, tiene un solo registro.

EXEC GetReservaCliente 63

Veamos el plan de ejecución que el SQL Server armó para la consulta:

El valor de “Estimated Number of Rows” es de 28656, exactamente el mismo valor que la consulta anterior, a pesar de que la cantidad de registros devueltos es 1. Esto significa que el motor esta reutilizando el plan de ejecución anterior, que esta optimizado para devolver 28656 registros y por lo tanto no hay certeza que este plan sea igual de eficiente para el ClienteId 63.

4) Borramos y volvemos a crear el Store Procedure para limpiar el plan de ejecución y ver cuál es el eficiente para el ClienteId 63:

DROP PROCEDURE dbo.GetReservaCliente
GO
CREATE PROCEDURE dbo.GetReservaCliente

@Param2    int
AS

BEGIN

SELECT * FROM Reserva R WHERE R.ClienteId = @Param2
END

GO

Ahora que la cache del Store Procedure fue limpiada, ejecutamos nuevamente el procedimiento y vemos que plan de ejecución genera:

EXEC GetReservaCliente 63


Como podemos observar, el plan más eficiente generado para el ClienteId 63 (1 solo registro) es muy diferente al plan más eficiente para el ClienteId 1065 (28656 registros).

Soluciones cuando hay problemas con Parameter Sniffing:

  • Hacer que el motor regenere el plan de ejecución cada vez que se ejecuta el Store Procedure. Para eso debemos crearlo con la opción WITH RECOMPILE. El costo de hacer esto, es que el SQL Server pierde tiempo en recompilar en vez de usar la cache, pero si este costo es menor al de ejecutar un plan ineficiente, se debe hacer.

CREATE PROCEDURE dbo.GetReservaCliente
@Param2 int
WITH RECOMPILE
AS
BEGIN
SELECT * FROM Reserva R WHERE R.ClienteId = @Param2
END

  • Utilizar el hint RECOMPILE dentro de la query (Solo a partir de SQL Server 2005). Esta solución es similar a la anterior, con la ventaja de que en vez de recompilar todo el store, el SQL Server solo recompila una query especifica. Esto es útil cuando tenemos muchas querys dentro de un mismo Store Procedure y el problema de uso de un plan de ejecución ineficiente refiere a una sola query.

SELECT * FROM Reserva R WHERE R.ClienteId = @Param2 OPTION(RECOMPILE)

  • Deshabilitar Parameter Sniffing usando variables locales. Esto se debe a que SQL Server no tiene en cuenta los valores de las variables locales para hace sniffing, sin embargo, algunos suponen que un futuro esto podría llegar a cambiar, aunque por el momento funciona y es uno de los métodos mas utilizados.

    Ejemplo:

CREATE PROCEDURE dbo.GetReservaCliente
@Param2    int
AS
BEGIN

DECLARE @Param3 int

SET @Param3 = @Param2
SELECT * FROM Reserva R WHERE R.ClienteId = @Param3

END

De este modo, al no utilizar en las consultas del Store Procedure los parámetros propios, al generar el Plan de Ejecución no se utilizan los valores de los parámetros, por lo que en vez de generar el Plan de Ejecución para un caso particular, se generará el Plan de Ejecución genérico para el caso medio.

  • Utilizar el hint OPTIMIZE FOR (Solo a partir de SQL Server 2005). Este hint permite forzar a una consulta a que se ejecute con un plan de ejecución optimizado para un determinado valor.

    SELECT * FROM Reserva R WHERE R.ClienteId = @Param2
    OPTION (OPTIMIZE FOR (@Param2 = 1065))

Este método nos garantiza que para el valor que estamos teniendo problemas va a funcionar bien, aunque podría presentar problemas con otro valores.
Es cuestión de ir probando y ver si vale la pena usarlo. Si el valor típico del parámetro del Store Procedure es compatible con un óptimo plan de ejecución, es una opción interesante.

  • Existen opciones más avanzadas, para casos complejos como por ejemplo crear un propio plan de ejecución usando Plan Guides.


Diciembre 13, 2008

Un ejemplo de inseguridad…

Archivado en: Opinion, Seguridad — Etiquetas: — grimpi @ 1:09 pm

La semana pasada tuve que ir a instalar un sistema en una planta de una empresa de bebidas muy pero muy conocida.
Me sorprendió de sobremanera, la increíble inseguridad a nivel accesos a datos que existe en dicha planta.
Paso a enumerar los horrores bizarros que encontré:
1) El password sa del servidor SQL Server de PRODUCCION era vacio.
2) El password de administrador de todas las PC era el mismo, incluyendo el del Servidor SQL y estaba anotado en marcador rojo indeleble en unos de los monitores!!!.
3) Estaba habilitado el acceso por Terminal Server al servidor de producción (cuya password recordemos estaba anotada en el monitor). No vaya ser que un potencial hacker tenga que tomarse la molestia de tener que ir físicamente al rack de servidores…
4) El usuario administrador del repositorio de reportes (Crystal Reports Server) también era vacio.

Es realmente difícil encontrar un cuadro más patético e inseguro que este. El que instaló todo esto, debía ser un junior y muy vago, porque de otra manera no se explica como un profesional de sistemas sea tan pero tan irresponsable. Como también es terrible que no exista una auditoria, control interno o normas de seguridad mínimas que detecten este tipo de situaciones.
Un dato importante a tener en cuenta que el sistema que instalamos monitorea en tiempo real determinados aparatos de la fábrica, que luego son volcados a una base de datos y visto por distintos reportes. Tal vez no sean datos estratégicos para la empresa que requieran un nivel de acceso excesivamente restrictivos, pero definitivamente semejante nivel de inseguridad no es aceptable en NINGUN sistema. Creo que no tengo que aclarar los desastres que una persona con suficientes conocimientos puede hacer con un acceso tan amplio y fácil a los servidores.

Y no estamos hablando de una pyme familiar de 50 empleados, sino de una empresa multinacional que en la Argentina solamente su facturación debe ser de cientos de millones de dólares y seguramente más.
No es la primera vez que veo este tipo de situaciones, especialmente en las plantas de las empresas. Recuerdo que una vez también en una importante farmacéutica, descubrimos que el password administrador del dominio era de toda la red era “administrador”. En muchas plantas industriales, independientemente del rubro, el sector informático está totalmente ausente.
Porque estoy seguro que en su sector administrativo y contable, este tipo de situaciones no ocurren. Pero como en las plantas, no suele existir ningún tipo de empleado en sistemas, sino mas bien consultores que instalan sistemas y tratan de hacer las cosas lo mas fácil y rápido posible, nadie controla el aspecto de la seguridad.
Me pregunto en cuantos lugares sucederán este tipo de cosas y en cuantas situaciones hay riesgo de desastre absoluto porque el que instalo el servidor de base de datos, no puso un password.

Noviembre 29, 2008

Lo nuevo de SQL Server 2008: Grouping Sets

Archivado en: SQL Server, SQL Server 2008, T-SQL — Etiquetas:, — grimpi @ 4:33 pm

Una característica piola que tiene la nueva versión de SQL Server, es el nuevo operador GROUPING SETS, que ya existía en otros motores como Oracle hace tiempo (es un estándar ANSI 2006) y permite combinar consultas de agrupación distintas en una sola consulta. El operador GROUPING SETS es una extensión de la cláusula estándar GROUP BY.
Cuando no se requieren todas las agrupaciones posibles que se generan utilizando un operador ROLLUP o CUBE (que ya existían en SQL Server 2005), se debe utilizar GROUPING SETS para especificar sólo las agrupaciones que se deseen. O sea, gracias a GROUPING SETS obtenemos los niveles de agrupación deseados y además nos devuelve el subtotal para cada subconjunto de agrupación.
Podríamos decir que GROUPING SET es mas abarcativo y genérico que ROLLUP o CUBE. Generalmente consultas que usen este tipo de operadores están relacionadas con el análisis de datos, reporting y todo lo relacionado con el mundo BI.
Este nuevo operador no permite hacer nada nuevo que antes no se pudiese, solo simplifica y optimiza determinadas consultas, lo ya de por si, es un factor importante.
Veamos un ejemplo de donde utilizarlo:

Supongamos que tenemos la siguiente tabla de Clientes con datos ya cargados:

CREATE TABLE [dbo].[Client]
(

[ID] [int] IDENTITY(1,1) NOT NULL,
[Name] [varchar](50) NULL,
[Country] [varchar](50) NULL,
[Zone] [varchar](50) NULL,
[Year] [int] NULL,
[Precio] [numeric](12, 4) NULL,

)

Y ahora supongamos que queremos traer la suma del campo Precio, agrupada por los Campos Country y Zone, luego solo por Zone y luego por el total y todo en una sola misma sentencia de tal manera que el resultado de la consulta fuese el siguiente:


La primera opción que uno piensa, es hacer una consulta usando UNION ALL con tres consultas diferentes, una por cada agrupación:

SELECT SUM(Value) AS Total, Zone, Country FROM dbo.Client
GROUP BY Zone,Country

UNION ALL
SELECT SUM(Value), NULL, Country FROM dbo.Client
GROUP BY Country

UNION ALL
SELECT SUM(Value), NULL, NULL FROM dbo.Client
ORDER BY Zone, Country

Sin embargo, podemos escribir una consulta equivalente y acá entra GROUPING SETS de SQL Server 2008 para simplificarnos la vida:

SELECT SUM(value) AS Total, Zone, Country FROM dbo.Client
GROUP BY
GROUPING SETS
(
(Zone, Country),
(Country),
()
)
ORDER BY GROUPING(Zone), GROUPING(Country)

Ahora como podrán ver en este último ejemplo, además de usar el operador GROUPING SETS, hago uso de la función GROUPING en el ORDER BY, para ordenar el resultado por Zona y Country.
Esta función (que ya existía en SQL Server 2005), nos indica si una expresión de columna especificada en una lista GROUP BY es agregada o no. GROUPING devuelve 1 para agregado y 0 para no agregado, en el conjunto de resultados.

Performance:

La ventaja del uso GROUPING SETS no está solo dada por simplificar sintácticamente las consultas, sino también en cuestiones de performance.
En las pruebas que hice para monitorear el uso de recursos (SET STATISTICS IO ON) con 90000 registros en la tabla Client estos fueron los resultados:

Usando UNION ALL:
Table ‘Client’. Scan count 3, logical reads 1728, physical reads 6, read-ahead reads 590, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.

Usando GROUPING SETS:
Table ‘Client’. Scan count 1, logical reads 576, physical reads 6, read-ahead reads 590, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.

Como podemos ver, GROUPING SETS hace menor uso de recursos de I/O. La razón de la mejor performance, se debe a que usando la nueva característica de SQL Server, el motor necesita leer menos paginas de datos, ya que hace el cálculo de agregación de más alto nivel sobre las agregaciones de menor nivel (usando como ejemplo nuestra consulta, el nivel de menor de agregación seria Zone y Country, luego solo Country y por último a partir del resultado de la prime).
Dejo la tarea al lector para que compare los planes de ejecución y pueda apreciar las diferencias en las operaciones que realiza el motor entre una consulta y otra.

Links:
Recomiendo leer el articulo de MSDN de equivalencias de GROUPING SETS.

Noviembre 22, 2008

Patrones de Acceso a Datos: Active Record

Archivado en: .NET, Arquitectura, Capa de Datos, Patrones — Etiquetas:, — grimpi @ 10:12 pm

Existen varias estrategias, arquitecturas y patrones de diseño para el manejo de la lógica de negocio y el acceso a la base de datos.
Que determina el uso de una u otra arquitectura esta dado por la especificadores y características del software a desarrollar y también por el gusto de quien diseña el esqueleto de la aplicación (la subjetividad es un factor muy determinante en el desarrollo de software, muchas veces más que cualquier argumento racional).
Hoy vamos a ver uno de estos patrones: Active Record.
ActiveRecord es un patrón en el cual, el objeto contiene los datos que representan a un renglón (o registro) de nuestra tabla o vista, además de encapsular la lógica necesaria para acceder a la base de datos. De esta forma el acceso a datos se presenta de manera uniforma a través de la aplicación.
Lógica de Negocio + Acceso a Datos en una misma clase.
Una clase Active Record consiste en el conjunto de propiedades que representa las columnas de la tabla más los típicos métodos de acceso como las operaciones CRUD, búsqueda (Find), validaciones, y métodos de negocio.
Personalmente me gusta mucho este enfoque, es muy elegante y simple.

En que situaciones CONVIENE usar este patrón?

  • Lógica de negocio simple y poco relacionada con otras entidades.
  • Es ideal cuando la estructura de la tabla coincide con la estructura de la clase.

En que situaciones NO CONVIENE de Active Record:

  • Es simple. Esto es bueno y malo al mismo tiempo. Con lógica de negocio compleja, este patrón pierde coherencia.
  • Otra desventaja que al estar tan acoplado a la estructura de la clase, un cambio en el diseño de la tabla, implica cambiar la clase.
  • En situaciones de operaciones de alto volumen de datos, el overhead que se paga en el pasaje y carga de datos, es innecesario. Esta desventaja aplica tanto a Active Record, como a cualquier otro diseño orientado a objetos.
  • Muchos “puristas” de OOP critican que ActiveRecord tiene una misma clase tanto la responsabilidad de acceder a la base como de manejar la lógica de negocio y “ensucia” el código. Nunca coincidí con ese fundamentalismo que a veces prioriza el purismo por sobre la simplicidad, pero es algo que muchos critican de este patrón.

Ejemplo de una clase típica de Active Record:

public class Order
{

public Order()
{
}

public Order(int orderID)
{
}

public int OrderID {get; set;}
public DateTime OrderDate {get; set;}
public DateTime ShipDate {get; set;}
public string ShipName {get; set;}
public string ShipAddress {get; set;}
public string ShipCity {get; set;}
public string ShipCountry {get; set;}

public void Insert()
{
// Inserta un registro en la tabla
}

public void Delete()
{
// Elimina el registro de la tabla
}

public void Update()
{
// Modifica el registro en la tabla
}

public static int GetCount()
{
// Retorna el total de registros de la tabla
}

public static Order FindById(int id)
{
//Busca en la tabla el objeto usando como criterio su id.
}

public static List<Order> LoadAll()
{
// Carga todos los regisotros de la tabla.
}

}
Por lo general siempre existen algunos métodos estáticos, como por ejemplo LoadAll(), que devuelve una colección de objetos de la misma clase. Otro típico ejemplo de un método estático es el FindById()

Conversión de datos

Este es un problema habitual en cualquier metodología que haga un mapeo de datos de un objeto con la base de datos.
Por ejemplo, si tenemos en la tabla una columna de tipo int cuyo valor en un registro es nulo, como convertimos este valor dentro de una propiedad del objeto? Por un lado podemos usar Nullable<int> que viene a partir de .NET 2.0. Otra estrategia es transformar valores nulos en 0. Puede parecer un poco desprolija esta metodología, pero muchas veces hay que preguntarse si queremos identificar entre un campo numérico nulo y uno cuyo valor sea 0. Obviamente, si queremos poder distinguir entre estos 2 valores, deberemos usar Nullable<int>.

Foreign Key Mapping Pattern (FKM)

Supongamos con el ejemplo que vimos anteriormente, que la tabla Order, tiene una FK a la tabla Customer. Como se debe comportar una clase Active Record cuando la tabla con la que trabaja tiene una Foreing Key?
Existen 2 maneras:

En primer lugar, mantener lo mas purista y simple posible y agregar solamente una propiedad más que sea el CustomerId, de manera que la estructura del objeto sea idéntica a la estructura de la tabla.

public int CustomerID {get; set;}

La segunda opción, es usar el patron Foreign Key Mapper. En mi opinión, una opcion mucho mas clara mucho más clara que consiste en vez de agregar una propiedad que represente el ID de la tabla, agregar una propiedad que sea una referencia directa al objeto.

public Customer Customer {get; set;}

Row Data Gateway Pattern (RDG)

Row Data Gateway es un patrón exclusivamente orientado al acceso a la base de datos, pero de características similares a Active Record.
La gran diferencia entre ambos, es que Active Record incluye métodos de acceso a la base de datos y métodos de lógica de negocio, mientras que Row Data Gateway, solo incluye métodos de acceso a la base.

Frameworks y generadores de codigo

  • Castle ActiveRecord: Por lejos, el framework más común que para trabajar con Active Record en .NET. Esta implementado sobre nhibernate, por lo cual también hay que tener esta librería para poder usarlo.
  • LINQ to SQL: Es la solución desarrollada por Microsoft para el mapeo de objetos con la base de datos. Puede también usarse con el patrón Active Record.
  • Además de estos frameworks, existen herramientas de generación de código en base a este patrón como .netTiers. También uno se puede crear su propio template de generación de código con MyGeneration o CodeSmith.


Entradas más antiguas »

Blog de WordPress.com.