Buscar este blog

martes, 21 de junio de 2016

Usa el esquema en tus consultas o rompe tu código sin darte cuenta

Hace ya algún tiempo contábamos en otra entrada de este blog la importancia que tiene nombrar correctamente a los objetos de una base de datos a la hora de realizar las consultas: siempre debemos usar el formato esquema.nombre.

Los motivos quedaban claros entonces: evitar problemas de rendimiento en la compilación de las consultas debidos al proceso de resolución de nombres y evitar errores si el esquema por defecto de un usuario es distinto a "dbo" o "NULL".

Si bien estos errores pueden ocasionar algún quebradero de cabeza, su resolución es bastante sencilla, tal y como se explica en el artículo anterior. Sin embargo, añadimos ahora un nuevo caso que podría ser más problemático, ya que podemos generar un error que pasase inadvertido, devolviendo registros incorrectos en nuestras consultas.

Cuando trabajamos con más de un esquema -cosa que deberíamos hacer no sólo por motivos de organización de nuestros objetos, sino también para aplicar permisos sobre ellos más ágilmente- es especialmente importante prestar atención en la definición de vistas y procedimientos almacenados.

En ellos, deberemos poner siempre el esquema y el nombre de los objetos a los que hagamos referencia, si no queremos exponer nuestro código a futuros problemas.

Abocados al cataclismo


Supongamos que en nuestra base de datos usamos esquemas, en concreto "dbo" y "web". En el esquema dbo tenemos dos tablas "Cliente" y "Pedido", en el esquema web añadimos ahora la vista "PedidosCliente" e insertamos algunos registros en las tablas a modo de prueba. El script completo para crear los objetos y rellenar las tablas es el siguiente:

Una vez tenemos los objetos creados, ejecutamos una simple consulta sobre la vista:

Que nos devolverá tres resultados: Dos pedidos para Juan y uno para María.

Ahora bien, imaginemos que solamente para los clientes web necesitamos guardar más información, por ejemplo su password. Como tenemos clientes tradicionales y web, decidimos añadir una segunda tabla Cliente, esta vez en el esquema web, que hará referencia a la tabla dbo.Cliente por el Codigo y extenderá la información de sus registros con la columna Password, para aquellos que vengan a través de la web. Así:


Empieza la batalla de los esquemas


A estas alturas ya podemos empezar a tener dudas sobre a qué tabla referencia nuestra vista web.PedidosCliente. Recordemos que habíamos referenciado a las tablas Pedido y Cliente sin usar el esquema en la definición de la vista. Así pues, ¿la vista seguirá usando dbo.Clientes como hasta ahora, o bien pasará a referenciar la nueva tabla web.Clientes?

Una primera comprobación que podemos hacer es, en el SQL Server Management Studio, hacer clic derecho sobre la vista y en el menú contextual seleccionar "Ver Dependencias".

SQL Server nos dice que la vista sigue dependiendo de dbo.Cliente
Parece que sigue usando la tabla dbo.Cliente. Para estar doblemente seguros, podemos consultar también la definición de la vista  -igualmente con clic derecho sobre ella- y ver cómo está definida. Efectivamente, SQL Server parece estar seguro, es sobre dbo.Cliente:

SELECT        P.Numero, P.FechaPedido, C.Nombre
FROM            dbo.Pedido AS P INNER JOIN
                         dbo.Cliente AS C ON P.CodigoCliente = C.Codigo

SQL Server Management Studio no dice la verdad


A estas alturas podemos asumir que nuestra vista seguirá funcionando como si nada hubiera pasado. Para comprobarlo, ejecutemos la misma consulta del principio de esta entrada sobre la vista. Debería devolver los mismos tres resultados, ¿verdad? Veámoslo:

Msg 207, Level 16, State 1, Procedure PedidosCliente, Line 3 [Batch Start Line 44]El nombre de columna 'Nombre' no es válido.Msg 4413, Level 16, State 1, Line 46No se pudo usar la vista o función 'web.PedidosCliente' debido a errores de enlace.

¡Oh! ¡Oh! Hemos roto nuestra vista y no nos hemos dado cuenta hasta que hemos ejecutado una consulta sobre ella.

Sin embargo, esta es la buena noticia dentro de la gravedad del problema. Ha ocurrido un error y SQL Server nos lo ha notificado. Este error se produce porque la vista está definida dentro del esquema web. Al estar dentro de este esquema, pese a que el Management Studio nos insista en que la vista hace referencia a la tabla dbo.Cliente, no es así: al crear la tabla web.Cliente, la vista hace referencia a ella, porque está en su mismo esquema. Como esta tabla no tiene la columna Nombre, se produce el error.

El mismo error lo podríamos obtener si recreásemos la vista o la intentásemos modificar sin alterar la definición.

Aún puede ser peor


Sí, podría serlo. Si nuestra nueva tabla web.Cliente también tuviese un campo Nombre -imaginemos que queremos poner nombre de usuario web, pero llamamos a la columna Nombre- no se produciría ningún error. Simplemente cambiaría el conjunto de resultados devuelto, circunstancia que podría pasar inadvertida hasta que, por ejemplo, un usuario detectase la anomalía. Por ejemplo:

¡Oooooh! ¡Ooooooh! ¿Ningún resultado? ¿Qué ha pasado con nuestros tres pedidos de Juan y María? Fácil: la consulta sobre la vista ha funcionado, pero haciendo referencia a la tabla web.Cliente, donde no tenemos ningún registro. Así que la vista nos devuelve eso: ningún registro.

La solución


Es sencilla: Nombrar a los objetos por su nombre completo, que incluye esquema.nombre. Si en la definición de la vista original simplemente hubiésemos puesto dbo.Cliente y dbo.Pedido, no habría forma de haberla roto, ni con error ni inadvertidamente. Así:


Recuérdalo, llama a las cosas por su nombre, o podrás tener problemas que, por muy poco probables que parezcan, ocurrirán en el momento más inoportuno. Siempre es mejor no dejarlo en manos del azar.

Con SCHEMABINDING esto no pasaría


Efectivamente, hay otra solución. SQL Server nos permite definir nuestras vistas (y funciones) con la opción SCHEMABINDING. Esta opción hace que no se pueda cambiar la definición de los objetos a los que hace referencia la vista (las tablas Pedido y Cliente en nuestro caso). Además, tal y como explica la MSDN, se nos obliga a usar el esquema siempre cuando definimos una vista con WITH SCHEMABINDING.

Cuando se utiliza SCHEMABINDING, select_statement debe incluir los nombres de dos partes (schema.object) de las tablas, vistas o funciones definidas por el usuario a las que se hace referencia.
Por ejemplo, si intentamos definir nuestra vista web.PedidosCliente así:
Recibiremos el siguiente error:

Msg 4512, Level 16, State 3, Procedure PedidosCliente, Line 4 [Batch Start Line 27]No se puede enlazar a esquema vista 'web.PedidosCliente' porque el nombre 'Pedido' no es válido para enlazar a esquema. Los nombres deben constar de dos partes y los objetos no pueden hacer referencia a sí mismos.