jueves, 12 de diciembre de 2013

Read uncommitted


En esta entrada exploraremos cómo funciona el nivel de aislamiento READ UNCOMMITTED.

Antes de comenzar el ejercicio quisiera hacer notar que el nivel de aislamiento por defecto de SQL Server es READ COMMITTED, esto implica que no podemos leer la información sino hasta que ésta ha sido actualizada de forma definitiva (committed) en la base de datos o bien los cambios han sido deshechos a través de un rollback.

Por ejemplo, supongamos que la transacción 1 ejecuta la siguiente instrucción:
USE AdventureWorks GO BEGIN TRAN UPDATE Production.Product SET ListPrice += 1 WHERE ProductID = 1

Notamos que la transacción queda "abierta", es decir que no ha sido confirmada hacia la base de datos (committed) o deshecha (rollback), por lo tanto SQL Server mantendrá el bloqueo de tipo exclusivo sobre el producto cuyo id sea 1.

En otra ventana ejecutaremos la siguiente sentencia:
USE AdventureWorks GO BEGIN TRAN UPDATE Production.Product SET ListPrice += 1 WHERE ProductID = 1

Veremos que este query queda en espera debido a que el nivel de bloqueo que él requiere (READ COMMITTED) no le permite obtener la información que necesita debido a que la transacción 1 no ha finalizado. La transacción 1 tiene un bloqueo exclusivo que no es compatible con READ COMMITTED de otra transacción.

Esta información la podemos confirmar con la siguiente sentencia:
USE AdventureWorks GO SELECT DB_NAME(T.resource_database_id) AS database_name, OBJECT_NAME(P.[object_id]) FROM sys.dm_tran_locks T INNER JOIN sys.partitions P ON T.resource_associated_entity_id = P.hobt_id WHERE T.request_mode = 'X'

Veremos que se tiene un bloqueo exclusivo sobre algún elemento del objeto Production.Product. También se podría obtener la fila que está siendo bloqueada, pero perderíamos el enfoque de esta entrada.

Volvamos a la ventana de la transacción 1 y ejecutemos COMMIT, veremos que de forma casi inmediata la transacción 2 será desbloqueada y podrá obtener los datos que requiere.

¿Qué pasa cuando la información que queremos obtener es "inmutable"?, no quisiera utilizar la palabra inmutable en su totalidad debido a que en teoría los únicos datos inmutables de una base de datos son las llaves primarias, pero podemos suponer que el nombre de un producto es inmutable, sería muy extraño tener que actualizar el nombre de un producto. En este caso no requerimos que alguna otra transacción genere algún bloqueo sobre la lectura del Id y del Nombre del producto, por lo tanto, necesitamos leer los datos independientemente de los cambios que se estén realizando sobre ellos.

Antes de hacer cambios en el código, vamos a la ventana de la transacción 2 y ejecutamos COMMIT para no dejar transacciones abiertas (colgadas) en el manejador de bases de datos.

Volvamos a ejecutar el código de la transacción 1 para generar un bloqueo sobre el producto cuyo Id es 1, y vamos a cambiar un poco el código de la transacción 2:
SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED BEGIN TRAN SELECT ProductID, Name, ListPrice FROM Production.Product WHERE ProductID = 1

Podemos notar que ahora estamos estableciendo el nivel de aislamiento en READ UNCOMMITTED para poder leer datos que no han sido hechos "oficiales" en la base de datos. Si ejecutamos la transacción veremos que la información del nombre, producto y precio son inmediatamente devueltos.

Hasta ahora todo parecería que salió tal como lo deseamos, pero... ¿qué pasaría si necesitáramos realizar alguna acción tomando el precio del producto?, he ahí que vendría un fenómeno conocido como "dirty reads", eso quiere decir que podríamos leer un valor que no ha sido hecho oficial sobre la base de datos y que posiblemente su cambio pudiera ser deshecho, es decir que operaríamos con un valor que jamás debió ser tomado.

¿Qué debemos hacer?, dependerá de la operación y de los valores que queremos utilizar, deberemos establecer categorías de los datos que requieren un bloqueo exclusivo y también identificar los datos que pueden ser leídos durante una transacción de actualización.

