Bases de datos con Delphi 1. Introducción Cuando queramos desarrollar una aplicación con Delphi que dé acceso a una base de datos, no debemos perder nunca este doble punto de vista: la aplicación será, por una parte, cliente de un servidor de base de datos y, por otra parte, será el interface de usuario de la base de datos, para todas las tablas o sólo para algunas (eso ya depende de la amplitud de la aplicación con respecto al total de los datos). Debemos centrarnos en un buen diseño de la aplicación, pues esto facilita al usuario la edición de datos, y a nosotros, como programadores, el mantenimiento, sea como sea de complejo el modelo de datos sobre el que trabajemos. Para ello, debemos conocer muy bien tanto el modelo de datos de la base de datos, como la filosofía de trabajo con bases de datos en Delphi. En este curso vamos a preocuparnos, lógicamente, de la filosofía de trabajo con Delphi. Delphi utiliza un intermediario para conectar con la base de datos: se llama BDE (Borland Database Engine). Puede conectar con diversos tipos de bases de datos de forma directa (Interbase, Paradox, Informix, ...) o bien a través de ODBC. Así pues, es el BDE el que realiza el trabajo duro de "hablar" con el servidor de base de datos, quedándonos a nosotros una tarea mucho más sencilla: poder olvidarnos de cómo es la estructura interna (a nivel de estructura de ficheros) de la base de datos, centrándonos en las tablas como idea y en el diseño de las consultas. Hay un concepto clave para todo esto: el concepto de alias. Se trata de un nombre lógico (que no tiene por qué coincidir con el nombre del fichero físico de la base de datos) con el que Delphi reconocerá a la base de datos. Así, creamos un alias para la base de datos, y desde Delphi no tendremos que preocuparnos de la ruta. Especificaremos estos datos previamente accediendo directamente al BDE Administrator desde el panel de control, o bien usando la utilidad SQL Explorer. En los anexos se detalla cómo hacerlo. Delphi separa en dos grupos los componentes de bases de datos: los componentes de acceso a datos (que se encuentran en la pestaña Data Access) y los componentes visuales que muestran datos (en la pestaña Data Controls). La filosofía es separar el contenido de la forma. Así, en los componentes visuales especificaremos cómo queremos que el usuario vea los datos, y en los componentes de acceso a datos programaremos cómo obtener los datos que necesitamos. Hay un componente muy específico que enlazará nuestro acceso a datos con nuestra visualización de datos, y que es el que se encargará de proveer al control visual de los datos que genera el control no visual de acceso a datos. 2.i. Descripción rápida de Data access Comenzamos dando una primera mirada a los componentes de la paleta Data access que trataremos en este curso. Todos estos componentes son no visuales y permiten que los datos lleguen a la aplicación. Paleta DATA ACCESS TDatabase Encapsula una conexión cliente/servidor a una única base de datos. A pesar de que si no incluimos un componente de este tipo, Delphi usa uno de forma temporal, será conveniente empezar dando a nuestra aplicación uno o más componentes de este tipo para poder añadir algunos parámetros adicionales. TTable Representa un conjunto de datos que recupera todas las columnas y registros de una tabla de la base de datos. Tiene propiedades y métodos que nos permiten movernos por el conjunto de datos y encontrar datos que cumplan un determinado patrón de búsqueda. TQuery Representa un conjunto de datos que recupera un subconjunto de columnas y registros de una o más tablas de bases de datos basadas en una consulta SQL. Al igual que TTable, tiene propiedades y métodos que nos permiten movernos entre los datos y escoger aquellos que cumplan ciertas características requeridas por la aplicación. TStoredProc Representa un conjunto de datos que recupera uno o más registros de una tabla de base de datos basándose en un procedimiento almacenado definido para un servidor de base de datos. Sólo tiene sentido usarlo cuando la base de datos soporta esta característica. TDataSource Actúa como un conducto entre otros componentes de acceso a datos y controles visuales de datos. Es el encargado de que los datos de componentes como TTable, TQuery y TStoredProc puedan mostrarse de forma cómoda para el usuario en los controles visuales de datos. La paleta Data access tiene algunos componentes más, pero con los que describiremos en este curso, hay más que suficiente para hacer aplicaciones medianas-grandes. 2.ii. Descripción rápida de Data controls Ahora le toca el turno a los componentes de visualización de datos: Paleta DATA CONTROLS TDBGrid Muestra y edita registros de conjunto de datos en formato de tabla. Puede ser la tabla completa o sólo algunas de las columnas. TDBNavigator Mueve el cursor a través de registros de un conjunto de datos, permite editar e insertar registros, almacena los registros modificados o nuevos, cancela el modo de edición y actualiza la visualización de datos. Permite una cómoda navegación por conjuntos grandes de registros. TDBText Muestra un campo como si fuera una etiqueta. TDBEdit Muestra y edita un campo en un cuadro de edición. TDBListBox Muestra una lista con opciones para introducir un campo. TDBComboBox Muestra un cuadro de edición y una lista desplegable de elecciones para editar y entrar en un campo. TDBCheckBox Muestra y configura una condición de campo booleana en un cuadro de comprobación. TDBRadioGroup Muestra y configura opciones únicas para un campo de un grupo de botones de radio. TDBLookupListBox Muestra una lista con opciones derivadas de un campo en otro conjunto de datos para entrar en un campo. TDBLookupComboBox Muestra un ComboBox de opciones derivada de un campo de otro conjunto de datos para introducirlos en un campo 3. Módulos de datos Antes de comenzar a detallar los componentes que usaremos, vamos a hablar de esta peculiar característica de Delphi. Un módulo de datos es una clase especial de Delphi que se usa para la gestión centralizada de componentes no visuales de una aplicación. Generalmente incluye componentes para el acceso a datos (como TDatabase, TTable, TQuery, ...), pero también puede incluir otros componentes no visuales (como TTimer, TOpenDialog, TImageList, etc). Un módulo de datos nos permite: • • • Mantener todos los componentes de acceso a datos en un solo contenedor visual en modo de diseño, en lugar de duplicarlos para cada form de la aplicación Diseñar tablas y consultas una sola vez y usarlas en varios forms, en lugar de crearlas por separado para cada form. Guardarlo en el Object Repository para poder reutilizarlo. La centralización de recursos para la aplicación quizá sea una de las características más interesantes. Por ejemplo, un cuadro de diálogo de apertura de ficheros es un recurso candidato a ser centralizado. Si tenemos una aplicación con 10 forms que necesiten de un cuadro de diálogo, tenemos dos opciones, crear 10 componentes, uno para cada form, o crear un único componente de cuadro de diálogo y usarlo en los 10 forms. El método de trabajo que usaremos será el de crear los componentes no visuales en un módulo de datos, y usarlo en todas las units que lo requieran. Así lo tendremos disponible para toda la aplicación sin tener que repetirlos. Además, alteraremos el orden de creación de forms en el proyecto, entrando en Auto-create forms de las opciones del proyecto, situando en primer lugar a nuestro módulo de datos. Haremos esto por lo siguiente: asociaremos al evento onCreate código para abrir la conexión con la base de datos, estando así disponible desde el principio de la aplicación y no después de crearse el form principal. Así, nos evitaremos problemas en el caso de que antes de que se muestre el form principal sea necesario realizar alguna operación con la base de datos. Los módulos de datos se encuentran en Delphi accediendo a New -> Data Module, dentro de la pestaña por defecto, New. Nuestra aplicación puede usar cuantos necesite. Puede ser buena idea, incluso, usar distintos módulos de datos, atendiendo a finalidades distintas, para poner en ellos los componentes no visuales, agrupados por esta finalidad. 4.i. TDatabase El componente TDatabase nos va a permitir: • • • • Crear conexiones persistentes a bases de datos (esto es, permaneceremos conectados a la base de datos siempre que el componente tenga especificado que debe estar conectado). Personalizar el acceso a servidores de bases de datos. Por ejemplo, solicitar o no una contraseña de acceso al conectar con la base de datos. Controlar las transacciones y especificar sus niveles de aislamiento (aunque en este curso no vamos a hablar de transacciones). Crear alias BDE locales de aplicaciones. Esto es especialmente útil por lo siguiente: supongamos una aplicación que debe funcionar con una base de datos en una determinada ruta. Todos los componentes de acceso a datos que necesite la aplicación, deben estar ligados a esa base de datos vía el alias. Si, por algún motivo, cambia el alias, entonces tendríamos que cambiarlo a todos los componentes de acceso a datos. Teniendo definido un alias local, asociaremos a los componentes de acceso a datos el alias local, y así, en caso de que haya que cambiar algo, sólo tendremos que hacerlo en el componente TDatabase. Vamos a estudiar las propiedades más importantes de este componente: AliasName El valor de esta propiedad será el alias BDE asociado. Delphi nos presentará un menú desplegable con los alias definidos en el sistema. De ahí será de donde tendremos que escoger uno. Si damos un valor a la propiedad DriverName entonces se elimina el valor dado a esta propiedad. DriverName Identifica al controlador BDE del componente (esto forma parte del alias). Es autoexcluyente con AliasName, como hemos visto. Connected Es un booleano que nos dice si estamos conectados o no a la base de datos. DatabaseName Es el alias local a la aplicación que damos al componente. No es necesario, pero sí recomendable darle uno. LoginPrompt Es un booleano que nos dice si se debe solicitar o no un nombre de usuario y su contraseña la primera vez que se vaya a acceder a la base de datos. Si le damos el valor False tendremos que especificar estos valores en la propiedad Params Params Se trata de una lista con los parámetros de acceso para la conexión. Params En el inspector de objetos, al situarnos sobre la propiedad Params, podemos hacer doble click con el ratón, con lo que se abre la siguiente ventana: En ella escribiremos lo siguiente: USER NAME=ElNombreDelUsuario PASSWORD=LaClaveDelUsuario cuando pongamos a False el valor de la propiedad LoginPrompt Resumiendo: Cuando vayamos a realizar una aplicación de base de datos, comenzaremos creando un form de módulo de datos (un DataModule) en el que incluiremos el componente TDatabase. Pondremos los valores oportunos en sus propiedades, y le daremos un valor a la propiedad DatabaseName para tener definido un alias local. Veremos en los siguientes capítulos la utilidad de ese alias local. Para poder asociar a los componentes de acceso a datos este alias local, tendremos que estar conectados a la base de datos. En tiempo de diseño podremos conseguirlo poniendo el valor de la propiedad Connected a True. En tiempo de ejecución también podemos acudir al método Open del componente. Será conveniente protegerlo con un bloque try .. except por si se produjera algún problema en la apertura de la base de datos. 4.ii. TDataSet Este componente es el que encapsula los datos de una base de datos. No debemos usarlo directamente en nuestras aplicaciones. Para ello, tenemos a sus descendientes, los componentes de acceso a datos TTable, TQuery y TStoredProc. Sin embargo, debemos conocer bien este componente, pues es el que nos prové de propiedades y métodos comunes a sus descendientes, fundamentales para el acceso a datos. Además, cada conjunto de datos debe tener asociado su componente TDataSource correspondiente, que es el que vincula los controles de datos visuales con los conjuntos de datos. El componente de fuente de datos es el que se encarga de canalizar los datos desde el componente de acceso a datos al componente visual. Así que vamos a comentar las características interesantes que nos ofrece este componente (recordemos que serán comunes a sus descendientes): En primer lugar, tenemos que saber cómo abrir y cerrar conjuntos de datos. Mientras un conjunto de datos esté cerrado, no podremos extraer datos de él. Esto se traduce en que los componentes visuales que estén asociados a él (vía un TDataSource) no mostrarán datos. Así, cuando esté abierto, sí se podrán ver los datos, e igualmente se podrá recorrer las distintas filas que pueda contener. Por tanto, para abrir el conjunto de datos tenemos dos posibilidades que son equivalentes: CompDescDeTDataSet.Active := True; { diseño y ejecución } CompDescDeTDataSet.Open; { ejecución } Y para cerrarlo tenemos otras dos, igualmente equivalentes: CompDescDeTDataSet.Active := False; { diseño y ejecución } CompDescDeTDataSet.Close; { ejecución } Si hay que modificar alguna propiedad del conjunto de datos que afecte a la consulta, es necesario cerrar ANTES dicho conjunto. Vamos a ver ahora cómo nos desplazamos por conjuntos de datos: Cada conjunto de datos tiene un cursor (es decir, un puntero que apunta a la fila ACTUAL del conjunto). Dicha fila corresponde a los valores que pueden manipularse vía métodos de edición/inserción/borrado, y cuyos valores de campo muestran actualmente controles de dato monocampo, como TDBEdit o TDBText. Disponemos de los siguientes métodos para desplazarnos por conjuntos de datos: First Desplaza el cursor a la primera fila del conjunto de datos Last Desplaza el cursor a la última fila del conjunto de datos Next Desplaza el cursor a la fila siguiente del conjunto de datos Prior Desplaza el cursor a la fila anterior del conjunto de datos MoveBy(NumFilas: Integer) Desplaza el cursor un número dado de filas del conjunto de datos, hacia delante si NumFilas es positivo, y hacia atrás si NumFilas es negativo El objeto TDataSet cuenta además con las propiedades: BOF (beginning of file) Es TRUE si: • • • • Se Se Se Se abre el conjunto de datos llama al método First del conjunto de datos llama a Prior y falla (porque el cursor ya está en la primera fila) llama a SetRange en un rango o conjunto de datos vacío (no veremos SetRange) Es FALSE en todos los demás casos. EOF (end of file) Es TRUE si: • • • • Se Se Se Se abre un conjunto de datos vacío llama al método Last del conjunto de datos llama a Next y falla (porque el cursor ya está en la última fila) llama a SetRange en un rango o conjunto de datos vacío (no veremos SetRange) Es FALSE en todos los demás casos. Marcadores Un marcador es un objeto que nos va a permitir marcar una posición concreta del conjunto de datos, de manera que si nos movemos después por dicho conjunto, podamos más adelante volver a esta posición marcada. Para ello, Delphi cuenta con tres métodos marcador que permiten asignar un indicador a un registro de un conjunto de datos para volver al mismo posteriormente (es decir, marcamos una posición concreta): • • • GetBookmark: asigna un marcador a la posición actual en el conjunto de datos GotoBookmark: volver a un marcador creado previamente con GetBookmark FreeBookmark: libera un marcador creado previamente con GetBookmark Para crear un marcador es necesario declarar una variable de tipo TBookmark. Se trata de un puntero, así que tanto para liberarla definitivamente como para usarla para marcar otro registro, hemos de llamar a FreeBookmark. Por ejemplo: PROCEDURE HazAlgo(CONST Tbl: TTable); VAR B: TBookmark; BEGIN B := Tbl.GetBookmark; Tbl.DisableControls; TRY Tbl.First; WHILE NOT Tbl.EOF DO BEGIN { Proceso } Tbl.Next; END; FINALLY Tbl.GotoBookmark(B); Tbl.EnableControls; Tbl.FreeBookmark(B); END; END; Este código consta de un procedimiento al que le pasamos como argumento un objeto de tipo TTable (uno de los descendientes de TDataSet). Declaramos una variable de tipo TBookmark y marcamos la posición actual de la tabla (será la que estuviera apuntada en ese momento por el cursor, no tiene por qué ser la primera). Nos vamos al principio de la tabla (usando el método First) y mientras no lleguemos al final (usando la propiedad EOF), procesamos lo que haya que procesar de la fila actual, y avanzamos el cursor (usando el método Next). Terminado el proceso, volvemos a la posición marcada inicialmente y liberamos el marcador. Todo esto va englobado dentro de un bloque TRY ... FINALLY porque hemos de asegurarnos de que el marcador se libera. Nuestro proceso podría dar lugar a una excepción, así que hemos de ser cuidadosos. Para buscar datos dentro de un conjunto de datos usaremos la siguiente función: FUNCTION TDescDeTDataSet.Locate('Campo; ...; Campo', VarArrayOf([Valor1, ..., ValorN]), SearchOptions): Boolean; Especificamos los campos, los valores a buscar como un array de variants (por eso he puesto que el segundo parámetro es VarArrayOf, pero si hubiera un único campo no haría falta convertirlo a array de variant) y un conjunto de opciones de búsqueda que podeis consultar en la ayuda. Devuelve si ha tenido éxito o no con la búsqueda. De todas formas, cuando veamos el componente TQuery, casi con toda seguridad no le encontraremos mucha utilidad a esta función, pues en el componente TQuery podremos restringir la selección de datos tanto como queramos usando sentencias SQL. Modificación de datos del conjunto de datos Otro punto importante es saber cómo modificar los datos existentes en un conjunto de datos. Para ello, contamos con los siguientes métodos: Edit Asigna al conjunto de datos el estado dsEdit, si es que el conjunto no está ya en ese estado, o si está en modo dsInsert. Entonces, podremos editar datos. Append Almacena los datos pendientes, desplaza el registro actual al final del conjunto de datos, y asigna al conjunto de datos el estado dsInsert. Insert Almacena los datos pendientes y asigna al conjunto el estado dsInsert. Post Intenta almacenar el nuevo registro, o modificar el registro existente, en la base de datos. Si hay éxito, se asigna al conjunto de datos el estado dsBrowse. Si no, se mantiene el estado. Cancel Cancela la operación actual y asigna al conjunto el estado dsBrowse. Delete Elimina el registro actual y asigna al conjunto de datos el estado dsBrowse. Por ejemplo: Tbl.Edit; Tbl.FieldValues['Campo'] := Valor; Tbl.Post; Ponemos la tabla para editar, usamos la propiedad FieldValues para acceder al campo Campo, y almacenamos en este campo el valor Valor. A continuación, usando el método Post cerramos la edición, almacenándose los datos. Previo al estudio de los descendientes de TDataSet será necesario estudiar los componentes de campo, que se engloban en el objeto TField. Este objeto da formas de acceder a los campos individuales de un conjunto de datos. Todos los componentes de conjunto de datos cuentan con tantos objetos TField como campos tenga la tabla/consulta/etc. Los veremos en capítulo aparte. Dentro del objeto TDataSet, hay un evento que cabe destacar, el evento OnCalcFields, que nos sirve para decidir los valores de los llamados campos calculados en función de los valores de los campos normales. Si la propiedad AutoCalcFields del conjunto de datos es TRUE, entonces se produce un evento OnCalcFields si: • • • Hay un conjunto de datos abierto El foco se desplaza de un componente visual a otro, o de una columna a otra en un TDBGrid (rejilla de datos) Se recupera un registro de la base de datos Si el valor de un campo no calculado cambia, se llama a este evento independientemente de si AutoCalcFields es TRUE o FALSE. Veremos en el capítulo dedicado a TField qué es un campo calculado, y qué es un campo no calculado. Hay que tener cuidado con este evento y no llamar a Post dentro del código del manejador del evento: aunque AutoCalcFields es FALSE, OnCalcFields es llamado si hay un Post. Hacer Post en el evento sería recursivo y habria desbordamiento de pila. ¿Resulta confuso todo esto? Quizá un poco. Hay que tener en cuenta que llegados a este punto aún no hemos visto cómo llevar a la práctica este componente que no vamos a tratar directamente, ni cómo usarlo exactamente, ni cómo podemos mostrar los datos, ni... No hay que olvidarlo: no usaremos este componente sino sus descendientes, y todo lo que hemos visto aquí es puramente teórico pero imprescindible para avanzar. Es en los siguientes capítulos donde se empieza a manipular de lo que se habla aquí. 4.iii. TDataSource Se trata de un componente de base de datos no visual que sirve de conducto entre un conjunto de datos y un componente de visualización de datos en un form. Es necesario que cada control de datos esté asociado a un componente TDataSource para poder manipular y visualizar los datos. Propiedades interesantes DataSet Especifica el nombre del conjunto de datos del que este componente obtiene los datos. Puede también asignarse a un conjunto de datos de otro form para sincronizar los componentes de visualización de datos de los dos forms. Enabled Nos dice si está conectado a un conjunto de datos. AutoEdit Especifica si los conjunto de datos conectados al TDataSource entran automáticamente en modo Edit cuando el usuario empieza a introducir los componentes de visualización vinculados al conjunto de datos. Eventos interesantes OnDataChange Este evento se da cada vez que el cursor se desplaza a un nuevo registro. Si se llama al método Next, Previous, Insert o cualquier otro que implique un cambio de posición del cursor, se llamará a este evento. OnUpdateData Se produce cada vez que los datos están a punto de actualizarse, es decir, tras llamadas a Post pero antes de que los datos se almacenen definitivamente en la base de datos. OnStateChange Se produce cada vez que cambia el estado del conjunto de datos asociado al TDataSource. Veremos su utilidad cuando estudiemos los componentes gráficos, pues es en ese momento cuando hay que elegir un descendiente adecuado de TDataSet, asociarle un TDataSource para, en el componente gráfico, asociar el mismo TDataSource. Entonces tendremos conectados el componente gráfico con el TDataSet vía el TDataSource. 4.iv. TField: Uso de campos Vamos a ver en este capítulo un objeto muy importante y útil, el objeto TField. Este objeto representa columnas de bases de datos individuales en conjuntos de datos. No lo usaremos directamente en nuestras aplicaciones, sino que emplearemos sus componentes descendientes. Cada uno de ellos representa un tipo de dato distinto en una columna del TDataSet correspondiente al componente de conjunto de datos. Estos tipos son: Nombre del componente Nombre del componente Nombre del componente TStringField TSmallIntField TIntegerField TBooleanField TFloatField TCurrencyField TDateField TTimeField TDateTimeField TVarBytesField TBlobField TMemoField TWordField TBCDField TBytesField TGraphicsField TAutoIncField TNumericField Este componente no es visual, y tampoco está visible en tiempo de diseño. Los TField están asociados a un componente de conjunto de datos y dan a componentes de datos como TDBGrid acceso a ciertas columnas de bases de datos vía ese conjunto. Aunque se verá más adelante, voy a intentar explicar esto de forma más inteligible. Como ya sabemos, en una aplicación de base de datos tenemos, por una parte, componentes visuales que mostrarán los datos y nos dejarán cambiarlos y, por otra parte, componentes no visuales que obtienen los datos a mostrar. En medio tenemos a TDataSource, que es el conducto entre unos y otros, el que realiza el paso efectivo de la información. Podemos tener un control visual en que queramos mostrar varias columnas fijas de una determinada consulta. Bien, pues TField va a ser nuestro aliado. Definiremos lo que se llaman campos persistentes que podrán ser referenciados desde el componente visual para poder fijar las columnas a mostrar. Lo iremos viendo poco a poco. Al abrirse un conjunto de datos, se genera un componente de campo para cada columna de datos de la tabla o consulta correspondiente. Delphi usa BDE para determinar el tipo correcto de componente de campo que se debe asignar a cada columna (los que hemos visto en la tabla). Este tipo, además, determina las propiedades del campo así como cómo debe mostrarse. Por ejemplo, si se da el caso de que para una columna se genera un TFloatField, entonces tendremos una propiedad que nos permitirá decirle el formato que queremos de visualización. Si es, por ejemplo, de dos decimales, este formato sería 0.00#. También podríamos fijar el número de dígitos a mostrar. Es sólo un ejemplo. Como veremos, los componentes de campo tienen propiedades comunes, y luego habrá otras específicas del tipo de campo que sea. En modo de diseño, estos componentes se crean de forma dinámica si el correspondiente conjunto de datos tiene a True su propiedad Active. En tiempo de ejecución también se generan de forma dinámica. No obstante, es recomendado crear componentes de campo persistentes. Si, por ejemplo, tenemos un control TDBGrid que se basa en una estimación del número de campos, al añadir uno nuevo la aplicación podría responder de forma "extraña". Pero si el número de campos viene definido de antemano, estos problemas no se dan. Estos campos definidos "de antemano" son los que llamamos campos persistentes. Entre otras ventajas, nos ofrecen las siguientes: • Restricción de los campos del conjunto de datos a un subconjunto de las columnas disponibles en la base de datos correspondiente. • • • Definición de nuevos campos basándose en columnas de la tabla o consulta correspondiente al conjunto de datos. Definición de campos calculados. Modificación de las propiedades de edición y visualización de los componentes de campo. Con los componentes de campo persistentes tenemos garantizado que cada vez que se ejecute la aplicación se usarán y mostrarán las mismas columnas, en el mismo orden, incluso si ha cambiado la estructura física de la base de datos. Cómo se crean Para crearlos usaremos el editor de campos. Se inicia haciendo doble click sobre un componente de conjunto de datos (un descendiente de TDataSet). Presenta el siguiente aspecto: También podemos acceder a él pulsando con el botón derecho del ratón sobre el componente de conjunto de datos y escogiendo la opción "Fields Editor" que se nos mostrará en el menú contextual. Notar que en la barra de título pone NombreForm.NombreComponenteConjuntoDeDatos. Ahora tenemos que añadir aquí los campos que queramos que sean persistentes. Para ello, pulsamos con el botón derecho del ratón sobre el Editor de Campos, con lo que aparece el menú: Entonces nos aparece una lista con todos los campos disponibles. Por ejemplo: Los campos que nos aparecen son los correspondientes a la tabla seleccionada (si elegimos como componente conjunto de datos TTable) o a la consulta SQL (si elegimos TQuery). En este ejemplo, los campos son los resultantes de una consulta SQL en un componente de tipo TQuery. Si no termináis de verlo claro, en el ejemplo desarrollado de la agenda iremos viendo paso a paso qué es lo que debemos hacer y por qué. Podemos crear nuevos campos o eliminar campos que hayamos puesto en la lista. Además, podemos ordenarlos como queramos, pinchando sobre el campo a cambiar de sitio y arrastrándolo a la nueva posición. Para crear un campo nuevo (es decir, uno que no existe y por tanto no aparece en la lista de campos disponibles) pulsaremos con el botón derecho del ratón sobre el Editor de Campos y elegiremos la opción New Field... del menú contextual. Se nos abrirá una ventana como esta: Vamos a estudiar únicamente los dos primeros tipos de campo, Data y Calculated. El tercer tipo, Lookup puede ser estudiado como ejercicio buscando en la ayuda de Delphi (si no lo he dicho ya, la ayuda de Delphi es muy completa y un buen lugar donde encontrar documentación de forma estructurada; además, es muy buena en general). Definición de campos de datos Imaginad que quereis cambiar, por el motivo que sea, el tipo de dato de un campo. Por ejemplo, teneis un campo de tipo TSmallIntField y queréis convertirlo en TIntegerField. Dado que no podemos cambiar el tipo de dato de forma directa, tendremos que usar un campo de este tipo para substituir al existente. Los pasos a seguir (en la ventana que hemos visto antes) son: 1. 2. 3. 4. En primer lugar, hemos de asegurarnos que en el apartado Field type tenemos seleccionado Data. Introducimos como nombre de campo el nombre de un campo persistente que exista (aquel del que queremos cambiar su tipo) en el campo Name. No hemos de introducir un nombre nuevo, sino uno que exista. Elegir el nuevo tipo de dato seleccionándolo del combo Type. Introducir el tamaño del campo en la opción Size si es necesario (en los tipos TStringField, TBytesField y TVarBytesField). Con todos los datos necesarios introducidos, ya podemos pulsar el botón OK. Ya tenemos definido nuestro campo; podremos cambiar sus propiedades en el Inspector de Objetos. Definición de campos calculados Un campo calculado nos muestra valores que el evento OnCalcFields del objeto conjunto de datos calcula en tiempo de ejecución. Para crear un campo calculado, seguiremos los siguientes pasos: 1. 2. 3. 4. En primer lugar, nos aseguraremos de que en el apartado Field type tenemos seleccionado Calculated. Elegimos el tipo de datos seleccionándolo del combo Type. Le damos un nombre en el campo Name. Hemos de introducir un nombre nuevo, no el de un campo que ya exista. Introducir el tamaño en Size si es necesario. Con todo esto, pulsamos OK y ya tenemos creado nuestro campo calculado. Ahora habrá que acudir al evento OnCalcFields del conjunto de datos correspondiente y escribir el código que asignará un valor a nuestro campo calculado. Algunas propiedades de los componentes de campo Propiedad Breve descripción Aligment Nos permite alinear a la izquierda, derecha o centro el contenido del campo Calculated Si es True nos indica que el contenido del campo es calculado en el evento onCalcFields del respectivo componente fuente de datos. DisplayFormat Especifica el formato de los datos que se muestran (no disponible para todos los tipos de campo). DisplayWidth Ancho, en caracteres, de una columna de un TDBGrid que muestra el campo. FieldName Especifica el nombre de la columna real de la tabla de la que el campo toma sus datos. ReadOnly Si es True muestra los valores del campo, pero no permite editarlos. Por ejemplo, un componente de campo de tipo TStringField no cuenta con la propiedad DisplayFormat. Cuando hagáis vuestras propias aplicaciones os daréis cuenta de que si teneis un componente de conjunto de datos, llamémosle ConjuntoDeDatos, cuando creéis un campo persistente, por ejemplo CAMPO_PERSISTENTE, el objeto de campo que se crea recibe el identificador (el nombre para Delphi) ConjuntoDeDatosCAMPO_PERSISTENTE. En una aplicación, podemos acceder al valor de una columna de la base de datos vía la propiedad Value del componente de campo correspondiente haciendo (por ejemplo): EdNombreAlumno.Text := ConjuntoDeDatosNOMBRE_ALUMNO.Value; Esto va bien para las cadenas, pero en otros tipos de campos será necesario hacer una conversión; para ello, el componente de campo dispone de unos métodos que nos permitirá hacer esta conversión. Vamos a ver una tabla en la que mostramos estas funciones de conversión y apuntamos el tipo de campo para el que dicha función va bien: Tipo de campo / función AsVariant AsString AsInteger AsFloat AsCurrency AsDateTime AsBoolean TStringField OK OK OK OK OK OK TIntegerField OK OK OK OK OK TSmallIntField OK OK OK OK OK TWordField OK OK OK OK OK TFloatField OK OK OK OK TCurrencyField OK OK OK TBCDField OK OK OK TDateTimeField OK OK OK OK OK TDateField OK OK OK OK OK TTimeField OK OK OK OK OK TBooleanField OK OK TBytesField OK OK TVarBytesField OK OK TBlobField OK OK TMemoField OK OK OK OK TGraphicField OK OK En caso de duda, usar AsVariant. Vamos a hacer un pequeño inciso mostrando el resultado de algunas conversiones "especiales": De String a Boolean: Únicamente convierte los valores "True" o "Yes" a True y los valores "False" o "No" a False. El resto genera excepciones. De Float a Integer: Redondea al entero más cercano. De DateTime a Float: Convierte fechas al número de días transcurridos desde el 31 de diciembre de 1899 y las horas a una fracción de 24 horas. De Boolean a String: Convierte True a "True" y False a "False". En cualquier caso, una conversión no válida generará una excepción. Varios ejemplos de uso: EdNombre.Text := ConjuntoDeDatosNOMBRE_ALUMNO.AsString; EdApellidos.Text := ConjuntoDeDatosAPELLIDOS_ALUMNO.AsString; DTPickerFechaNac.Date := ConjuntoDeDatosFECHA_NAC_ALUMNO.AsDate; Curso := ConjuntoDeDatosCURSO_ALUMNO.AsInteger; ConjuntoDeDatosREPETIDOR_ALUMNO.AsBoolean := ChkBxRepetidor.Checked; Otras formas de acceder a los campos: También podemos acceder a los valores de los campos vía la propiedad Fields del objeto de conjunto de datos. Esta propiedad es de tipo array, por lo que necesitaremos saber la posición de un campo en el objeto conjunto de datos, siendo la primera columna la 0. Por ejemplo: EdNombre.Text := ConjuntoDeDatos.Fields[2].AsString; Esto sería si supiéramos que la tercera columna es la del nombre. Una alternativa que evita saber el orden correcto es usar el método FieldByName. Así, lo siguiente sería equivalente al ejemplo anterior: EdNombre.Text := ConjuntoDeDatos.FieldByName('NOMBRE_ALUMNO').AsString; 4.v. TTable Gracias a este componente vamos a poder trabajar con los datos de cualquier fila y columna de una tabla concreta de la base de datos. Hemos de recordar que es un descendiente de TDataSet, así que todo lo dicho para TDataSet es válido para TTable. Creación de un componente TTable Siempre que queramos usar un componente TTable, los pasos fundamentales a seguir son estos: 1. 2. 3. 4. Insertar el componente bien en un form bien en un Data Module y darle nombre en Name. Definir en la propiedad DatabaseName la base de datos a la que estará ligado. Definir en la propiedad TableName el nombre de la tabla a la que hará referencia. Insertar un componente TDataSource en el form o en el Data Module y asignar a su propiedad DataSet el nombre de la tabla (i.e., su propiedad Name). Nota: DatabaseName puede ser un alias de BDE (aparecerá en un desplegable junto a esta propiedad) o bien una ruta completa. Es recomendable que sea un alias BDE porque si cambiamos la base de datos de sitio, sólo tendremos que cambiar en el alias este dato y no en todos los componentes que hagan referencia a ella. Es más recomendable aún que sea el nombre de algún componente TDatabase que exista en la aplicación, porque si cambiara el alias, únicamente habría que cambiarlo aquí. Búsqueda de registros Para buscar registros usaremos el método Locate ya visto en la explicación de TDataSet. Este método nos desplazará el cursor a la primera fila que coincide con el conjunto de criterios de búsqueda especificados. Un ejemplo puede ser: Exito := Tabla.Locate('NOMBRE_ALUMNO', 'Juan', [loPartialKey]); No hay mucho más que decir; el resto ya fue estudiado con TDataSet. 4.vi. TQuery Este componente puede usarse con servidores de bases de datos remotos (en la version Client/Server Suite de Delphi), con bases de datos Paradox y dBase y con bases de datos compatibles con ODBC (como Access). Gracias a este componente vamos a poder acceder a varias tablas al mismo tiempo (vía un JOIN, claro está) o acceder de forma automática a un subconjunto de filas y columas de una tabla o tablas. Hemos de recordar que también es un descendiente de TDataSet, así que todo lo dicho para TDataSet es válido para TQuery. Los criterios de selección son sentencias SQL y pueden ser estáticos (todos los parámetros de la sentencia se dan en tiempo de diseño) o dinámicos (algunos o todos los parámetros se dan en tiempo de ejecución). Creación de un componente TQuery Siempre que queramos usar un componente TQuery, los pasos fundamentales a seguir son estos: 1. Insertar en el componente bien en un form bien en un Data Module y darle nombre en Name. 2. Definir en la propiedad DatabaseName la base de datos a la que estará ligado. 3. Especificar la sentencia SQL en la propiedad SQL del componente y definir, si es necesario, los parámetros de la misma en la propiedad Params. 4. Insertar un componente TDataSource en el form o en el Data Module y asignar a su propiedad DataSet el nombre del query (i.e., su propiedad Name). Consultas estáticas y dinámicas Vamos a distinguirlas con un ejemplo. Supongamos que escribimos en la propiedad SQL (veremos a continuación cómo asignar valores a esta propiedad) el texto: SELECT NOMBRE_ALUMNO FROM ALUMNOS WHERE NOTAMEDIA >= 5 AND EDAD = 15 Todos los valores para la cláusula WHERE vienen dados de forma explícita. Es una consulta estática, porque todos los valores vienen dados. Sin embargo, si en vez de eso escribiéramos: SELECT NOMBRE_ALUMNO FROM ALUMNOS WHERE NOTAMEDIA >= 5 AND EDAD = :Edad Vemos que aparece, en vez de un valor concreto para la edad, el valor :Edad. Eso quiere decir que en tiempo de ejecución, de acuerdo a los criterios que exija nuestro programa, asignaremos el valor adecuado de la edad según la situación. La forma de especificar estos nombres variables es anteponiendo dos puntos al nombre que le queramos dar. Luego, en el código, asignaremos el valor adecuado usando el método ParamByName del componente TQuery. Por ejemplo, para nuestro caso anterior podría quedar como sigue: QAlumnos.ParamByName('Edad').AsInteger := 17; Notar que el nombre del parámetro, en la llamada al método ParamByName, no lleva los dos puntos. Los dos puntos se ponen única y exclusivamente en la consulta, para distinguirlos de un valor estático y para saber que el identificador que va a continuación es el que se debe esperar en el programa. Dónde especificar la consulta SQL El componente TQuery tiene una propiedad llamada SQL que es donde se almacena la consulta a enviar a la base de datos. Podemos introducirla en tiempo de diseño pulsando sobre ella (se nos abre el editor de consultas) o en tiempo de ejecución, asignando a la propiedad Text de la propiedad SQL el texto completo de la consulta. Hay que señalar que esta propiedad SQL es de tipo TStringList, por ello contamos con una propiedad Text así como el resto de propiedades de estos objetos. En tiempo de ejecución, antes de asignar cualquier valor a la propiedad SQL la consulta debe estar cerrada. Esto lo conseguimos con el método Close que ya estudiamos en el objeto TDataSet. Así, un ejemplo de asignación en tiempo de ejecución puede ser: QAlumnos.Close; QAlumnos.SQL.Text := 'SELECT NOMBRE_ALUMNO FROM ALUMNOS WHERE EDAD = 15'; Para más información, repasad TDataSet y consultad en la ayuda de Delphi. 5.i. TDBGrid Este componente nos permite mostrar los registros de un TDataSet en forma tabular: Para asociar este componente con el TDataSet del que obtiene los datos, hemos de poner en su propiedad DataSource el nombre del TDataSource asociado al TDataSet. Si la propiedad State del DBGrid toma el valor csDefault, el aspecto de los registros se determina a través de las propiedades de los campos incluidos en el TDataSet asociado al DBGrid. Si el TDataSet asociado al DBGrid consta de componentes de campo persistentes (ya vimos cómo crearlos en el capítulo correspondiente), éstos permanecen aunque se cierre el TDataSet, de manera que las columnas asociadas a estos campos conservarán también sus propiedades aunque se cierre el TDataSet. Podemos personalizar una DBGrid precisamente gracias a los objetos de campo persistentes. Así, en tiempo de diseño podremos dejar la DBGrid lista para que funcione exactamente como queremos que se muestre siempre. La forma de hacerlo es la siguiente. En primer lugar, nos vamos a la propiedad Columns y hacemos doble click sobre el botón de los puntos suspensivos. Entonces se nos abre una ventana como la siguiente: Añadimos tantas columnas como queramos pulsando New. En principio, todas ellas no están asociadas a campos. Si pulsamos sobre una columna concreta, entonces en el Inspector de objetos nos muestra todas las propiedades de esa columna. El título de la columna puede tener su propio formato, independientemente del formato de la columna, pudiendo poner el título en negrita de color clNavy y la columna en cursiva de color clMaroon con fondo clInfoBk. Hay una propiedad que nos interesa mucho: FieldName. Si pulsamos, se nos abre un desplegable con todos los componentes de campo persistentes que hayan sido definidos para el TDataSet. Elegimos uno y ya está. Repetimos la operación con las restantes columnas, y ya tenemos el DBGrid completamente personalizado. Ejemplo: Creación de una agenda I. Diseño de la base de datos y las operaciones Como ejemplo de uso de base de datos con Delphi, vamos a acudir al socorrido ejercicio de crearnos una agenda con los datos de nuestros conocidos. Para evitar simplificar demasiado con una única tabla en la base de datos, añadiremos un pequeño extra en este ejemplo: los contactos estarán agrupados por categorías, lo que nos da la siguiente estructura (podeis añadir/eliminar los campos que queráis): Como vamos a usar Interbase, primero crearemos la base de datos bajo el usuario que queramos que sea su dueño. Si no sabeis dar de alta usuarios, o no sabeis crear la base de datos, id al anexo sobre Interbase. Yo le voy a dar el nombre Agenda.gdb. A continuación tenemos que crear las tablas. Podemos hacerlo directamente en el IBConsole, o podemos crear un alias BDE y entonces crear las tablas con el SQL Explorer. Yo voy a hacerlo usando la segunda opción, porque de todas maneras, necesitaremos el alias BDE para cuando llegue el momento de diseñar la aplicación con Delphi. Recordad que en el anexo 1 encontrareis la información necesaria para crear alias BDE y en el SQL Explorer. anexo 3 hay un pequeño manual de uso de Elijáis el camino que elijáis, hay que crear las tablas, para lo cual vamos a introducir las siguientes sentencias SQL: CREATE TABLE CATEGORIAS( IDCATEGORIA INTEGER NOT NULL, DESCRIPCION VARCHAR(255) NOT NULL, PRIMARY KEY(IDCATEGORIA) ); CREATE GENERATOR GENIDCATEGORIA; CREATE TABLE CONTACTOS( IDCONTACTO INTEGER NOT NULL, CATEGORIA INTEGER NOT NULL, NOMBRE VARCHAR(100) NOT NULL, APELLIDO1 VARCHAR(100) NOT NULL, APELLIDO2 VARCHAR(100), TELEFONO VARCHAR(20) NOT NULL, MOVIL VARCHAR(20), DIRECCION VARCHAR(255), EMAIL1 VARCHAR(255), EMAIL2 VARCHAR(255), WEB1 VARCHAR(255), WEB2 VARCHAR(255), PRIMARY KEY(IDCONTACTO) ); CREATE GENERATOR GENIDCONTACTO; ALTER TABLE CONTACTOS ADD FOREIGN KEY(CATEGORIA) REFERENCES CATEGORIAS(IDCATEGORIA); Una vez introducidas, vamos a pensar en las operaciones básicas de gestión de estos datos: inserción, edición, eliminación y búsqueda. Dejaremos claras cuáles serán las sentencias SQL que llevarán a cabo cada una de las operaciones, con lo que el desarrollo del programa será más sencillo. Gestión de la tabla CATEGORIAS Comenzaremos con la tabla CATEGORIAS. Entre otras cosas, empezamos por ella porque ningún campo de esta tabla depende de que exista un valor previo en otra tabla (cosa que no sucede en la tabla CONTACTOS). La clave primaria de esta tabla es IDCATEGORIA. Me gusta usar enteros como claves primarias porque son más simples. Si estamos seguros de que la combinación NOMBRE + APELLIDO1 + APELLIDO2 es única podríamos usarla como clave primaria, por ejemplo. Personalmente, prefiero que la clave primaria sea un entero independiente del resto de información. Es un criterio que he ido adoptando con el tiempo y que quizá vosotros veáis innecesario. Para generar los valores sucesivos de la clave primaria utilizo un generador, GENIDCATEGORIA. Le pongo el prefijo GEN porque así lo identifico rápidamente como generador de los valores del campo que le sigue, IDCATEGORIA en este caso. Así que vamos ya a ver en qué consisten las operaciones básicas para esta tabla. Inserción Cuando se crea un generador, se inicializa automáticamente a 0. Así que, cuando insertemos un nuevo registro, debemos incrementar este valor, e insertarlo junto con la descripción de la categoría. Por tanto, las sentencias necesarias para insertar una nueva categoría serían: INSERT INTO CATEGORIAS( IDCATEGORIA , DESCRIPCION ) VALUES ( GEN_ID(GENIDCATEGORIA, 1) , :Descripcion ); ¿Qué es :Descripcion? Es un parámetro. Con eso quiero decir que la descripción a insertar será la que se nos dé en el programa. Edición El único campo que podemos editar es la descripción de la categoría, ya que el ID permanece invariable. Es un dato que al usuario no le sirve y que no le vamos a mostrar (el exceso de información siempre es peligroso), pero que nosotros vamos a tener disponible (veremos cómo cuando entremos en el desarrollo con Delphi, por ahora, asumimos que es así). Por tanto, actualizar el registro será tan sencillo como: UPDATE CATEGORIAS SET DESCRIPCION = :Descripcion WHERE IDCATEGORIA = :Id ; Eliminación Por supuesto, tras asegurarnos de que el usuario realmente quiere eliminar ese registro, procederemos a realizar la operación de borrado: DELETE FROM CATEGORIAS WHERE IDCATEGORIA = :Id Cuidado con la operación de borrar, si no pusiéramos la cláusula WHERE, se borraría la tabla completa. Búsqueda Únicamente va a tener sentido buscar una categoría por su nombre. Una sentencia sencilla que nos daría esto sería: SELECT * FROM CATEGORIAS WHERE DESCRIPCION LIKE %LoQueBuscamos% pero en lugar de eso, vamos a usar el método Locate de los descendientes de TDataSet. Cuando desarrollemos la aplicación veremos que es bastante sencillo :-) Gestión de la tabla CONTACTOS Al contrario que con la otra tabla, ésta sí que depende de otra tabla. En el dibujo se ha mostrado que hay una relación entre la tabla CONTACTOS y la tabla CATEGORIAS, y es que todo contacto pertenece a una categoría, por ello, antes de crear un contacto, debe existir la categoría a la que se le quiere asociar. Así, creadas las categorías necesarias, ya podemos dar de alta contactos. Esta tabla también tiene como clave primaria un ID que será generado vía su generador asociado GENIDCONTACTO por medio de la función GEN_ID que ya hemos usado. He elegido los siguientes campos como obligatorios (especificado vía el NOT NULL que hay en la definición de algunos campos): IDCONTACTO, CATEGORIA, NOMBRE, APELLIDO1, TELEFONO. No he escogido APELLIDO2 porque no siempre sabemos el segundo apellido de una persona, y tampoco he escogido MOVIL porque un contacto cualquiera en principio no tiene por qué tener móvil, pero sí un TELEFONO, ya que la agenda se supone que es de teléfonos. Y la DIRECCION tampoco es obligatoria porque no tenemos por qué saber dónde vive. Lo mismo con el resto de campos. Son datos opcionales. Inserción Tras comprobar que los campos no nulos contienen valores, tendremos que escribir la siguiente consulta de inserción (los campos opcionales los represento entre []): INSERT INTO CONTACTOS( IDCONTACTO, CATEGORIA, NOMBRE, APELLIDO1[, APELLIDO2] , TELEFONO[, MOVIL][, DIRECCION][, EMAIL1][, EMAIL2] [, WEB1][, WEB2] ) VALUES ( GEN_ID(GENIDCONTACTO), :Categoria, :Nombre, :Apellido1[, :Apellido2] , :Telefono[, :Movil][, :Direccion][, :Email1][, :Email2] [, :Web1][, :Web2] ); Es obvio que de alguna manera hemos de conocer el IDCATEGORIA correcto para asociarlo al campo CATEGORIA. No hay ningún problema: veremos cómo nos ayuda Delphi en este sentido. Edición La edición es semejante a la inserción y queda como ejercicio para el lector ;-) No hay que preocuparse por cómo localizar los ID adecuados: Delphi sigue ayudándonos en este trabajo. Eliminación Previo asegurarse de si de verdad se va a eliminar el registro o no, la sentencia para eliminar el registro es análoga a la vista con las categorías y queda de nuevo como ejercicio ;-) Búsqueda Aquí podemos buscar por varios campos: por nombre, por apellido, por teléfono, ... al igual que con la tabla de CATEGORIAS, dejaremos el trabajo de la búsqueda al método Locate de los objetos descendientes de TDataSet ;-) Seguimos en el próximo capítulo diseñando la parte gráfica de la aplicación. Estudiad todo lo que hemos pensado hasta el momento, en particular, la relación entre las tablas. Ejemplo: Creación de una agenda II.I Diseño del interface de la aplicación: Categorías Una vez pensada la estructura de la base de datos así como las operaciones a realizar, vamos a diseñar el interface de la aplicación. Por variar un poco, vamos a crear un form con un TPageControl que contendrá dos TTabSheet: uno será la ficha de contactos y otro será la ficha de categorías: Además, vamos a tener en un DataModule el componente TDatabase: De las propiedades del TDatabase cabe destacar: AliasName = Agenda DatabaseName = DBGlobal LoginPrompt = False Name = DBGlobal En el evento OnCreate del DataModule escribiremos este código: procedure TDM.DataModuleCreate(Sender: TObject); begin DBGlobal.Params.Values['USERNAME'] := TuNombreDeUsuario; DBGlobal.Params.Values['PASSWORD'] := TuContraseña; try DBGlobal.Open; except ShowMessage('Error abriendo la base de datos: cerrando aplicación'); Application.Terminate; end; end; Para que reconozca ShowMessage debéis incluir la unit Dialogs, y para que reconozca al objeto Application debéis incluir la unit Forms. Además, hemos de colocar esta unit dentro del fichero del proyecto la primera de entre todos los forms que se crean. Así, lo primero que comprobamos al arrancar es que podemos conectar con la base de datos; si es así, pues ya se crean los forms y si no, se termina la aplicación. Ahora que ya tenemos el DataModule con la base de datos, comenzaremos con la ficha de categorías por ser la más sencilla: Por partes: la idea de funcionamiento (que aplicaremos también a los contactos) es que no vamos a crear categorías ni editarlas directamente sobre la DBGrid, sino que lo haremos aparte (concretamente en un panel). Los botones de gestión son de tipo TSpeedButton y los he llamado SBtnNuevo, SBtnEditar, SBtnEliminar, SBtnBuscar y SBtnOKBuscar. Cuando estemos editando datos en el panel, estos botones se deshabilitarán y al revés, cuando no estemos editando será el panel quien esté deshabilitado. El panel inferior recibe el nombre de PnlEdicion, tiene Caption = '' y BevelOuter = bvLowered. Dentro contiene los componentes que usaremos para crear nuevas categorías o editar las existentes. Concretamente, tenemos el edit EdCategoria, el label LblTipoModif, que nos dirá si estamos creando una nueva categoría o editando una existente y los TBitBtn BtnAceptar y BtnCancelar. Hemos colocado un DBGrid, DBGCategorias, un Table, TblCategorias y un DataSource, DSCategorias. El DataSource tiene por valor en su propiedad DataSet = TblCategorias. TblCategorias tiene en su propiedad DatabaseName = DBGlobal (debemos incluir la unit del DataModule para que esté disponible) y en su propiedad TableName = CATEGORIAS. El DBGrid tiene en su propiedad DataSource = DSCategorias y hemos definido una columna que mostrará el valor del campo persistente DESCRIPCION (he añadido los dos campos de la tabla a la lista de campos persistentes), por tanto, para esta columna, FieldName = DESCRIPCION. Además, al DBGrid le hemos quitado la posibilidad de que pueda editarse sobre él, el indicador de registro actual y que pueda cambiarse el tamaño de la columna. Empezaremos definiendo qué debe suceder en los eventos OnShow y OnCreate. El código para estos eventos será ampliado cuando trabajemos con la pestaña de contactos. procedure TFrmAgenda.FormCreate(Sender: TObject); begin HabilitarPanelEdicion(False); end; procedure TFrmAgenda.FormShow(Sender: TObject); begin TblCategorias.Close; TblCategorias.Open; end; Por otro lado, definimos los siguientes procedimientos que nos serán útiles para realizar la gestión. procedure TFrmAgenda.HabilitarBotonesGestion(Habilitar: Boolean); begin SBtnNuevo.Enabled := Habilitar; SBtnEditar.Enabled := Habilitar; SBtnEliminar.Enabled := Habilitar; SBtnBuscar.Enabled := Habilitar; end; procedure TFrmAgenda.HabilitarPanelEdicion(Habilitar: Boolean); var i: Integer; begin PnlEdicion.Enabled := Habilitar; for i := 0 to PnlEdicion.ControlCount - 1 do PnlEdicion.Controls[i].Enabled := Habilitar; VaciarCamposEdicion; end; procedure TFrmAgenda.CerrarEdicion; begin HabilitarPanelEdicion(False); HabilitarBotonesGestion(True); end; procedure TFrmAgenda.VaciarCamposEdicion; begin EdCategoria.Text := ''; LblTipoModif.Caption := ''; end; procedure TFrmAgenda.RefrescarCategorias(Id: Integer); begin TblCategorias.Close; TblCategorias.Open; if Id > 0 then begin TblCategorias.Locate('IDCATEGORIA', Id, []); DBGCategorias.SetFocus; end; end; Ahora vamos a definir los procedimientos de crear, editar y eliminar registros. La idea será que los botones Nuevo y Editar pondrán los valores pertinentes en los campos y luego, al pulsar el botón Aceptar, en función de una variable que llamaremos GestionNuevo: Boolean (privada dentro de la definición del form) sabremos si tenemos que hacer un INSERT o un UPDATE. El botón de Cancelar simplemente vaciará el edit del panel de edición y retornará el control a los botones de gestión. Para Eliminar primero pediremos confirmación. Vamos primero a crear una nueva tabla en la base de datos, cuya definición será: CREATE TABLE AUXILIAR( COLUMNA INTEGER ); La usaremos para incrementar el valor del generador para los ids, tanto de categorías como de contactos. Debemos insertar una única fila en esta tabla pudiendo contener cualquier valor entero, por ejemplo, 1. Este detalle es muy importante; lo que haremos a continuación no funcionará si esta tabla no tiene una y sólo una fila con un valor numérico cualquiera. Recordad que en la estructura de la base de datos se vio que hay un id en cada tabla que es único. Este id es el que nos permite distinguir un registro de otro. Además, incluimos en el form un objeto TQuery al que llamaremos QConsulta en cuya propiedad DatabaseName pondremos el valor DBGlobal. Este query nos servirá para realizar la consulta siguiente: SELECT GEN_ID(GENIDCATEGORIA, 1) FROM AUXILIAR; que nos devolverá el siguiente valor a insertar del generador. Así, creamos la siguiente función: function TFrmAgenda.GenID(Generador: String): Integer; begin QConsulta.Close; QConsulta.SQL.Text := 'SELECT GEN_ID(' + Generador + ', 1) FROM AUXILIAR;'; QConsulta.Open; Result := QConsulta.Fields[0].AsInteger; end; que usaremos en el lugar apropiado. Vamos a ver ahora cómo preparamos los controles según si pulsamos el botón Nuevo o el botón Editar: procedure TFrmAgenda.SBtnNuevoClick(Sender: TObject); begin GestionNuevo := True; HabilitarPanelEdicion(True); HabilitarBotonesGestion(False); LblTipoModif.Caption := 'Nueva categoría'; EdCategoria.SetFocus; end; procedure TFrmAgenda.SBtnEditarClick(Sender: TObject); begin if TblCategoriasIDCATEGORIA.AsInteger > 0 then begin GestionNuevo := False; HabilitarPanelEdicion(True); HabilitarBotonesGestion(False); LblTipoModif.Caption := 'Editando categoría'; EdCategoria.Text := TblCategoriasDESCRIPCION.AsString; end; end; Como podeis ver, en Editar la condición dice que si no hay ids (es decir, no hay registros, porque cuando se crea el primer registro su id es 1) entonces no hace nada, pero si hay ids, preparo el edit del panel de edición con el valor del campo seleccionado. Al pulsar el botón Eliminar ejecutaremos la siguiente acción: procedure TFrmAgenda.SBtnEliminarClick(Sender: TObject); var Id: Integer; begin Id := TblCategoriasIDCATEGORIA.AsInteger; if (Id > 0) and ( Application.MessageBox('¿Está seguro de que desea eliminar el registro seleccionado?', 'Eliminar registro', MB_YESNO + MB_ICONQUESTION ) = IDYES ) then begin TblCategorias.Delete; RefrescarCategorias(0); end; end; Si pulsamos el botón Cancelar tendremos: procedure TFrmAgenda.BtnCancelarClick(Sender: TObject); begin if Application.MessageBox('¿Está seguro de que desea cancelar la edición de los datos?', 'Confirmación de cancelación', MB_YESNO + MB_ICONQUESTION ) = IDYES then CerrarEdicion; end; Y si pulsamos Aceptar: procedure TFrmAgenda.BtnAceptarClick(Sender: TObject); var Id: Integer; begin if GestionNuevo then begin Id := GenID('GENIDCATEGORIA'); TblCategorias.Close; TblCategorias.Open; TblCategorias.Insert; TblCategoriasIDCATEGORIA.AsInteger := Id; TblCategoriasDESCRIPCION.AsString := EdCategoria.Text; TblCategorias.Post; RefrescarCategorias(Id); CerrarEdicion; end else { not GestionNuevo -> Editando existente } begin Id := TblCategoriasIDCATEGORIA.AsInteger; TblCategorias.Edit; TblCategoriasDESCRIPCION.AsString := EdCategoria.Text; TblCategorias.Post; RefrescarCategorias(Id); CerrarEdicion; end; end; Aquí tenemos un ejemplo de la aplicación funcionando: Por último, tratamos la búsqueda. Para la búsqueda, en primer lugar pondremos, en tiempo de diseño, la propiedad Enabled del botón OK a False. Después, al botón buscar le asignaremos AllowAllUp = True y GroupIndex = 1. A continuación, escribimos el siguiente código para el botón Buscar: procedure TFrmAgenda.SBtnBuscarClick(Sender: TObject); begin SBtnNuevo.Enabled := BtnBuscarPulsadoCategorias; SBtnEditar.Enabled := BtnBuscarPulsadoCategorias; SBtnEliminar.Enabled := BtnBuscarPulsadoCategorias; if not BtnBuscarPulsadoCategorias then begin EdBuscar.Enabled := True; SBtnBuscar.Down := True; SBtnOKBuscar.Enabled := True; BtnBuscarPulsadoCategorias := True; EdBuscar.SetFocus; end else begin EdBuscar.Text := ''; SBtnBuscar.Down := False; EdBuscar.Enabled := False; SBtnOKBuscar.Enabled := False; BtnBuscarPulsadoCategorias := False; end; end; Y para el botón OK escribimos el siguiente: procedure TFrmAgenda.SBtnOKBuscarClick(Sender: TObject); begin if BtnBuscarPulsadoCategorias then begin if EdBuscar.Text = '' then begin ShowMessage('Debe escribir un texto para realizar la búsqueda'); EdBuscar.SetFocus; end else begin if TblCategorias.Locate('DESCRIPCION', EdBuscar.Text, [loCaseInsensitive, loPartialKey]) then DBGCategorias.SetFocus else ShowMessage('No se encuentra ninguna categoría con esa descripción'); SBtnBuscar.Down := False; EdBuscar.Text := ''; EdBuscar.Enabled := False; SBtnOKBuscar.Enabled := False; BtnBuscarPulsadoCategorias := False; SBtnNuevo.Enabled := True; SBtnEditar.Enabled := True; SBtnEliminar.Enabled := True; end; end; end; Con lo que ya tenemos funcionando toda la parte de definición de categorías para nuestra agenda. En el capítulo siguiente, daremos cuerpo a la parte de contactos. Tened en cuenta que, visto lo que hemos visto aquí con tanto detalle, me saltaré algunas partes dejándoos a vosotros que penseis un poco y las desarrolleis. Vereis como no es tan difícil. Ejercicio Ampliar el programa no permitiendo repetir categorías. Sugerencia: usar un SELECT sobre la tabla AUXILIAR con la función EXISTS de SQL. A1. Borland Database Engine En este apartado vamos a ver los aspectos más básicos que necesitaremos configurar para poder acceder a nuestras bases de datos desde un programa Delphi. Arrancaremos el programa de configuración de BDE accediendo a la ruta Panel de control -> BDE Administrator, con lo que se nos abrirá la ventana: A la izquierda, en la pestaña Databases tenemos una lista con todos los alias que hayamos dado de alta. A la derecha no tenemos nada porque no hemos seleccionado alias alguno. Si pulsamos sobre un alias cualquiera, la parte derecha cambia y se muestra la configuración del alias, que podremos editar. Vamos a comenzar creando un nuevo alias. Con este nuevo alias podremos hacer referencia a la base de datos desde Delphi. Para ello, pulsamos en el menú Object, elegimos New... y a continuación escogemos el tipo de base de datos de la lista que nos ofrece: Trataremos con Interbase por ser un gestor que da suficiente potencia a la base de datos, así como que es gratuito y con buena calidad. Para ello, elegimos como nombre de driver INTRBASE. Hecho esto, veremos aparecer el nuevo alias, con un nombre temporal (a espera de que nosotros pongamos el nuestro) y con un indicador de que es necesario aplicar los cambios para dar la tarea por concluida: A la derecha de la pantalla nos han aparecido las opciones por defecto para este nuevo alias: Comenzaremos cambiando el nombre del alias, escribiendo DBPrueba en el lugar de INTRBASE1, que es el que sugiere por defecto: Ahora vamos a centrarnos en las opciones más importantes que nos ofrece: BLOB SIZE Tamaño máximo en Kb que podrá tener un campo de tipo Blob al insertarlo o recuperarlo de la base de datos. Es recomendable que tenga un tamaño entre 32 y 1000 Kb BLOBS TO CACHE Es el número máximo de blobs que pueden estar almacenados a la vez en el ordenador cliente. No es conveniente que sea una número muy grande. El valor suele variar entre 64 y 65536. LANGDRIVER Es el juego de caracteres usado por la base de datos. Es recomendable dejar en blanco el campo. MAX ROWS Es el número de filas que devolverá la base de datos al hacer una consulta. Si dejamos el valor -1 (recomendado), nos devolverá todas las filas que cumplan la condición de la consulta. OPEN MODE Es el modo de apertura de la base de datos. Sólo hay dos posibles formas de abrirla: READ ONLY La base de datos se abre en modo lectura, con lo que no podremos escribir registros ni actualizarlos READ/WRITE Es la opción por defecto. Teniendo esta opción, podremos realizar cualquier tipo de operación sobre la base de datos SERVER NAME Nombre del servidor y ruta dentro del mismo vía el que se accede a la base de datos. Depende de cómo esté configurado el acceso a la red, se escribirá de una de las siguientes formas: Acceso local Unidad:\Ruta\Fichero.gdb En red con TCP/IP bajo Windows NombreMaquina:Unidad:\Ruta\Fichero.gdb En red con TCP/IP bajo Unix NombreMaquina:Ruta/Fichero.gdb En red con NETBEUI NombreServidor\\Unidad:\Ruta\Fichero.gdb La extensión gdb es propia de los ficheros de Interbase. USER NAME Nombre de usuario que se tomará por defecto al establecer la conexión con el servidor. Cuando instalamos Interbase hay un usuario administrador por defecto, que es SYSDBA. Será recomendable usar un nuevo usuario. Si no queremos que el sistema nos muestre el nombre del usuario cuando vayamos a establecer la conexión, lo dejaremos en blanco. Esto no querrá decir que no haya un usuario definido, sólo que no se verá qué nombre es. Dejaremos todas las opciones por defecto para nuestro alias, salvo dos, SERVER NAME y USER NAME. Usaremos una base de datos local, así que en SERVER NAME simplemente escribiremos la ruta en la que se encuentra el fichero. Por otra parte, como nombre de usuario pondremos Prueba. En el apartado de Interbase veremos cómo dar de alta usuarios. , con lo que ya tenemos dado de alta el alias. A Finalizamos pulsando el botón de "Aplicar" partir de ahora podremos acceder a la base de datos desde Delphi simplemente refiriéndonos a DBPrueba. Si intentamos abrirla pulsando el + que hay al lado del nombre del alias, el programa nos dará un error informándonos de que no puede acceder a la base de datos. Hemos creado el alias, pero en ningún momento hemos creado físicamente la base de datos. En el capítulo de Interbase veremos cómo hacerlo. A2. InterBase Vamos a crear una base de datos Interbase. Para ello, abriremos el programa IBConsole, que se instala junto con Interbase. Si el servicio de Interbase no se carga de forma automática (en los sistemas Windows NT y Windows 2000 se carga de forma automática), tendremos que arrancarlo nosotros de forma manual. Recién instalado el servidor de Interbase, al abrir el programa IBConsole no tendremos definidos servidores ni más usuarios que el usuario por defecto, SYSDBA, cuya clave de acceso es masterkey. Sobre la opción InterBase Servers pulsaremos con el botón derecho del ratón, con lo que se nos despliega el siguiente menú: Elegimos Register... y accedemos a la pantalla que nos permite dar de alta el servidor local. Hasta que no registremos un servidor local, no tendremos accesible la opción de dar de alta un servidor remoto. En la pantalla que vemos: rellenaremos los campos USER NAME y PASSWORD con los valores respectivos SYSDBA y masterkey (muy importante que la clave esté en minúsculas). Pulsamos el botón OK y ya tenemos registrado el servidor local, que será nuestra propia máquina. Ahora podemos crear bases de datos en el servidor local, registrar bases de datos existentes (importante si queremos acceder a ellas, pues sólo veremos las registradas) y registrar servidores remotos (uno o varios). Comenzaremos viendo cómo crear un nuevo usuario. Cuando hemos registrado el servidor local, el aspecto de la pantalla principal cambia un poco, mostrándonos estas opciones: Si pulsamos sobre Users veremos únicamente al usuario SYSDBA. Para crear uno nuevo, accedemos a la opción User security del menú Server. Se nos muestra entonces la ventana: en la que pulsaremos el botón New, con lo que se vacían los campos USER NAME y PASSWORD para que los rellenemos con los del nuevo usuario. Una vez introducidos los datos, pulsamos el botón Apply, con lo que ya tenemos dado de alta el usuario. Ahora vamos a crear la base de datos. Lo primero de todo será hacer login como el usuario que queramos que tenga acceso a la base de datos, así que si estamos como SYSDBA y queremos que el usuario sea PEPE, tendremos que "desregistrarnos" como SYSDBA y registrarnos como PEPE en el servidor Local Server. Estando en Local Server, subopción Databases, pulsamos el botón derecho y elegimos Create database..., con lo que se nos abre la ventana: Rellenamos el nombre del fichero (ruta completa) y el alias que le vamos a dar a la base de datos, y pulsamos OK: El directorio debe existir, o de lo contrario se nos mostrará un error de escritura. Creada la base de datos, InterBase nos muestra la pantalla: Ya podemos empezar a trastear con nuestra recién creada base de datos. Podemos crear tablas, dar del alta registros, etc. Para ello, podemos emplear la utilidad ISQL, o bien SQL Explorer, del propio Delphi, que es el que describimos brevemente en el anexo siguiente. Realmente, se parecen tanto y son tan intuitivos, que no vale la pena repetir lo mismo. No hay que olvidar que para poder acceder a la base de datos desde Delphi, tenemos que haber creado un alias para ella, bien sea desde BDE Administrator o desde SQL Explorer. Crear un alias desde InterBase no quiere decir que Delphi lo vaya a reconocer. Delphi sólo lo reconocerá si ha sido creado desde sus herramientas. A3. SQL Explorer El programa SQL Explorer nos permite examinar bases de datos cuyo alias tengamos dado de alta con mucha comodidad: separa los elementos de la base de datos en categorías: dominios, tablas, generadores, funciones, procedimientos... Podremos modificar registros de las tablas sin necesidad de acudir a sentencias SQL, simplemente situándonos sobre el campo a modificar, actualizar valores de los generadores, editar procedimientos... y también nos permite introducir sentencias SQL con las que podremos crear tablas, modificarlas, crear procedimientos, generadores, etc. En última instancia, si se nos ha olvidado crear el alias a la base de datos con el programa BDE Administrator, con SQL Explorer podremos hacerlo siguiendo exactamente los mismos pasos. Para mostrar el manejo básico de este programa, trabajaremos con un ejemplo muy sencillo (y típico): supongamos que tenemos una librería, y únicamente necesitamos una base de datos formada por una tabla con los siguientes datos: • • • • • • • • Código Libro Título Autor Tema Editorial Precio Unidades ISBN Lo primero que tenemos que hacer es pensar de qué tipo de dato va a ser cada uno de los campos de la tabla. Parece lógico pensar que los campos "Código Libro","Precio", "Unidades" van a ser de tipo numérico, mientras que los demás serán de tipo carácter. Ejecutamos el programa SQL Explorer, que viene con Delphi, para dar de alta el alias de esta base de datos. En el anexo anterior vimos cómo crear la base de datos desde InterBase, así que ahora ya podemos abrirla con SQL Explorer y crear tablas. Al ejecutarlo, se nos muestra la siguiente pantalla: Si pulsamos en el iconito del + que hay a la izquierda de nuestra base de datos: se nos abre una ventana en la que tenemos que introducir el nombre de usuario y la contraseña del usuario activo en Interbase cuando creamos la base de datos: Entrados estos datos correctamente, se despliega una lista con varias entradas: Además, también veremos que el botón izquierdo de abrir se ha pulsado automáticamente: la base de datos está abierta, y podemos trabajar con ella. Podríamos haber conseguido lo mismo poniéndonos sobre ella y pulsando ese botón. Luego la cerraremos con la opción CLOSE del menú OBJECT, o bien "despulsando" el botón de abrir. Al abrirla, aparece en la ventana de la derecha una nueva solapa: ENTER SQL. Podemos distinguir una base de datos abierta de una que no lo está porque el icono que la acompaña está rodeada por un cuadrado de color verde, como se ve en el dibujo. Si nos ponemos sobre ella, veremos esto: Como hemos creado la base de datos, pero no le hemos añadido tablas, comprobamos pulsando el icono + que esa entrada de la lista está vacía: Así que la primera sentencia que vamos a introducir será la de la creación de la tabla. Para ello, escribimos lo siguiente: CREATE TABLE LIBROS( CODIGO INTEGER NOT NULL, TITULO VARCHAR(255) NOT NULL, AUTOR VARCHAR(100), TEMA VARCHAR(100), EDITORIAL VARCHAR(50), PRECIO INTEGER, UNIDADES INTEGER, ISBN VARCHAR(25), PRIMARY KEY (CODIGO) ); y pulsamos sobre el botón de ejecutar. Podemos ver que ahora Tables sí que contiene una entrada: la tabla que acabamos de crear. Si nos ponemos sobre la propia tabla, nos aparecen más opciones: De aquí, la pestaña que más nos interesa en este momento es DATA. ENTER SQL ya la conocemos, y TEXT únicamente nos muestra la definición de la tabla. Útil por si olvidamos de qué tipo eran los campos. Si nos ponemos sobre DATA, nos aparece esto: Aquí podemos dar valores a los campos directamente, sin necesidad de escribir sentencias SQL. Además, en la parte superior derecha de la ventana ha salido una barra de botones que explicamos a continuación casi completamente (únicamente nos dejaremos dos de ellos). Esta barra de botones nos va a servir para movernos entre registros, añadir registros, borrar registros, actualizarlos... Sin embargo, como estamos aprendiendo, vamos a ver primero un ejemplo de inserción de un registro con la instrucción INSERT de SQL. Para ello, empezamos por situarnos en la pestaña ENTER SQL, e introducimos lo siguiente: INSERT INTO LIBROS( CODIGO, TITULO, AUTOR, TEMA, EDITORIAL, PRECIO, UNIDADES, ISBN ) VALUES ( 1 , 'Manual de Matematicas' , 'I. Bronshteim' , 'Matematicas' , 'Mir' , 1500 , 10 , '5-38588-5' ); como se puede ver en el gráfico: Al pulsar sobre el botón de ejecutar la instrucción, veremos que en la barra de estado pone "1 rows were affected". Si nos vamos a la solapa DATA, aún no veremos nada: eso es porque no hemos actualizado los datos. Vamos a explicar ahora para qué sirven los botones de la parte superior derecha: Así que tendremos que pulsar el botón de actualizar datos para poder ver el resultado de la inserción: Como no caben todos los datos, con ayuda de las barras de desplazamiento podremos ir comprobando que los valores han sido introducidos correctamente. Con todo esto, podemos seguir introduciendo datos para practicar con SQL, o directamente en la solapa DATA. Además, podemos también investigar qué sucede con las sentencias UPDATE, DELETE y SELECT. Como último ejemplo de este capítulo, vamos a ver el resultado de una SELECT (previamente, he insertado dos registros más que podeis insertar a vuestro gusto como ejercicio ;-) ). Escribimos lo siguiente: SELECT * FROM LIBROS WHERE PRECIO > 1000; Y al pulsar sobre el botón que ejecuta la instrucción, obtenemos: Al desplazarnos hasta llegar al campo PRECIO: observamos que los campos han sido devueltos correctamente. Usar este programa para crear generadores, triggers, procedimientos, etc. es bastante sencillo. Normalmente haremos todo este trabajo en la pestaña ENTER SQL. Además, cada elemento tiene siempre la opción de desplegar datos sobre él. Por ejemplo, una tabla concreta despliega datos sobre quién es la clave primaria, qué claves ajenas tiene o qué triggers se han definido sobre ella (entre otros). Los procedimientos almacenados nos dirían cuáles son los parámetros de entrada, las vistas sus columnas... Es un programa muy intuitivo y agradable de manejar, así que el resto del aprendizaje os lo dejo a vosotros ;-)