Por Jesús López Méndez (SqlRanger) Gestión de concurrencia en ADO.NET La concurrencia, en un entorno multiusuario, es siempre una cuestión problemática, pero si además se trata de un entorno desconectado como el que se usa en ADO.NET con sus DataSets y DataAdapters, la problemática es aún mayor debido a la propia naturaleza desconectada del entorno. en primer lugar, cual es esa problemática y << Veamos,posteriormente qué alternativas tenemos para tratarla. El problema fundamental que se nos plantea son los conflictos de concurrencia. Un conflicto de concurrencia se produce cuando un usuario modifica un registro de una tabla de una base de datos y ese registro ha cambiado desde la última vez que lo leyó. Por ejemplo, consideremos la siguiente secuencia de sucesos: • Los usuarios A y B leen el registro R1 de la base de datos cargándolo en un DataSet. • El usuario A modifica R1 • El usuario A guarda R1 en la base de datos. • El usuario B modifica R1 • El usuario B guarda R1 en la base de datos. Los conflictos de concurrencia no se producen solamente al actualizar un registro porque otro usuario lo haya modificado, también ocurren si el registro ha sido eliminado por otro usuario. Asimismo, tienen lugar cuando un usuario intenta eliminar un registro que ha sido modificado e incluso que ha sido eliminado. Con la inserción, sin embargo, es evidente que no se producen conflictos de concurrencia, ya que es imposible que otro usuario pueda modificar un registro que aún no existe en la base de datos. En definitiva, los conflictos de concurrencia pueden producirse: • Al modificar un registro • Al eliminar un registro Y la causa del conflicto puede ser porque dicho registro: <<dotNetManía • Ha sido modificado desde la última vez que se leyó. • Ha sido eliminado desde la última vez que se leyó. 20 • El usuario B recibe una excepción DBConcurrencyException, indicando un conflicto de concurrencia al haber sido modificado R1 desde la última vez que B lo leyó. Otro aspecto básico acerca de los conflictos de concurrencia es la forma de detectarlos. La técnica de detección se basa fundamentalmente en incluir en la cláusula WHERE de la instrucción UPDATE o DELETE el valor original de los campos, es decir, el valor que tenían los campos del registro cuando se leyeron de la base de datos. Pongamos un ejemplo para aclarar ideas. << dnm.plataforma.ado.net pos en la cláusula SET, excepto el IdEmpleado que es autonumérico y por tanto de sólo lectura. Esto viene a CREATE TABLE Empleados ( IdEmpleado INT IDENTITY(1,1) PRIMARY KEY, DNI VARCHAR(12) NOT NULL UNIQUE, Nombre VARCHAR(50) NOT NULL, Apellidos VARCHAR(50) NOT NULL ) El comando UPDATE que detecta conflictos de concurrencia sería el siguiente: suponer que se actualizarán todos los campos en la tabla, independientemente de si se han modificado o no, UPDATE Empleados SET DNI=@DNI, Nombre=@Nombre, Apellidos=@Apellidos WHERE IdEmpleado=@Original_IdEmpleado AND DNI=@Original_DNI AND Nombre=@Original_Nombre AND Apellidos=@Original_Apellidos Como veis, están todos los valores originales en la cláusula WHERE de esta instrucción parametrizada. De esta manera, si ha cambiado alguno de los campos, no se cumplirá la condición, y por tanto la instrucción no actualizará ningún registro, o lo que es lo mismo, el número de registros afectados será cero. Solamente la instrucción tendrá éxito, o sea, actualizará el registro, si éste no ha cambiado. Así es como ADO.NET detecta los conflictos de concurrencia, concretamente un DataAdapter lanzará una excepción DBConcurrencyException cuando el comando de actualización afecte a cero registros. Observad que este comando incluye todos los cam- lo que implica una falta de optimización. Sin embargo, es posible que no nos interese detectar conflictos de concurrencia y que queramos que la actualización se lleve a cabo independientemente de si el registro ha sido modificado o no desde la última vez que se leyó. En ese caso sólo incluiríamos la clave primaria en la cláusula WHERE. La instrucción UPDATE sería la siguiente: Aún así, podríamos obtener un conflicto de concurrencia, pero sólo en el caso de que haya sido eliminado el registro. Un inconveniente de esta opción es que es posible perder modificaciones. Si por ejemplo, los usuarios A y B leen el empleado 10, el usuario A modifica su nombre y lo guarda, y luego B modifica el apellido y lo guarda, las dos actualizaciones tienen éxito, pero la modificación que hizo A se pierde, ya que el nombre es sobrescrito con el valor que leyó B. En ciertos sistemas, esta posible pérdida de modificaciones es inaceptable y por tanto habría que elegir otra opción. Otra alternativa sería incluir en la cláusula SET sólo los campos que se han modificado. De esta manera, aunque sólo incluyéramos la clave primaria en la cláusula WHERE, no se perderían las modificaciones. También es una opción interesante incluir en la cláusula SET sólo los campos modificados, y en la cláusula WHERE la clave primaria más el valor original de los campos que han cambiado, así el conflicto de concurrencia que detectaríamos sería en el caso de que otro usuario hubiera modificado alguno de los campos que han sido modificados o en el caso de eliminación. Por ejemplo, si los usuarios A y el B leen el empleado 10, el usuario A modifica su UPDATE Empleados SET DNI=@DNI, Nombre=@Nombre, Apellidos=@Apellidos WHERE IdEmpleado=@Original_IdEmpleado Los conflictos de concurrencia no se producen solamente al actualizar un registro porque otro usuario lo haya modificado, también ocurren si el registro ha sido eliminado por otro usuario nombre y lo guarda y el usuario B modifica su nombre y lo guarda, el usuario B recibe un conflicto de concurrencia. Pero si lo que ocurre es que el usuario A modifica el nombre y el B el apellido, no hay conflicto de concurrencia y las dos actualizaciones tienen éxito. Una última alternativa para la cláusula WHERE es incluir la clave primaria más el valor original de un campo de tipo TimeStamp que por supuesto tendría que formar parte de la tabla. El funcionamiento es equivalente a incluir los valores originales de todos <<dotNetManía Supongamos que estamos trabajando con la siguiente tabla en una base de datos de SQL Server: 21 << dnm.plataforma.ado.net los campos, pero resulta más eficiente ya que la instrucción es más corta, reduciéndose el tráfico de red y reduciendo el trabajo del procesador de consultas del servidor de base de datos. Un campo de tipo TimeStamp en SQL Server es una especie de autonumérico de 64 bits único en toda la base de datos. No puede haber dos registros en una base de <<dotNetManía La técnica de detección se basa fundamentalmente en incluir en la cláusula WHERE de la instrucción UPDATE o DELETE el valor original de los campos 22 datos con el mismo valor de TimeStamp, incluso aunque pertenezcan a distintas tablas. Cada vez que se modifica un registro que tiene un campo TimeStamp, el valor del campo también cambia. Debido a esto y si usamos esta alternativa, después de modificar un registro, sería necesario volver a leer el campo TimeStamp para poder realizar más modificaciones en el mismo registro. Detectar conflictos de concurrencia en la eliminación es similar a la actualización, con la salvedad de que en este caso sólo podemos jugar con la cláusula WHERE de la instrucción DELETE. Podríamos incluir sólo la clave primaria, en cuyo caso sólo obtendremos conflictos de concurrencia cuando otro usuario haya eliminado el registro. En la mayoría de los casos, este conflicto sencillamente lo podríamos ignorar. También podríamos incluir todos los valores originales de los campos o la clave primaria más el TimeStamp, en cuyo caso recibiremos un conflicto de concurrencia cuando otro usuario haya modificado o eliminado el registro. Una vez que tenemos decidido cómo vamos a detectar los conflictos de concurrencia y cómo vamos a hacer las actualizaciones y eliminaciones, hemos de decidir cómo los vamos a tratar, o sea, qué acciones vamos a tomar en el caso de un conflicto de concurrencia. Cada conflicto de concurrencia lo trataremos de manera diferente en función de si se ha producido al hacer una actualización o al realizar una eliminación y en función de la causa del conflicto, esto es, si ha sido porque otro usuario lo ha modificado o porque lo ha eliminado. Empecemos primero por los conflictos que se producen al actualizar. Si la causa es que otro usuario lo ha modificado podríamos tener las siguientes alternativas: • Descartar las modificaciones y refrescar el registro volviéndolo a leer de la base de datos. Al usuario le avisaríamos del conflicto de concurrencia y le daríamos la oportunidad de volver a hacer las modificaciones. • Refrescar sólo los valores originales sin descartar las modificaciones. Al usuario le avisaríamos del conflicto. Entonces él tendría la oportunidad de ver las modificaciones deshaciendo cambios o de volver a guardar con lo que forzaría la actualización. • Directamente forzar la actualización. Esto se conoce como la técnica “el último que llega gana”. En realidad esta acción no es una respuesta a un conflicto de concurrencia, ya que para llevarla a cabo incluiríamos únicamente la clave primaria en la cláusula WHERE, no detectándose conflictos de concurrencia por modificación. Si la causa es que otro usuario lo ha eliminado, las alternativas serían las siguientes: • Volver a insertar el registro en la base de datos. En el caso de que tengamos un autonumérico en la tabla no sería posible volver a insertar el registro exactamente igual a como era anteriormente. • Eliminarlo del DataSet. Esta es la opción que más se suele utilizar. Detectar la causa del conflicto, al igual que refrescar un registro, puede realizarse volviendo a leer tal registro de la base de datos basándose en la clave primaria, pero si la clave primaria puede cambiar, esta técnica no sirve para su propósito ya que si ésta ha cambiado no es posible identificar el registro y no es posible determinar si el conflicto de concurrencia ha ocurrido por modificación o por eliminación. Por eso sería recomendable usar claves primarias artificiales como autonuméricos o GUID’s. En cuanto a los conflictos de concurrencia que se producen al eliminar un registro, podríamos tener las siguientes alternativas cuando la causa es por modificación: • Deshacer la eliminación y refrescar el registro. Al usuario le informaríamos del conflicto y tendría la posibilidad de volverlo a eliminar después de haber visto los cambios realizados. << dnm.plataforma.ado.net • Forzar la eliminación. En realidad esta acción no es una respuesta a un conflicto de concurrencia, ya que para llevarla a cabo incluiríamos únicamente la clave primaria en la cláusula WHERE con lo que no se detectan conflictos de concurrencia por modificación. Figura1. Opciones avanzadas de generación de instrucciones SQL. del valor actual o del valor original. Antes de poder invocar al método Update de un DataAdapter tenemos que configurarlo correctamente, esto es, tenemos que establecerle los comandos de actualización. Para configurar un DataAdapter tenemos tres alternativas: • Usar el asistente para la configuración del DataAdapter • Usar un CommandBuilder • Configurarlo manualmente escribiendo nosotros mismos el código Para usar el asistente, sólo tenemos que arrastrar un DataAdapter de la ficha datos del cuadro de herramientas a nuestro formulario o componente y seguir sus instrucciones. En el paso “Generar las instrucciones SQL” tenemos un botón “Opciones avanzadas” que nos pre- senta el cuadro de diálogo de la figura 1. Como vemos, el asistente puede generar por nosotros los comandos de actualización INSERT, UPDATE y DELETE. Si elegimos “Usar concurrencia optimista”, el asistente incluirá en la cláusula WHERE de las instrucciones UPDATE y DELETE el valor original de todos los campos del registro. Mientras que si no activamos esa casilla de verificación, la cláusula WHERE sólo incluirá la clave primaria. Si elegimos “Actualizar el conjunto de datos” el asistente añade una instrucción SELECT a los comandos de actualización para refrescar el registro. En cualquier caso, la instrucción UPDATE incluye en la cláusula SET todos los campos. Esta sería la instrucción UPDATE para nuestra tabla de ejemplo usando concurrencia optimista. Ver tabla 1. UPDATE Empleados SET DNI=@DNI, Nombre=@Nombre, Apellidos=@Apellidos WHERE IdEmpleado=@Original_IdEmpleado AND DNI=@Original_DNI AND Nombre=@Original_Nombre AND Apellidos=@Original_Apellidos Tabla 1 <<dotNetManía Por último, el conflicto de concurrencia que se produce al eliminar un registro que ha sido eliminado, generalmente puede tratarse sencillamente ignorando el conflicto y eliminando definitivamente el registro del DataSet. Como hemos visto, existen varias alternativas para detectar y tratar los conflictos de concurrencia. Veamos ahora qué nos ofrece ADO.NET en este sentido. En ADO.NET tenemos una serie de clases, los DataAdapters, que son los encargados de revertir las modificaciones realizadas en un DataSet sobre la base de datos mediante su método Update. Los DataAdapters tienen tres propiedades: DeleteCommand, UpdateCommand e InsertCommand que son los comandos de actualización. Estos comandos son parametrizados, de manera que sirvan para todas las filas de un DataTable. Cuando invocamos al método Update de un DataAdapter, éste recorre todas las filas del DataTable, y si la fila es una fila eliminada, ejecuta el DeleteCommand; si la fila es una fila modificada, invoca el UpdateCommand; y si es una fila nueva, invoca al InsertCommand. Si al invocar al UpdateCommand o al DeleteCommand, el número de registros afectados es cero, el DataAdapter lanza una excepción DBConcurrencyException indicando que se ha producido un conflicto de concurrencia. Antes de invocar un comando de actualización, el DataAdapter establece el valor de los parámetros del comando con los valores originales o actuales de los campos de la fila basándose en la configuración del propio comando. Cada parámetro de la colección Parameters de un comando tiene la propiedad SourceColumn que indica el nombre del campo cuyo valor deberá copiarse al parámetro, y la propiedad SourceVersion que indica si se trata 23 << dnm.plataforma.ado.net Y esta sería la instrucción UPDATE sin usar la concurrencia optimista: ciones, que sea capaz de gestionar los conflictos de concurrencia y que disponga de todas las opciones UPDATE Empleados SET DNI=@DNI, Nombre=@Nombre, Apellidos=@Apellidos WHERE IdEmpleado=@Original_IdEmpleado Como hemos dicho anteriormente, también podemos usar un CommandBuilder. Este sería el código a usar para nuestra tabla de ejemplo: mencionadas en este artículo. Podéis encontrar un DataAdapter para SQL Server (SqlRanger.SqlAdapter) escrito por mí en la web de la revista o en mi propia página web: http://sqlranger.com/descargas.aspx. <<dotNetManía SqlDataAdapter Adapter = new SqlDataAdapter(“SELECT * FROM Empleados”, Connection); SqlCommandBuilder CommandBuilder = new SqlCommandBuilder(Adapter); Adapter.UpdateCommand = CommandBuilder.GetUpdateCommand(); Adapter.InsertCommand = CommandBuilder.GetInsertCommand(); Adapter.DeleteCommand = CommandBuilder.GetDeleteCommand(); 24 Las instrucciones UPDATE y DELETE serían equivalentes a las generadas por el asistente usando concurrencia optimista y sin actualizar el conjunto de datos. La alternativa de configurar manualmente el DataAdapter no es muy recomendable, ya que requiere escribir bastante código y la funcionalidad obtenida es exactamente igual a la conseguida usando el asistente. Además es posible que cometamos algún error al escribir el código, mientras que el asistente no los comete. Como vemos, el DataAdapter sólo nos deja la posibilidad de incluir en la cláusula WHERE de las instrucciones UPDATE y DELETE o bien la clave primaria, o bien todos los campos. No tenemos las otras alternativas que se mencionan en este artículo. Además en la cláusula SET de la instrucción UPDATE, sólo podemos incluir todos los campos, no tenemos la opción de incluir sólo los modificados. Por otra parte, ADO.NET sólo da soporte para la detección del conflicto de concurrencia, no hay nada que nos ayude a gestionarlo, por lo que tendremos que escribir nosotros mismos el código necesario. El código de ejemplo del fuente 1 muestra como gestionar conflictos de concurrencia, refrescando el registro si ha sido modificado y eliminándolo si ha sido eliminado. Como vemos, gestionar los conflictos de concurrencia no es trivial y repetir el mismo código una y otra vez para cada caso es muy laborioso y pesado. Una buena alternativa a los DataAdapters que vienen incluidos en .NET Framework, es escribir nuestro propio DataAdapter que no tenga estas limita- Este DataAdapter es completamente gratis y se incluye el código fuente así como un ejemplo de su uso. El SqlRanger.SqlAdapter tiene propiedades específicas para tratar la concurrencia. Entre las que se incluyen: • UpdateCriteria: Determina los campos a incluir en la cláusula WHERE de la instrucción UPDATE. Puede tomar los siguientes valores: • All: Se incluirán los valores originales de todos los campos. • Key: Se incluirá sólo la clave primaria. • Modified: Se incluirá la clave primaria más los valores originales de los campos modificados. • TimeStamp: Se incluirá la clave primaria más el valor original del campo TimeStamp si es que existe. • UpdateColumns: Determina qué campos aparecerán en la cláusula SET de la instrucción UPDATE. Puede tomar los siguientes valores: • All: Se incluyen todos los campos. • Modified: Se incluyen sólo los campos modificados. • DeleteCriteria: Determina los campos a incluir en la cláusula WHERE de la instrucción DELETE. Puede tomar los siguientes valores: • All: Se incluirán los valores originales de todos los campos. • Key: Se incluirá sólo la clave primaria. • TimeStamp: Se incluirá la clave primaria más el valor original del campo TimeStamp si es que existe. << dnm.plataforma.ado.net public void Guardar(DataTable Empleados) { // creamos un adapter para realizar la actualización SqlDataAdapter Adapter = new SqlDataAdapter(“SELECT * FROM Empleados”, this.cn); // usamos un command builder para configurar los comandos de actualización SqlCommandBuilder CommandBuilder = new SqlCommandBuilder(Adapter); Adapter.UpdateCommand = CommandBuilder.GetUpdateCommand(); Adapter.InsertCommand = CommandBuilder.GetInsertCommand(); Adapter.DeleteCommand = CommandBuilder.GetDeleteCommand(); // este comando nos sirve para refrescar un registro SqlCommand Resync = new SqlCommand(“SELECT * From Empleados WHERE IdEmpleado=@IdEmpleado”, this.cn); Resync.Parameters.Add(“@IdEmpleado”, SqlDbType.Int); try { Adapter.Update(Empleados); } catch ( DBConcurrencyException ex ) { // Nuestra respuesta a un conflicto va a ser refrescar el registro Adapter.SelectCommand = Resync; Resync.Parameters[“@IdEmpleado”].Value = ex.Row[“IdEmpleado”, DataRowVersion.Original]; // // if // { el método Fill buscará el registro en el DataTable por clave primaria (IdEmpleado) y lo “refrescará” ( Adapter.Fill(Empleados) == 0 ) la causa del conflicto es que ha sido eliminado (Fill devuelve cero registros) if ( ex.Row.RowState == DataRowState.Deleted ) { // en este punto tenemos un conflicto de concurrencia // al eliminar un registro porque ha sido eliminado. // Eliminamos definitivamente el registro ex.Row.AcceptChanges(); // ignoramos el conflicto y seguimos con la actualización Guardar(Empleados); } else { // en este punto tenemos un conflicto de concurrencia // al modificar un registro porque ha sido eliminado // Eliminamos el registro // y volvemos a lanzar la excepción ex.Row.Delete(); ex.Row.AcceptChanges(); throw ex; } } else { // la causa del conflicto es que ha sido modificado // Si el conflicto ha sido al eliminar el registro // Fill ya lo habrá “recuperado” y refrescado. // Aparecerá el registro con el error // Si el conflicto ha sido al modificar el registro // Fill lo habrá “refrescado”. Y aparecerá el registro // con el error // sólo hay que volver a lanzar la excepción throw ex; } Fuente 1. Ejemplo de gestión de conflictos de concurrencia <<dotNetManía } } 25 << dnm.plataforma.ado.net • ConflictUpdatingChangedAction: Determina la acción a realizar en caso de un conflicto de concurrencia al actualizar un registro porque haya sido modificado desde la última vez que se leyó. Puede tomar los siguientes valores: • NoAction: No hace nada. • ResyncAllValues: Refresca todos los valores del registro, volviéndolo a leer de la base de datos. • ResyncOriginalValues: Refresca los valores originales del registro, leyéndolo de la base de datos. • ConflictUpdatingDeletedAction: Determina la acción a realizar en caso de un conflicto de concurrencia producido al actualizar un registro porque haya sido eliminado desde la última vez que se leyó. Puede tomar los siguientes valores: ¿Qué es qué? ¿Qué • Delete: Elimina definitivamente el registro del DataSet. • Insert: Vuelve a insertar el registro en la base de datos. • NoAction: No hace nada. • ConflictDeletingChangedAction: Determina la acción a realizar en caso de un conflicto de concurrencia producido al eliminar un registro porque haya sido modificado desde la última vez que se leyó. Puede tomar los siguientes valores: • NoAction: No hace nada. • ResyncAllValues: Refresca el registro, volviéndolo a leer de la base de datos. • ResyncCommand: Comando parametrizado basado en clave primaria utilizado para refrescar un registro. Whitehorse es el nombre en clave del software que se incluirá en Visual Studio 2005 y que aporta herramientas de diseño model-driven dirigida a los arquitectos de software, enlazando el modelo conceptual al código. Tendremos más información en el devdays que se celebrará en San Diego, California entre el 23 y el 28 de Mayo (http://www.microsoft.com/seminar/teched2004). En Europa se celebrará en Ámsterdam, Holanda, entre el 29 de Junio y el 2 de Julio (http://www.microsoft.com/europe/teched). Entretanto puede descargarse un video demostrativo de la web de MSDNTV en http://msdn.microsoft.com/msdntv ¿Qué es Laguna? En el Microsoft Mobile DevCon Conference 2004 celebrado en San Francisco entre el 23 y el 27 de marzo se habló de “Laguna”, nombre en clave del SQL Server CE 3.0. Esta versión se verá retrasada igual que la versión completa, el SQL Server 2005. Según nuestras noticias, ambas versiones saldrán juntas, si bien la versión beta 1 de Laguna estará disponible cuando esté la beta 2 de Yukon. La web del Mobile DevCon Conference 2004 está en: http://www.microsoftmdc.com. Puede ver información de la versión actual de SQL Server CE 2.0 en: http://www.microsoft.com/sql/ce. ¿Qué es Indy? <<dotNetManía Conclusión La concurrencia es un tema problemático en ADO.NET dada su naturaleza desconectada. Existen varias opciones para detectar y tratar los conflictos de concurrencia. Cada una de estas opciones tiene sus ventajas e inconvenientes y es necesario elegir cuidadosamente la más adecuada para el sistema en cuestión. ADO.NET da soporte limitado para la gestión de la concurrencia, siendo una buena alternativa escribir nuestro propio DataAdapter para superar las limitaciones. es qué? ¿Qué es Whitehorse? 26 El SqlRanger.SqlAdapter genera automáticamente los comandos de actualización y el ResyncCommand, no siendo necesario proporcionárselos. Para ello hace uso del SqlRanger.CommandBuilder. Indy es el nombre en clave de una nueva herramienta de gestión desarrollada por Microsoft Research y que se comercializará por la división Enterprise Management de Microsoft. Simula un centro de datos empresarial derivado del modelo de hardware, software y los sistemas de servidores del cliente. Indy está inmerso en la versión 2.0 de la suite Microsoft System Center para la que aún no hay fecha prevista de salida, ni tan siquiera una aproximación. La versión actual, la 1.0 llamada System Center 2005 es la primera suite de gestión integrada para el Windows Server System e incluye el System Management Server 2003, Microsoft Operations Manager 2005 y el nuevo sistema común de reporting. Se habló de él en el Summit celebrado en Las Vegas el pasado mes de marzo. La web del Summit 2004: http://www2.mms2004.com. Más información en Microsoft Watch: http://www.microsoft-watch.com y en el sito Betanews http://www.betanews.com/article.php3?sid=1079576470 ¿Qué es Lonestar? Aparte de un mítico grupo de rock catalán de los años 70, Lonestar es el nombre en clave de la próxima versión del sistema operativo de Microsoft para Tablet PC. Si bien se iba a vender como un add-on para los usuarios de Tablet PC, finalmente será incluido dentro de Windows XP SP2. Tendrá un nuevo SDK para desarrolladores e integración con Office 2003. ¿Qué es Windows XP Reloaded? Windows XP Reloaded es el nombre en clave para la versión de Windows XP que hará de puente entre la actual y Longhorn. ¿Qué es Symphony y Harmony? Symphony es el nombre en clave de la próxima versión de Windows XP Media Center Edition el cual está basado en XP SP2. Una versión previa a Windows XP Media Center Edition 2004. Harmony es el nombre en clave del próximo Windows XP Media Center Edition 2004. Incluirá soporte para High Definition Televisión, soporte para múltiples sintonizadores, soporte para diferentes formatos de grabación de vídeo y radio. noticias.noticias.noticias.noticias Suscripción a dotNetManía ❑ Deseo suscribirme a dotNetManía por un año (11 ejemplares) y beneficiarme de la oferta del 10% de descuento por un importe total de 60 € para España; o por 75 € para el resto de Europa; o por 90 € para el resto del mundo (IVA incluido). ❑ Deseo suscribirme a dotNetManía por un año (11 ejemplares) por un importe de 45 € por ser estudiante (IVA incluido). Aporto fotocopia del carné de estudiante o sello del centro académico (IMPRESCINDIBLE). OFERTA VÁLIDA SÓLO PARA ESTUDIANTES RESIDENTES EN ESPAÑA. IMPORTES VÁLIDOS HASTA NUEVA OFERTA DATOS DE FACTURACIÓN CIF/NIF . . . . . . . . . . . . . . . . . . . . .Empresa . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Nombre y apellidos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Dirección . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Población . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .Código Postal . . . . . . . . . . . . . . . . . . . Provincia . . . . . . . . . . . . . . . . . . . . . . . . . Teléfono . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Fax . . . . . . . . . . . . . . . . . . . . . . . . . . . email . . . . . . . . . . . . . . . . . . . . . . . . . . . . DATOS DE ENVÍO (sólo si son distintos de los datos de facturación) CIF/NIF . . . . . . . . . . . . . . . . . . . . .Empresa . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Nombre y apellidos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Dirección . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Población . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .Código Postal . . . . . . . . . . . . . . . . . . . Provincia . . . . . . . . . . . . . . . . . . . . . . . . . Teléfono . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Fax . . . . . . . . . . . . . . . . . . . . . . . . . . . email . . . . . . . . . . . . . . . . . . . . . . . . . . . . FORMA DE PAGO ❑ Talón nominativo a nombre NETALIA, S.L. ❑ Transferencia bancaria a nombre de NETALIA, S.L. a: La Caixa - Número de cuenta 2100 4315 48 2200014696 (Indique su nombre en la transferencia) ❑ Domiciliación Bancaria (con renovación automática, previo aviso) Indique su número de cuenta: ❑ Tarjeta de crédito ❑ VISA ❑ MASTERCARD Número de su tarjeta: Fecha de caducidad: / (imprescindible) Firma y/o sello (imprescindible) a ❑ Nº3 ❑ Nº4 ❑ Nº5 de ❑ Nº6 Usted autoriza a la mecanización de estos datos. El responsable y destinatario de éstos es Netalia, S.L. Usted tiene derecho a acceder a sus datos, modificarlos y cancelarlos cuando lo desee. Sus datos no serán cedidos en ninguna de las formas posibles a terceras partes y no se utilizarán más que para el buen funcionamiento de su suscripción a la revista dotNetManía y para informarle de las actividades comerciales que realice la editorial Netalia, S.L. Si no desea recibir información comercial de dotNetManía marque la casilla siguiente ❑ de 20 ❑ Nº7 ❑ Nº8 Si desea algún otro número indíquelo Puede enviar los datos al email [email protected], al FAX (34) 91 499 13 64 o al teléfono (34) 91 666 74 77. También puede enviarlo por correo postal a la siguiente dirección: C/ Robledal, 135 28529- Rivas Vaciamadrid Madrid (España) ❑ Nº9