Espero te haya resultado de utilidad esta entrada, nos vemos la siguiente.

miércoles, 11 de diciembre de 2013

Refrescando los metadatos de una vista


En esta entrada mostraré un fenómeno que se da cuando no establecemos de forma explícita las columnas que queremos devolver en una vista.

Comenzaremos creando una tabla, metiéndole datos y creando una vista que mostrará el contenido de toda la tabla:
CREATE TABLE Test( Id INT IDENTITY(1, 1), Value VARCHAR(10) ) GO INSERT INTO Test (Value) VALUES ('Valor 1'), ('Valor 2'), ('Valor 3') GO CREATE VIEW vTest AS SELECT * FROM Test GO SELECT * FROM vTest

Veremos que el resultado de la consulta a la vista nos devolverá el contenido completo de la tabla (tal como lo esperábamos):

Ahora vamos a modificar la estructura de nuestra tabla para agregar una columna calculada que muestre la primera letra de la columna Value:
ALTER TABLE Test ADD FirstLetter AS SUBSTRING(Value, 1, 1) PERSISTED

Ahora vamos a ejecutar un query que muestre todo el contenido de la tabla y también que muestre todo el contenido de la vista:

Notemos que la primera sentencia que trabaja sobre la tabla muestra las tres columnas que conforman al objeto, mientras que la segunda sentencia sólo muestra dos columnas, las columnas que existían en la tabla durante el momento de la creación de la vista. ¿Qué es lo que está pasando?, que los metadatos de la vista siguen siendo los mismos, es por ello que aunque cambió la definición de la tabla, la vista sigue utilizando las mismas columnas que antes.

La forma de corregirlo es a través del procedimiento almacenado sp_refreshview:
sp_refreshview 'vTest' GO SELECT * FROM vTest

Notemos que ahora ya se están mostrando las tres columnas que forman parte de la tabla.

Aunque te muestro cómo corregir este problema, no es nada recomendable utilizar un query del tipo SELECT * FROM Table porque se estará ignorando cualquier índice que haya sido creado sobre la tabla para optimizar su lectura.

Espero esta entrada te haya resultado de utilidad, nos vemos la siguiente.

martes, 10 de diciembre de 2013

Limpiar historial de respaldos


En esta entrada muy corta pero sumamente útil, te mostraré cómo eliminar el historial de copias de respaldo que se acumula en la base de datos msdb a lo largo del tiempo.

Comenzaré explicándote que no importa cómo saques el respaldo de tu base de datos, el historial de haberlo hecho se guarda, haya sido un respaldo de tipo Full, Log, Differential o de cualquier otro tipo.

Es común que en servidores que tengan implementado un esquema de Log Shipping, el tamaño de la base de datos msdb se dispare, esto se debe a que la cantidad de respaldos aumenta por la naturaleza propia de este esquema de alta disponibilidad y por lo tanto el historial de respaldos se incrementa.

Antes de limpiar el historial de respaldos es importante sacar un respaldo de la base de datos msdb, esto es para poder tener evidencia clara de las operaciones que se realizaron y en caso de una auditoría podamos demostrar que se hicieron los respaldos en tiempo y forma. También puede servirnos para demostrar que no se hicieron los respaldos, a veces la gente encargada de esta pequeña parte de la operación se le va el avión y no revisa que los respaldos se hayan ejecutado de forma correcta.

Si quisiéramos eliminar todo el historial de respaldos bastaría con utilizar el siguiente código:
DECLARE @Today DATETIME = GETDATE() EXEC msdb.dbo.sp_delete_backuphistory @Today

Veamos que el procedimiento almacenado sp_delete_backuphistory recibe como parámetro la fecha más antigua que se guardará en el historial de respaldos, eso implica que todo registro histórico de respaldo que se haya creado antes de esa fecha ya no estará almacenado en msdb.

Si se sacara un respaldo cada 30 días de la base de datos msdb, bien podríamos estar limpiando el historial de la siguiente manera:
DECLARE @MonthAgo DATETIME = DATEADD(DAY, -30, GETDATE()) EXEC msdb.dbo.sp_delete_backuphistory @MonthAgo

Espero esta entrada te haya resultado útil, nos vemos la siguiente.

lunes, 9 de diciembre de 2013

IDENTITY vs SCOPE_IDENTITY()


En esta entrada exploraremos la diferencia entre estas dos funciones del sistema que mucha gente utiliza y que es sumamente importante conocer el valor que están devolviendo.

Comenzaremos creando dos tablas, en una agregaremos valores y la otra servirá para llevar un historial muy sencillo de movimientos:
CREATE TABLE TestIdentity( Id INT IDENTITY(1, 1), Value VARCHAR(10) NOT NULL ) GO CREATE TABLE TestIdentityLog( Id INT IDENTITY(1, 1), Task VARCHAR(10), Value VARCHAR(10) )

En la tabla TestIdentity iremos guardando valores de prueba y la tabla TestIdentityLog estará guardando el historial de movimientos que se han llevado a cabo sobre la tabla TestIdentity. Debemos notar que en las dos tablas tenemos una columna IDENTITY.

Ahora vamos a crear dos triggers, uno para el INSERT y otro para el DELETE:
CREATE TRIGGER TestIdentity_Insert ON TestIdentity FOR INSERT AS BEGIN INSERT INTO TestIdentityLog (Task, Value) SELECT 'INSERT', Value FROM INSERTED END GO CREATE TRIGGER TestIdentity_Delete ON TestIdentity FOR DELETE AS BEGIN INSERT INTO TestIdentityLog (Task, Value) SELECT 'DELETE', Value FROM DELETED END

Ambos guardan el movimiento en la tabla de historial, sólo que el primero es para INSERT y el segundo para DELETE

Vamos a crear un procedimiento almacenado para agregar un valor a TestIdentity, de paso mostraremos el resultado de la ejecución de los dos valores que nos interesan: @@IDENTITY y SCOPE_IDENTITY():
CREATE PROC pAddValue( @Value VARCHAR(10) ) AS BEGIN INSERT INTO TestIdentity (Value) VALUES (@Value) PRINT '@@IDENTITY: ' + CAST(@@IDENTITY AS VARCHAR) PRINT 'SCOPE_IDENTITY(): ' + CAST(SCOPE_IDENTITY() AS VARCHAR) END

Ahora vamos a crear un procedimiento almacenado que nos permita eliminar un elemento de la tabla TestIdentity:
CREATE PROC pDeleteValue( @Id INT ) AS BEGIN DELETE TestIdentity WHERE Id = @Id END

Habitualmente se piensa que podemos utilizar cualquiera de los dos sin problema, pero al ejecutar el siguiente código veremos que ambas nos mostrarán números diferentes en la tercera ejecución:
SET NOCOUNT ON GO EXEC pAddValue 'Valor 1' GO EXEC pDeleteValue 1 GO EXEC pAddValue 'Valor 1'

El primer EXEC mostrará que @@IDENTITY devuelve 1 y SCOPE_IDENTITY() devuelve 1, pero veremos que el tercer EXEC mostrará que @@IDENTITY devuelve 3 y SCOPE_IDENTITY() devuelve 2.

Esto se debe a que @@IDENTITY devuelve el último valor obtenido a través de una columna IDENTITY, este valor pertence a la columna Id de la tabla TestIdentityLog, los trigger que se están disparando ante el INSERT o DELETE son los que están realizando esa inserción y por lo tanto el último valor asignado es el de la tabla TestIdentityLog.

La función SCOPE_IDENTITY() devuelve el último valor asignado por IDENTITY en el entorno actual de ejecución, para nuestro procedimiento pAddValue el único IDENTITY que tiene a su alcance es el que está en la columna Id de la tabla TestIdentity, es por ello que devuelve el valor 2.

Como podrás notar, es importante conocer la diferencia entre las dos opciones que tenemos, dependerá del objetivo del procedimiento o de la operación cuál te resulte adecuado a tus necesidades; generalmente utilizarás SCOPE_IDENTITY()

Espero esta entrada te haya resultado útil, nos vemos la siguiente.

viernes, 6 de diciembre de 2013

Mapa con geolocalización


En esta entrada crearemos un mapa de Google Maps utilizando la API versión 3.14 y ubicaremos un marcador en la posición actual del usuario utilizando la geolocalización del dispositivo.

Para comenzar crearemos un sitio web en VS2010 o VS2012 y agregaremos tres carpetas: css, img y js. En la carpeta js colocaremos jQuery, creamos un archivo map.js y también lo agregamos a la carpeta js. Creamos un archivo de hoja de estilos llamado site.css y lo colocamos en nuestra carpeta css.

Para el icono de la posición actual del usuario podemos utilizar uno de los que aparecen en el sitio http://mapicons.nicolasmollet.com, tienen un diseño atractivo y se puede personalizar el color de fondo. Una vez elegido el icono lo guardamos en nuestra carpeta img con el nombre current_location.png

Una vez realizados estos pasos nuestro Explorador de Soluciones debería verse de la siguiente forma:

Vamos a incorporar todos los elementos junto con la referencia a la API de Google Maps en nuestra página HTML con el siguiente código:
<head> <title>Ubicación actual</title> <script src="http://maps.google.com/maps/api/js?v=3.14&sensor=false&language=es-mx" type="text/javascript"></script> <script src="js/jquery-1.10.2.min.js" type="text/javascript"></script> <link href="css/site.css" rel="stylesheet" type="text/css" /> <script src="js/map.js" type="text/javascript"></script> </head> <body> <div id="map"></div> </body> </html>

Vamos a abrir nuestra hoja de estilos y vamos a colocar algunos estilos para que el mapa ocupe la pantalla completa del dispositivo:
html, body { margin: 0px; height: 100%; } #map { width: 100%; height: 100%; }

Ahora vamos a generar el código que nos permitirá la creación del mapa, la localización de la ubicación del usuario y la colocación del respectivo marcador en el mapa.

Abriremos nuestro archivo map.js y comenzaremos con la obtención de la ubicación actual del usuario:
///<reference path="jquery-1.10.2.min.js" /> var blogger = blogger || {}; blogger.start = function () { if (window.clientInformation) { window.clientInformation.geolocation.getCurrentPosition( function (e) { var latlng = new google.maps.LatLng(e.coords.latitude, e.coords.longitude); initialize(latlng); }, function (e) { initialize(null); }, { enableHighAccuracy: true, timeout: 5000, maximumAge: 0 } ); } else { window.navigator.geolocation.getCurrentPosition( function (e) { var latlng = new google.maps.LatLng(e.coords.latitude, e.coords.longitude); initialize(latlng); }, function (e) { initialize(null); }, { enableHighAccuracy: true, timeout: 5000, maximumAge: 0 } ); } function initialize(e) { } } $(document).ready(function () { blogger.start(); });

En la primera línea de código estamos agregando una referencia a la librería jQuery para que el editor tome la definición de las funciones de esa librería y nos facilite la codificación.

Estamos creando un espacio de nombres llamado blogger para que el manejo de nuestras variables y funciones no vayan a causar conflicto con otras que estén depositadas en el espacio global. Una vez que haya finalizado la carga de la página estamos mandando a llamar a la función blogger.start que será la encargada de todo el trabajo.

Estamos validando que exista la propiedad window.clientInformation para poder diferenciar entre el navegador Safari y los demás, veamos que cuando no existe esa propiedad la obtención de la localización actual del usuario se hace a través del objeto window.navigator

La función getCurrentPosition recibe tres argumentos, el primero marca la función que se mandará a llamar cuando se haya podido obtener la posición del usuario, el segundo es la función que se ejecutará cuando no se haya podido obtener la localización del usuario y el tercero es un conjunto de opciones que nos permiten establecer si queremos la mayor exactitud posible, el tiempo en milisegundos que esperaremos para obtener la posición y el tiempo que esta posición estará guardada en caché.

Veamos que cuando la localización haya sido exitosa, se estará construyendo un objeto google.maps.LatLng, este objeto nos permite establecer ubicaciones en la notación (Latitud, Longitud) en el mapa.

Ahora vamos a codificar nuestra función initialize que creará el mapa y el marcador con la ubicación actual del usuario.

Comenzaremos con la creación del mapa centrándolo en la ubicación del usuario con un zoom de 14:
function initialize(e) { var mapOptions = { center: e, zoom: 14 }; blogger.map = new google.maps.Map(document.getElementById('map'), mapOptions); }

Veamos que el constructor del objeto google.maps.Map nos pide el nodo donde se alojará el mapa (en nuestro caso un div cuyo id es "map") y también un literal object que tenga la configuración del mapa. Vamos a abrir nuestro archivo index.htm con el navegador y veamos que nos pedirá la autorización para enviar nuestra localización a la página web, esto es completamente normal.

Ahora vamos a crear un marcador que utilice la imagen que tenemos en la carpeta img y lo colocaremos en la posición actual del usuario: function initialize(e) { var mapOptions = { center: e, zoom: 14 }; blogger.map = new google.maps.Map(document.getElementById('map'), mapOptions); var markerOptions = { position: e, map: blogger.map, title: 'Usted se encuentra aquí', icon: 'img/current_location.png' } var currentPositionMarker = new google.maps.Marker(markerOptions); }

El constructor del objeto google.maps.Marker nos pide un objeto literal que traiga la configuración del marcador, en este caso nos pide la posición del marcador (google.maps.LagLnt), el mapa donded estará localizado el marcador (blogger.map), el texto que mostrará como tooltip del marcador y la url del icono que queremos utilizar. En caso de que no utilicemos el atributo "icon", entonces se colocará el marcador que tiene por defecto Google Maps.

Si visitamos de nueva cuenta nuestro sitio, veremos que colocará el marcador en nuestra posición actual.

Espero te haya resultado de utilidad esta entrada, actualmente muchas aplicaciones están requiriendo del uso de mapas y Google Maps es una opción muy buena dada la facilidad que tiene su API para ser utilizada.

¡Nos vemos la siguiente!

jueves, 5 de diciembre de 2013

Web Workers


En esta entrada veremos la implementación de un Worker de HTML5 muy sencillo pero con implicaciones de conocimiento que vale la pena aprender.

Los Worker se hicieron para ejecución de tareas asíncronas que se ejecutarán en un hilo ajeno al que está ejecutando la página web. Tan ajeno que desde el Worker no tenemos acceso al DOM de la página, es decir que toda la sincronización se hace a través de mensajes, de objetos que sean serializables. Por excelencia utilizaremos JSON.

El problema planteado es que necesitamos realizar peticiones cada 10 segundos a un endpoint el cual devolverá un valor estadísticamente único, simulando que debemos estar checando el servidor para ver si hay datos nuevos para actualizar la interfaz del usuario.

Para esto vamos a preparar nuestro entorno de desarrollo (VS2010 o VS2012):

  1. Crear un sitio web
  2. Agregar una carpeta llamada js
  3. Agregar un archivo index.htm
  4. Agregar un Generic Handler llamado GetRandom.ashx
  5. Agregar jQuery a la carpeta js
  6. Agregar dos archivos js a la carpeta: tools.js y randomWorker.js

El archivo tools.js será el que esté en comunicación con el randomWorker, éste último es el que realizará las peticiones a GetRandom.ashx para un valor nuevo.

Prepararemos nuestro archivo index.htm con el siguiente código:
<head> <title>Valor aleatorio a través de Worker</title> <script src="js/jquery-1.7.2.min.js" type="text/javascript"></script> <script src="js/tools.js" type="text/javascript"></script> </head> <body> <p>Este es el valor aleatorio generado desde el servidor: <span id="lblAleatorio"></span></p> </body>

Vamos a abrir nuestro randomWorker.js para programar la tarea asíncrona que se estará ejecutando cada 10 segundos:
self.onmessage = function (e) { var xmlhttp = new XMLHttpRequest(); xmlhttp.onreadystatechange = function () { if (xmlhttp.readyState == 4 && xmlhttp.status == 200) { postMessage(JSON.parse(xmlhttp.response)); } } var url = '../GetRandom.ashx'; xmlhttp.open('GET', url); xmlhttp.send(); }

Estamos creando una instancia de XMLHttpRequest para realizar las peticiones a GetRandom.ashx. ¿Por qué no utilizamos $.ajax?, porque jQuery está basado en el DOM y por lo tanto no puede ser utilizado desde un Worker.

Estamos asignando un manejador al evento onreadystatechange que se dispara cada vez que hay un cambio en el estado de la petición. Los valores para readyState son los siguientes:

0: Sin inicializar, es decir que el método open no ha sido llamado.
1: Cargando, ya fue abierto pero el método send no ha sido llamado.
2: El método send ya fue llamado.
3: La respuesta está siendo descargada.
4: Todas las operaciones han finalizado.

El atributo status establece un valor para el tipo de respuesta que se está recibiendo. Los códigos de estado pueden tener los siguientes valores:

1xx: El servidor ya recibió las cabeceras y se puede proceder con el envío del cuerpo de la petición.
2xx: La solicitud fue recibida por el servidor y fue correcta.
3xx: Para terminar la solicitud el cliente debe de tomar acciones adicionales. Generalmente encontraremos que se debe a que el recurso ya fue cambiado de dirección.
4xx: La sintaxis de la petición no es la correcta o bien esta no puede ser procesada. Esto indica un error del lado del cliente.
5xx: Ocurrió un error del lado del servidor.

Tomando en cuenta lo anterior, el readyState que nos interesa es el 4 y el status es el 200, es por ello que esa validación se está haciendo en el if.

Nuestro GetRandom.ashx estará devolviendo información en formato JSON por lo que procedemos a procesarla y con la función postMessage se la enviamos al cliente del Worker (tools.js)

Notemos también que no estamos utilizando la función window.setInterval para programar la petición cada 10 segundos, eso es debido a que en el Worker no tenemos acceso al DOM y por lo tanto no tenemos acceso al objeto window.

Abrimos nuestro archivo tools.js y colocamos el siguiente código:
var tools = tools || {}; tools.Worker = new Worker('js/randomWorker.js'); tools.Worker.onmessage = function (m) { $('#lblAleatorio').text(m.data.value); }; tools.Worker.onerror = function (e) { alert('Ocurrió un problema durante la ejecución del Worker'); } $(document).ready(function () { tools.Worker.postMessage('getRandomValue'); window.setInterval(function () { tools.Worker.postMessage('getRandomValue'); }, 10000); });

Estamos creando un espacio de nombres "tools" para que cualquier variable que utilicemos no entre en conflicto con otra que esté en la página. Después estamos creando una instancia del objeto Worker que será el encargado de ejecutar la tarea asíncrona establecida en el archivo randomWorker.js.

Estamos asignando un manejador al evento onmessage del objeto Worker que recibirá el resultado de la ejecución asíncrona del Worker. Notemos que el resultado viene en la propiedad data. El resultado de la ejecución la colocaremos en nuestro elemento span con el id lblAleatorio.

Por si hay algún error, estamos también estableciendo un manejador para el evento onerror. Al finalizar la carga de la página, estamos realizando una petición inicial y dejamos programada una nueva petición en intervalos de 10 segundos. El método postMessage requiere que le sea pasado un argumento, entonces estamos enviando un texto muy sencillo y que realmente en este ejercicio no tiene la mayor importancia porque de él no depende alguna ejecución por parte del Worker.

Finalmente vamos a programar nuestro GetRandom.ashx con el siguiente código:
public class GetRandom : IHttpHandler { public void ProcessRequest(HttpContext context) { context.Response.ContentType = "application/json"; context.Response.Write("{\"value\": \"" + Guid.NewGuid().ToString() + "\"}"); } public bool IsReusable { get { return false; } } }

Veamos que el ContentType de la respuesta está establecido en el MIME que necesita JSON, también deberemos notar que estamos generando un valor estadísticamente único con el objeto Guid.

Voy a visitar el archivo index.htm con IE 11. Esto es debido a que quiero hacer notar algo en particular con este navegador:

Ejecución con Internet Explorer 11

Podemos esperar más tiempo pero veremos que el valor único no es actualizado, esto se debe al caché del navegador. ¿Cómo podemos probarlo?, vamos a abrir las herramientas de desarrollo presionando F12 y activando la captura de red:

Captura del tráfico de red con la herramienta para desarrolladores de Internet Explorer 11

¿Qué está pasando?, podemos ver que el status es 304, esto significa que la petición está siendo servida desde el caché del navegador y no está yendo hasta el servidor por el nuevo valor. Para corregir este detalle vamos a ajustar el código de GetRandom.ashx de la siguiente manera:
context.Response.ContentType = "application/json"; context.Response.AppendHeader("Cache-Control", "no-cache, no-store, must-revalidate"); context.Response.AppendHeader("Pragma", "no-cache"); context.Response.AppendHeader("Expires", "0"); context.Response.Write("{\"value\": \"" + Guid.NewGuid().ToString() + "\"}");

Una vez realizado el cambio volvemos a visitar nuestro index.htm y veremos que cada 10 segundos el valor será actualizado.

Este ha sido un ejercicio muy sencillo pero aborda varias cosas que vale la pena notar:

  1. No tenemos acceso al DOM desde un Worker.
  2. No podemos utilizar jQuery en un Worker dado que jQuery está basado en el DOM.
  3. La comunicación entre el Worker y la página (en nuestro caso tools.js) debe ser a través de mensajes de texto. Es por ello que se recomienda el uso de JSON.
  4. Deshabilitamos el caché en nuestro Handler para evitar que el navegador no realice la petición al servidor al despachar el resultado desde el caché.

Espero les haya resultado de mucha utilidad, nos vemos la siguiente.

miércoles, 4 de diciembre de 2013

Variables privadas en javascript



En esta entrada quiero abordar el tema de la implementación de variables privadas en un objeto javascript. Seguramente para muchas personas resultará muy básico, pero en proyectos que he participado y en los cursos que imparto me he encontrado que no se conoce esta característica de los objetos implementados en javascript.

Comenzaremos con un objeto sumamente sencillo y plantearemos algunas interrogantes que resultarán interesantes:
function Persona(nombre, apellido, edad) { this.Nombre = nombre; this.Apellido = apellido; this.Edad = edad; }

Notemos que se está declarando un objeto Persona que tendrá tres propiedades públicas: Nombre, Apellido y Edad. Podemos perfectamente validar la información que estamos recibiendo antes de asignarla a nuestras propiedades de la siguiente manera:
function Persona(nombre, apellido, edad) { this.Nombre = nombre; this.Apellido = apellido; if (!isNaN(edad) && Number(edad) > 18) { this.Edad = edad; } else { this.Edad = null; } } var prueba = new Persona('Pepe', 'Pérez', 30); if (prueba.Edad) alert('La edad es: ' + String(prueba.Edad)); else alert('La edad no fue asignada');

Si primero lo probamos con 30 veremos que la edad es correctamente asignada, pero si le asignamos un valor menor o igual a 18 o bien una palabra, veremos que la propiedad Edad tendrá un valor null.

Hasta ahora todo va bien, pero, ¿qué pasa si una vez que es construido el objeto le asignamos un valor no válido a la propiedad pública Edad?:
var prueba = new Persona('Pepe', 'Pérez', 'Hola'); if (prueba.Edad) alert('La edad es: ' + String(prueba.Edad)); else { alert('La edad no fue asignada'); prueba.Edad = 'Hola'; alert('La edad es: ' + prueba.Edad); }

Veremos que el valor 'Hola' habrá sido asignado a Edad sin problema alguno.

Para evitar este problema y validar la información antes de que ésta sea actualizada en nuestros objetos, podemos implementar miembros privados expuestos a través de funciones públicas. En este caso lo haremos con la Edad:
function Persona(nombre, apellido, edad) { this.Nombre = nombre; this.Apellido = apellido; var m_Edad = null; if (!isNaN(edad) && Number(edad) > 18) { m_Edad = edad; } this.getEdad = function () { return m_Edad; } this.setEdad = function (e) { if (!isNaN(edad) && Number(edad) > 18) { m_Edad = edad; } } }

Veamos que ya no existe la propiedad pública Edad, ahora tenemos un miembro privado llamado m_Edad el cual es leído a través de la función getEdad y actualizado a través de la función setEdad. Dentro de la función setEdad estamos validando que sea una edad válida, una vez que pasó nuestra validación, procedemos a asignar el valor. Probemos con el código que habíamos escrito antes con unas pequeñas adecuaciones para utilizar las nuevas funciones:
var prueba = new Persona('Pepe', 'Pérez', 'Hola'); if (prueba.getEdad()) alert('La edad es: ' + String(prueba.getEdad())); else { alert('La edad no fue asignada'); prueba.setEdad('Hola'); alert('La edad es: ' + prueba.getEdad()); }

Veremos que el segundo mensaje mostrará 'La edad es: null', esto se debe a que la edad 'Hola' no fue asignada a nuestro miembro privado m_Edad debido a que no era un valor válido.

Espero te haya resultado de utilidad esta pequeña entrada, nos vemos la siguiente.

martes, 3 de diciembre de 2013

Índices y memoria



Habitualmente se piensa que el resultado positivo de un índice sólo se traduce en una lectura más rápida de datos. En esta entrada demostraré que también nos ayudan a reducir el uso de memoria al cargar menos páginas en el buffer de SQL Server.

Recordemos que SQL Server coloca en memoria los datos antes de poder despacharlos, esto se hace de esa forma para que la futura lectura de los datos sea mucho más rápida y no tenga que ir hasta los dispositivos de almacenamiento para recuperar la información.

Quiero hacer un ejercicio muy sencillo pero significativo con la base de datos AdventureWorks. El objetivo es obtener el total de ventas de los productos de color rojo. Analizaremos la cantidad de páginas que se cargan en memoria para poder despachar el resultado.

Nuestro query queda de la siguiente manera:
SELECT SUM(SOD.LineTotal) AS Total FROM Sales.SalesOrderDetail SOD INNER JOIN Production.Product P ON SOD.ProductID = P.ProductID WHERE P.Color = 'Red'

Para obtener la cantidad de páginas que está utilizando nuestra base de datos AdventureWorks en memoria, podemos conectarnos a ella y ejecutar el siguiente query:
SELECT COUNT(*) AS paginas_usadas FROM sys.dm_os_buffer_descriptors WHERE database_id = DB_ID()

Comenzaremos limpiando la memoria y verificando que no se estén usando páginas por parte de la base de datos AdventureWorks:
USE AdventureWorks GO CHECKPOINT DBCC DROPCLEANBUFFERS GO SELECT COUNT(*) AS paginas_usadas FROM sys.dm_os_buffer_descriptors WHERE database_id = DB_ID()

Vamos a ejecutar el query que obtiene el total de ventas de los productos de color rojo y verificamos de nueva cuenta la cantidad de páginas utilizadas por la base de datos AdventureWorks:
SELECT SUM(SOD.LineTotal) AS Total FROM Sales.SalesOrderDetail SOD INNER JOIN Production.Product P ON SOD.ProductID = P.ProductID WHERE P.Color = 'Red' GO SELECT COUNT(*) AS paginas_usadas FROM sys.dm_os_buffer_descriptors WHERE database_id = DB_ID()
En mi caso la cantidad de páginas usadas es de 772

Ahora vamos a crear un índice que nos ayude a optimizar el query, éste será creado sobre la columna ProductID e incluirá las columnas UnitPrice, UnitPriceDiscount y OrderQty las cuales son utilizadas para calcular LineTotal. Nuestro código queda de la siguiente manera:
CREATE INDEX ixSProductID ON Sales.SalesOrderDetail (ProductId) INCLUDE (UnitPrice, UnitPriceDiscount, OrderQty)

Volvemos a limpiar la memoria, ejecutamos nuestro query y verificamos la cantidad de páginas utilizadas por la base de datos AdventureWorks:
CHECKPOINT DBCC DROPCLEANBUFFERS GO SELECT SUM(SOD.LineTotal) AS Total FROM Sales.SalesOrderDetail SOD INNER JOIN Production.Product P ON SOD.ProductID = P.ProductID WHERE P.Color = 'Red' GO SELECT COUNT(*) AS paginas_usadas FROM sys.dm_os_buffer_descriptors WHERE database_id = DB_ID()
En mi caso la cantidad de páginas usadas es de 251

Notemos que la memoria necesaria para despachar el query se redujo en un 67.48%. ¿Qué debemos notar?, que los índices no sólo reducen el tiempo que tarda un query en ejecutarse sino también la cantidad de páginas que SQL Server necesita colocar en memoria para despachar el resultado.

Espero les haya resultado de utilidad esta entrada, nos vemos en la siguente.