La capa de abstracción de base de datos DAL

Dependencias

web2py viene con una capa de abstracción de base de datos (DAL), un API que asigna objetos Python a objetos de base de datos tales como consultas, tablas y registros. La DAL genera de manera dinámica el SQL en tiempo real usando el dialecto específico para el sistema de la base de datos, de manera que usted no tenga que escribir código SQL o tenga la necesidad de aprender diferentes dialectos SQL (el término SQL es usado genéricamente), y la aplicación será portable a distintas bases de datos. Al momento de escribir ésto, las base de datos soportadas son SQLite (la cual viene con Python, y por lo tanto, web2py), PostgreSQL, MySQL, Oracle, MSSQL, FireBird, DB:sub:2, Informix, Ingres y (parcialmente) el motor de aplicaciones de Google (Google App Engine GAE). El GAE es tratado como un caso particular en el capítulo 11.

La distribución binaria de Windows viene lista para ser usada con SQLite y MySQL, mientras que la versión binaria de Mac viene lista para ser usada con SQLite. Para usar cualquier otro sistema de base de datos, ejecute desde la distribución fuente e instale el controlador (driver) apropiado para la base de datos deseada.

Una vez que el controlador apropiado está instalado, inicie web2py desde la fuente y él encontrará el controlador. Aquí hay una lista de controladores:

Base de datos driver (fuente)
SQLite sqlite3 o pysqlite2 o zxJDBC[56] (en Jython)
PostgreSQL psycopg2[57] o zxJDBC[56] (en Jython)
MySQL MySQLdb[58]
Oracle cx_Oracle[59]
MSSQL pyodbc[60]
FireBird kinterbasdb[61]
DB:sub:2 pyodbc[60]
Informix informixdb[62]
Ingres ingresdbi[63]

web2py define las siguientes clases que componen a DAL:

DAL representa una conexión a una base de datos. Por ejemplo:

1
db = DAL('sqlite://storage.db')

Table representa una tabla de base de datos. Usted no instancia Table de manera directa; en cambio, DAL.define_table lo instancia.

1
db.define_table('mytable', Field('myfield'))

Los métodos más importantes de una tabla son:

.insert, .truncate, ,drop y import_from_csv_file.

Field representa un campo de base de datos. Puede ser instanciado y pasado como un argumento a DAL.define_table.

DAL Rows es el objeto retornado por una selección (select) de la base de datos. Puede ser pensado como una lista de Row filas:

1
rows = db(db.mytable.myfield!=None).select()

Row contiene valores de campos.

1
2
for row in rows:
     print row.myfield

Query es un objeto que representa una cláusula SQL “where”:

1
myquery = (db.mytable.myfield != None) | (db.mytable.myfield > 'A')

Set es un objeto que representa una grupo de registros. Sus métodos mas importantes son count, select, update y delete. Por ejemplo:

1
2
3
4
myset = db(myquery)
rows = myset.select()
myset.update(myfield='somevalue')
myset.delete()

Expression es algo como una expresión orderby o groupby. La clase Field es derivada de Expression. He aquí un ejemplo:

1
2
myorder = db.mytable.myfield.upper() | db.mytable.id
db().select(db.table.ALL, orderby=myorder)

Cadenas de Conexión

Una conexión con la base de datos es establecida creando una instancia del objeto DAL:

1
>>> db = DAL('sqlite://storage.db', pool_size=0

db no es una palabra clave; es una variable global que almacena el objeto de conexión DAL. Usted tiene la libertad de darle otro nombre. El constructor de DAL requiere de un solo argumento, la cadena de conexión. La cadena de conexión es el único código web2py que depende en un sistema de base de datos específico. Aquí hay ejemplos de cadenas de conexión para tipos específicos de sistemas de base de datos soportados (en todo los casos, asumimos que la base de datos esta siendo ejecutada desde localhost en su puerto por defecto y que se llama “test”):

SQLite sqlite://storage.db

MySQL mysql://username:password@localhost/test

PostgreSQL postgres://username:password@localhost/test

MSSQL mssql://username:password@localhost/test

FireBird firebird://username:password@localhost/test

Oracle oracle://username:password@test

DB2 db2://username:password@test

Ingres ingres://username:password@localhost/test

DB2 db2://username:password@test

Informix informix://username:password@test

Motor de Aplicaciones de Google gae

Note que en SQLite la base de datos consiste de un solo archivo. Si no existe, es creado. Este archivo se bloquea cada vez que es accesado. En el caso de MySQL, PostgreSQL, MSSQL, FireBird, Oracle, DB2 e Informix, la base de datos “test” debe ser creada fuera de web2py. Una vez que se establece la conexión, web2py creará, cambiará, y borrará tablas debidamente.

Es también posible establecer la cadena de conexión en None. En este caso DAL no conectará con sistema alguno de base de datos, pero el API aún podrá ser accesado para pruebas. Ejemplo de ésto serán discutidos en el capítulo 7.

Almacenamiento en memoria de la conexión a las base de datos (Connection Pooling)

El segundo argumento del constructor DAL es el pool_size, que por defecto está en 0.

Debido a que resulta lento establecer una nueva conexión a base de datos por cada solicitud, web2py, para evitar esto, implementa un mecanismo de almacenamiento en memoria de la conexión. Cuando se establece una conexión, después de que la página ha sido servida y la transacción completada, no es cerrada la conexión, sino que va a una sala de espera. Cuando la siguiente solicitud http es recibida, web2py trata de seleccionar una conexión desde la sala de espera y usa ésta para la nueva transacción. Si no hay conexión disponibles en la sala de espera, una nueva conexión es establecida.

El parámetro pool_size es ignorado por SQLite y GAE.

Las conexiones en la sala de espera son compartidas secuencialmente entre las tareas, en el sentido de de que pudiesen ser usadas por tareas diferentes pero no simultáneas. Sólo hay una sala de espera por cada proceso web2py.

Cuando se inicia web2py, la sala de espera siempre está vacía. La sala de espera se llena hasta el mínimo entre el valor de pool_size y el máximo número de solicitudes concurrentes. Ésto significa que si pool_size=10 pero nuestro servidor nunca recibe mas de 5 solicitudes concurrentes, entonces el tamaño verdadero de la sala de espera solo llegará a 5. Si pool_size=0, entonces el almacenamiento de las conexiones no es usado.

El almacenamiento en memoria de la conexión es ignorado por SQLite, ya que no traería ningún beneficio.

Fallos en la conexión

Si web2py no logra conectarse a la base de datos, esperará 1 segundo y tratará de nuevo 5 veces más antes de declarar un fallo. En el caso del almacenamiento en memoria de la conexión, es posible que una conexión almacenada que se mantiene abierta pero sin ser usada por algún tiempo sea cerrada por la base de datos. Gracias a la función de reintento, web2py de restablecer dichas conexiones caídas.

Bases de Datos replicadas

El primer argumento de DAL (...) puede ser una lista de URLs. En éste caso web2py trata de conectar con cada una de ellas. El fin de ésto es poder gestionar múltiples servidores de base de datos y distribuir la carga entre ellos. Aquí vemos una caso de uso típico:

1
db = DAL(['mysql://...1','mysql://...2','mysql://...3'])

En éste caso la DAL trata de conectar con la primera, y de fallar, tratará con la segunda y la primera a la vez. Ésto puede ser usado también para distribuir la carga en una base de datos con configuración maestro-esclavo. Hablaremos mas de esto en el capítulo 11 en el contexto de escalabilidad.

Palabras claves reservadas

Hay también otro argumento que puede ser pasado al constructor DAL para comprobar que nombres de tablas y columnas no coincidan con palabras claves SQL en los sistemas de base de datos de destino.

Éste argumento es check_reserved y por defecto es None.

Ésta es una lista de cadenas que contienen los nombres de los adaptadores de los sistemas de base de datos.

El nombre del adaptador es el mismo al usado en la cadena de conexión DAL. Así que si quiere comprobar contra PostgreSQL y MSSQL, entonces su cadena de conexión se vería así:

1
2
db = DAL('sqlite://storage.db',
       check_reserved=['postgres', 'mssql'])

La DAL revisará las palabras claves en el mismo orden que la lista.

Hay dos opciones extra, “all” y “common”. Si usted especifca todo (all), realizará comprobación con todas las palabras claves SQL conocidas. Si usted especifica común (common), solo realizará comprobación con palabras claves SQL comunes como SELECT, INSERT, UPDATE, etc.

Para sistemas soportados, quizás usted también quiera especificar si le gustaría realizar comprobación contra palabras claves SQL no reservadas. En éste caso usted agregaría _nonreserved al nombre. Por ejemplo:

1
check_reserved=['postgres', 'postgres_nonreserved']

Los siguientes sistemas de base de datos soportan comprobación de palabras claves no reservadas.

PostgreSQL postgres(_nonreserved)

MySQL mysql

FireBird firebird(_nonreserved)

MSSQL mssql

Oracle oracle

DAL, TABLE, FIELD

La mejor manera de entender el API de DAL es probar cada función. Ésto puede ser hecho de manera interactiva por medio del interprete de comandos de web2py, aunque últimamente el código DAL va en los modelos y controladores.

Comience creando una conexión. Para este ejemplo usted puede usar SQLite. Nada de lo discutido cambiará si cambia de motor del sistema.

1
>>> db = DAL('sqlite://storage.db')

La base de datos está ahora conectada y la conexión es almacenada en la variable global DB.

En cualquier momento puede recuperar la cadena de conexión.

1
2
>>> print db._uri
sqlite://storage.db

y el nombre de la base de datos

1
2
>>> print db._dbname
sqlite

La cadena de conexión es llamada _uri porque es una instancia de un Identificador de Recursos Uniformes (Uniform Resource Identifier URI).

La DAL permite conexiones múltiples con la misma base de datos o con diferentes bases de datos, incluso base de datos de distintos tipos. Por ahora asumiremos la presencia de una única base de datos ya que es la situación más común.

El método más importante de una DAL es define_table:

1
>>> db.define_table('person', Field('name'))

Define, almacena y devuelve un objeto Table llamado “person” que contiene un campo (column) “name”. Éste objeto también puede ser accesado por medio de db.person, así no tendrá que tomar el valor retornado.

Representación de Registros

Es opcional pero recomendado especificar una representación de formato para los registros:

1
>>> db.define_table('person', Field('name'), format='%(name)s')

o

1
>>> db.define_table('person', Field('name'), format='%(name)s %(id)s'

o incluso algunos más complejos usando una función:

1
2
>>> db.define_table('person', Field('name'),
       format=lambda r: r.name or 'anonymous')

El atributo de formato sera usado para dos fines:

  • Para representar registros referenciados en desplegables selección/opción.
  • Para establecer el atributo db.othertable.person.represent a todos los campos referenciando esta tabla. Ésto significa que SQLTABLE no mostrará referencias por id sino que en cambio usará la representación de formato preferida.

Éstos son los valores por defecto de un constructor de campo:

1
2
3
4
5
6
7
8
Field(name, 'string', length=None, default=None,
      required=False, requires='<default>',
      ondelete='CASCADE', notnull=False, unique=False,
      uploadfield=True, widget=None, label=None, comment=None,
      writable=True, readable=True, update=None, authorize=None,
      autodelete=False, represent=None, compute=None,
      uploadfolder=os.path.join(request.folder,'uploads'),
      uploadseparate=None)

No todos ellos son relevantes para todos los campos. La longitud “lenght” es relevante sólo para campos del tipo cadena “string”. Campo de carga “uploadfield” y autorizar “autorize” son sólo importantes para campos del tipo carga “upload”. Al borrar “ondelete” es sólo relevantes para campos del tipo referencia “reference” y carga “upload”.

  • length establece la longitud máxima de un campo cadena (string), clave (password) o carga de archivos (upload). Si length no es especificado, un valor por defecto es usado pero no se garantiza que el dicho valor sea compatible con versiones anteriores. Para evitar migraciones indeseadas en actualizaciones, recomendamos que siempre especifique la longitud para los campos de cadenas, claves o carga de archivos.
  • default establece el valor por defecto para el campo. El valor por defecto es usado cuando se realiza una inserción si una valor no es especificado explícitamente. Es también usado para pre-cargar formularios construidos desde la tabla usando SQLFORM.
  • required le dice a la DAL que no debe permitirse inserción en ésta tabla si un valor para éste campo no es explícitamente especificado.
  • requires es un validador o una lista de validadores. No es usado por la DAL, sino que es usado por SQLFORM. Los validadores por defecto para los tipos dados son mostrados en la siguiente tabla:

tipo de campo validadores de campo por defecto

string IS_LENGTH(length)

text None

blob None

boolean None

integer IS_INT_IN_RANGE(-1e100, 1e100)

double IS_FLOAT_IN_RANGE(-1e100, 1e100)

decimal(n,m) IS_DECIMAL_IN_RANGE(-1e100, 1e100)

date IS_DATE()

time IS_TIME()

datetime IS_DATETIME()

password None

upload None

reference <table> IS_IN_DB(db,’<table>.id’)

list:string None

list:integer None

list:reference <table> IS_IN_DB(db,’<table>.id’,multiple=True

Decimal requiere y retorna valores como objetos Decimal, como está definido en el módulo decimal de Python. SQLite no maneja el tipo decimal así que internamente lo tratamos como un double. Los (n,m) son el número de dígitos antes y después del punto decimal.

Los campos lista: (list:) son especiales porque están diseñados para tomar ventaja de ciertas características de desnormalización en NoSQL (en el caso del motor de aplicaciones de Google, GAE, los tipos de campos ListProperty y StringListProperty) y hacerles parches de compatibilidad con las bases de datos relacionales soportadas. En las bases de datos relacionales las listas son guardadas como campos de texto text. Los ítems son separados por | y cada | en un ítem cadena es escapado como un ||. Son discutidos en su propia sección.

Tome en cuenta que requires=... es forzado al nivel de los formularios, required=True es forzado al nivel de la DAL (inserción), mientras que notnull, unique y ondelete son forzados al nivel de la base de datos. Mientras en algunos casos esto pueda parecer redundante, es importante para mantener las distinción cuando se programa con la DAL.
  • ondelete se traduce en la declaración SQL “ON DELETE”. Por defecto “CASCADE” le dice a la base de datos que cuando borre un registro, también debe borrar todos los registros que se refieran al el.

  • notnull=True se traduce en la declaración SQL “NOT NULL”. Previene que la base de datos inserte valores nulos en los campos.

  • unique=True se traduce en la declaración SQL “UNIQUE” y se asegura que los valores de éste campo sean únicos dentro de la tabla. Es forzado a nivel de la base de datos.

  • uploadfield aplica sólo a campos del tipo “upload”. Un campo del tipo “upload” almacena el nombre de un archivo guardado en otra parte, por defecto en el sistema de archivos bajo carpeta “uploads/” de la aplicación. Si uploadfield es establecido, entonces el archivo es guardado en un campo blob dentro de la misma tabla y el valor de uploadfield es el nombre de dicho campo. Ésto será discutido con detalle más adelante en el contexto de SQLFORM.

  • uploadfolder es por defecto la carpeta “upload/” de la aplicación. Si se establece a una ruta distinta, los archivos serán cargados a una carpeta distinta. Por ejemplo, uploadfolder=os.path.join(request.folder,’static/temp’) cargará los archivos a la carpeta web2py/applications/myapp/static/temp.

  • Si uploadseparate se establece en True, cargará archivos bajo diferentes subcarpetas de la carpeta uploadfolder. Esto es optimizado para evitar que haya muchos archivos en la misma carpeta / subcarpeta. ATENCION: no puede cambiar el valor de uploadseparate de True a False sin dañar el sistema. web2py usa las subcarpetas separadas o no las usa. Cambiar el comportamiento después de que los archivos han sido cargados hará que web2py no pueda recuperar dichos archivos. Si esto sucede es posible mover los archivos y arreglar el problema pero el proceso no se describe aquí.

  • widget debe ser uno de los objetos widgets disponibles, incluyendo widgest personalizados, por ejemplo: SQLFORM.widgets.string.widget. Una lista de widgets disponibles será vista más adelante. Cada tipo de archivo tiene un widget por defecto.

  • label es una cadena (o algo que puede ser serializado a una cadena) que contiene la etiqueta que será usada para éste campo en los formularios autogenerados.

  • comment es una cadena (o algo que puede ser serializado a una cadena) que contiene un comentario asociado a este archivo, y sera mostrado a la derecha del campo de entrada en los formularios autogenerados.

  • writable. Si un campo permite escritura, puede ser editado los formularios de creación y actualización autogenerados.

  • readable. Si un campo permite lectura, será visible sólo en formularios de sólo-lectura. Si un campo no permite escritura ni lectura, no será mostrado en los formularios de creación y actualización.

  • update contiene el valor por defecto para éste campo cuando el registro es actualizado.

  • compute es una función opcional. Si un registro es insertado y no hay valor para un campo que tiene el atributo computo, el valor es calcular pasando el registro (como un diccionario) a la función de computo.

  • authorize puede ser usado para requerir control de acceso en el campo correspondiente, para campos de carga de archivos solamente. Será discutido en más detalle en el contexto de Autenticación y Autorización.

  • autodelete determina si el correspondiente archivo cargado debe ser borrado cuando el registro que hace referencia al archivo es borrado. Para campos de carga de archivo solamente.

  • represent puede ser None o puede apuntar a una función que toma el valor de un campo y devuelve una representación alterna para el valor del campo. Ejemplos:

    1
    2
    3
    4
    db.mytable.name.represent = lambda name: name.capitalize()
    db.mytable.other_id.represent = lambda id: db.other(id).myfield
    db.mytable.some_uploadfield.represent = lambda value: \
        A('get it', _href=URL('download', args=value))
    

Campos “blob” también son especiales. Por defecto los datos binarios son codificados en base64 antes de ser almacenados en el campo de la base de datos, y son decodificados al ser extraídos. Ésto tiene el efecto negativo de usar 25% más espacio de almacenamiento que el necesario en los campos blob, pero tiene dos ventajas. En promedio reduce la cantidad de data comunicada entre web2py y el servidor de base de datos, y hace que la comunicación sea independiente de las convenciones de escapado específicas del sistema de base de datos.

Puede consultar la basa de datos para buscar tablas existentes:

1
2
>>> print db.tables
['person']

También puede consultar una tabla para buscar campos existentes:

1
2
>>> print db.person.fields
['id', 'name']

No declare un campo llamado “id”, porque uno es creado por web2py de todos modos. Cada tabla tiene por defecto un campo llamado “id”. Es un campo entero auto-incremental (comenzando con 1) usado para referencia cruzada y para hacer que cada registro sea único, por lo que “id” es una clave primaria. (Nota: los id’s comenzando con 1 son específicos del sistema. Por ejemplo, ésto no aplica al motor de aplicaciones de Google (GAE).).

Opcionalmente puede definir un campo de tipo=’id’ (type=’id’) y web2py lo usara como un campo id autoincremental. Ésto no es recomendable a menos que se acceda a tablas de base de datos antiguas. Con alguna limitación, puede usar también diferentes claves primarias lo cual será discutido en la sección bajo “Base de Datos Antiguas y tablas con claves”.

Usted puede consultar para buscar el tipo de una tabla:

1
2
>>> print type(db.person)
<class 'gluon.sql.Table'>

Y puede acceder a una tabla desde la conexión DAL usando:

1
2
>>> print type(db['person'])
<class 'gluon.sql.Table'>

De manera similar usted puede accedar a campos por su nombre en múltiples maneras equivalentes:

1
2
3
4
5
6
>>> print type(db.person.name)
<class 'gluon.sql.Field'>
>>> print type(db.person['name'])
<class 'gluon.sql.Field'>
>>> print type(db['person']['name'])
<class 'gluon.sql.Field'>

incluyendo su tabla padre, nombre de la tabla, y conexión padre:

1
2
3
4
5
6
>>> db.person.name._table == db.person
True
>>> db.person.name._tablename == 'person'
True
>>> db.person.name._db == db
True

Migraciones

define_table chequea si la tabla correspondiente existe o no. No no existe, genera y ejecuta el SQL para crearla. Si la tabla existe pero es diferente a la que está siendo definida, genera y ejecuta el SQL necesario para modificar la tabla. Si un campo ha cambiado de tipo pero no de nombre, tratará de convertir los datos (si no quiere ésto, necesita redefinir la tabla dos veces, la primera dejando que web2py remueva el campo, y la segunda vez agregando el campo que se acaba de definir para que web2py pueda crearlo.). Si la tabla existe, y es idéntica a la definición actual, se dejará como está. En todos los casos se creará el objeto db.person que representa a la tabla.

Nos referimos a este comportamiento como “migración”. webp2y registra todas las migraciones e intento de migraciones en el archivo “databases/sql.log”.

El primer argumento de define_table siempre es el nombre de la tabla. Los otros argumentos sin nombre son los campos (Field). La función también toma un último argumento opcional llamado “migrate” (migrar) el cual deberá ser referenciado explícitamente por nombre como se ve en:

1
>>> db.define_table('person', Field('name'), migrate='person.table')

El valor de migrate es el nombre del archivo (en la carpeta “databases” de la aplicación) donde web2py almacena información interna de migración para ésta tabla. Éstos archivos son muy importantes y nunca deberán ser removidos excepto cuando se quiera eliminar toda la base de datos. En este caso los archivos “.table” tienen que ser borrados manualmente. Por defecto migrate está establecido en True. Ésto causa que web2py genere el nombre de archivo haciendo un hash de la cadena de conexión. Si migrate se establece en False, la migración no es llevada a cabo, y web2py asume que la tabla existe y que contiene (por lo menos) los campos listados en define_table. El deber ser es dar un nombre explícito a la tabla migrada.

No puede haber dos tablas en la misma aplicación con el mismo nombre de archivo de migración.

Arreglando migraciones dañadas

Hay dos problemas comunes relacionados con las migraciones, y hay dos maneras para recuperarse de ellos.

Un problema es específico con SQLite. SQLite no hace cumplir tipos de columna ni puede eliminarlas. Ésto significa que si usted tiene una columna de tipo cadena y la borra, en realidad no es removida. Si agrega de nuevo la columna con un tipo diferente (por ejemplo datetime) terminará con una columna datetime que contiene cadenas (para efectos prácticos, basura). web2py no se queja de esto ya que no sabe que es lo que hay en la base de datos, hasta que trata de recuperar los registros y falla.

Si web2py devuelve un error en la función gluon.sql.parse al seleccionar registros, éste es el problema: datos corruptos en una columna debido al anterior problema.

La solución consiste en actualizar todos los registros de la tabla y los valores de la columna en cuestión con None.

El otro problema es mas genérico, pero típico de MySQL. MySQL no permite mas de un ALTER TABLE en una transacción. Ésto significa que web2py debe dividir transacciones complejas en otras más pequeñas (haciendo un ALTER TABLE a la vez) y confirmar una pieza a la vez. Es por lo tanto posible que parte de las transacciones complejas sean confirmadas y una parte falle dejando a web2py en un estado corrupto. ¿Por qué fallaría una parte de una transacción? Porque, por ejemplo, supone modificar una tabla y convertir una columna cadena a una columna datetime, web2py trata de convertir los datos, pero los datos no pueden ser convertidos. ¿Qué le pasa a web2py? Se confunde acerca de cual es exactamente la estructura de la tabla almacenada realmente en la base de datos.

La consiste en deshabilitar migraciones para todas las tablas y habilitar migraciones falsas:

db.define_table(....,migrate=False,fake_migrate=True)

Esto reconstruirá la datos de web2py acerca de la tabla de acuerdo con la definición de la tabla. Pruebe múltiples definiciones de tablas para ver cual trabaja (el que está antes de la migración fallida y el que está después de la migración fallida). Una vez logrado, remueva el atributo fake_migrate=True.

Antes de intentar arreglar problemas de migración es prudente hacer una copia de los archivos “applications/tuaplicacion/databases/*.tables”.

insert

Dada una tabla, se pueden insertar registros

1
2
3
4
>>> db.person.insert(name="Alex")
1
>>> db.person.insert(name="Bob")
2

Insert devuelve el valor “id” único de cada registro insertado.

Puede hacerle “truncate” a la tabla, por ejemplo borrar todos los registros y reestablecer el contador del id.

1
>>> db.person.truncate()

Ahora, si usted inserta de nuevo un registro, el contador comenzará en 1 (ésto es específico del sistema de la base de datos y no aplica a GAE):

1
2
>>> db.person.insert(name="Alex")
1

web2py provee también un método /ft bulk_insert

1
2
>>> db.person.bulk_insert([{'name':'Alex'}, {'name':'John'}, {'name':'Tim'}])
[3,4,5]

Toma una lista de diccionarios de campos a ser insertados y realiza múltiples inserciones a la vez. Devuelve los Ids de los registros insertados. En la base de datos relacional soportadas no hay ventaja en usar esta función al contrario de hacer un ciclo y realizar inserciones individuales pero en el motor de aplicaciones de Google si hay una ventaja notable en velocidad.

commit y rollback

Ninguna operación crear, eliminar, insertar, limpiar, borrar o actualizar es confirmada hasta realizar el comando

1
>>> db.commit()

Para verificarlo insertemos un nuevo registro:

1
2
>>> db.person.insert(name="Bob")
2

Ahora nos devolvemos, por ejemplo, ignorando todas las operaciones desde la última confirmación:

1
>>> db.rollback()

Si ahora insertamos de nuevo, el contador será establecido de nuevo en 2, ya que la anterior inserción fue regresada.

1
2
>>> db.person.insert(name="Bob")
2

El código en los modelos, controladores y vistas está encerrado en código web2py que se ve así:

try:
     execute models, controller function and view
except:
     rollback all connections
     log the traceback
     send a ticket to the visitor
else:
     commit all connections
     save cookies, sessions and return the page

No hay nunca necesidad de realizar una confirmación commit o un regreso rollback de manera explícita en web2py a menos que se necesite más control granular.

SQLCrudo

executesql

La DAL le permite generar explícitamente sentencias SQL.

1
2
>>> print db.executesql('SELECT * FROM person;')
[(1, u'Massimo'), (2, u'Massimo')]

En este caso, los valores retornados no son analizados o transformados por la DAL, y el formato depende del driver específico de la base de datos. Éste uso con selecciones normalmente no es necesitado, pero es más común con índices. excutesql toma dos argumentos opcionales: placeholders y as_dictplaceholders es una secuencia opcional de valores para ser sustituidos en, de ser soportado por el controlador de la base de datos, un diccionario con claves que coinciden con los marcadores de posición con nombre en su SQL.

Si as_dict se establece en True, y el cursor de resultados devuelto por el controlador de la base de datos será convertido a una secuencia de diccionarios que tienen como claves los nombres de los campos de la base de datos. Los resultados devueltos con as_dict=True son los mismos que aquellos devueltos cuando se aplica .to_list() a una selección normal.

1
[{field1: value1, field2: value2}, {field1: value1b, field2: value2b}]

_lastsql

Ya sea que el SQL fue ejecutado manualmente usando executesql o que fué generado por la DAL, siempre puede encontrar el código SQL en db._lastsql. Ésto es útil para búsqueda de errores:

1
2
3
4
5
>>> rows = db().select(db.person.ALL)
>>> print db._lastsql
SELECT person.id, person.name FROM person;

web2py nunca genera consultas usando el operador “*”. web2py siempre es explícito al seleccionar campos.

drop

Finalmente, se puede hacer drop de las tablas y todos los datos se perderán:

1
>>> db.person.drop()

Índices

Actualmente el API de la DAL no provee de un comando para crear índices en las tablas, pero esto puede ser logrado usando el comando executesql. Esto es debido a que la existencia de índices pueden convertir a las migraciones en algo complejo, y es mejor manejarlos de manera explícita. Los índices pueden ser necesitados para aquellos campos que son usados en consultas recurrentes.

He aquí un ejemplo de como crear un índice usando SQL en SQLite:

1
2
3
>>> db = DAL('sqlite://storage.db')
>>> db.define_table('person', Field('name'))
>>> db.executesql('CREATE INDEX IF NOT EXISTS myidx ON person name;)

Otros dialectos de base de datos tienen sintaxis muy similares pero pueden no soportar la directiva opcional “IF NOT EXISTS”.

Bases de datos pre-existentes y tablas con llaves

web2py puede conectarse con bases de datos con legado bajo algunas circunstancias.

La manera más sencilla es cuando las siguientes condiciones se cumplen:

  • Cada tabla debe tener un campo entero auto-incremental llamado “id”.

  • Los registros deben ser referenciados usando exclusivamente el campo “id”.

    Cuando se accede a una tabla existente, por ejemplo, una tabla no creada por web2py en la aplicación actual, siempre establezca migrate=False.

Si la tabla legado tiene un campo entero auto-incremental pero que no es llamado “id”, web2py aun puede acceder a él, pero la definción de la tabla debe contener explícitamente Field(‘....’,’id’) donde .... es el nombre de campo entero auto-incremental.

Finalmente si la tabla legado usa una clave primaria que no es un campo id auto-incremental, es posible usar una “tabla con clave”, por ejemplo:

1
2
3
4
5
6
db.define_table('account',
    Field('accnum','integer'),
    Field('acctype'),
    Field('accdesc'),
    primarykey=['accnum','acctype'],
    migrate=False)

En este ejemplo el atributo clave primaria “primarykey” es una lista de campos que constituye la clave primaria. Al momento de escribir este libro, no podemos garantizar que el atributo primarykey trabaje con toda tabla antigua y todo sistema de base de datos soportado. Por simplicidad recomendamos, si es posible, crear una vista de base de datos que contenga una campo id auto-incremental.

Notese que actualmente esto está disponible solo en DB2, MS-SQL, Ingres e Informix, pero otras BDs pueden ser facilmente adicionadas.

Transacción Distribuida

Al momento de escribir este libro, esta característica es sólo soportada por PostgreSQL, MySQL y Firebird, ya que ellos exponen el API para confirmaciones de dos fases.

Asumiendo que usted tiene dos (o más) conexiones a distintas bases de datos PostgreSQL, por ejemplo:

1
2
db_a = DAL('postgres://...')
db_b = DAL('postgres://...')

En sus modelos o controladores, puede confirmarlas concurrentemente con:

1
DAL.distributed_transaction_commit(db_a, db_b)

Al fallar, esta función se regresa y levanta una excepción Exception.

En los controladores, cuando una acción devuelve algo, si usted tiene dos conexiones distintas y no llama a la función mostrada arriba, web2py confirma las conexiones separadamente. Esto significa que hay posibilidad de que una de las confirmaciones tenga éxito y la otra no. La transacción distribuida evita que ésto suceda.

Cargas Manuales

Considere el siguiente modelo:

1
db.define_table('myfile',Field('image','upload'))

Normalmente una inserción es gestionada de manera automática por medio de un SQLFORM o un formulario crud (que es un SQLFORM), pero en ocasiones usted ya tiene el archivo en el sistema de archivos y quiere cargarlo de manera programática. Ésto puede ser hecho de la siguiente manera:

1
2
stream = open(filename,'rb')
db.myfile.insert(image=db.myfile.image.store(stream,filename))

El método store del objeto de campo de cargas toma un segmento continuo de un archivo y también su su nombre. Usa el nombre del archivo para determinar la extensión (tipo) del archivo, crea un nuevo nombre temporal para el archivo (de acuerdo al mecanismo de carga de web2py) y carga el contenido del archivo en el nuevo archivo temporal (bajo la carpeta de carga “uploads” a menos que se especifique lo contrario). Devuelve el nuevo nombre temporal que debe ser guardado en el campo image de la tabla db.myfile.

Consulta, Conjunto, Filas

Consideremos de nuevo la tabla definida (y eliminada) previamente e insertemos tres registros:

1
2
3
4
5
6
7
>>> db.define_table('person', Field('name'))
>>> db.person.insert(name="Alex")
1
>>> db.person.insert(name="Bob")
2
>>> db.person.insert(name="Carl")
3

Puede almacenar la tabla en una variable. Por ejemplo, con la variable person, usted podría hacer:

1
>>> person = db.person

También puede almacenar un campo en una variable tal como name. Por ejemplo, también podría hacer:

1
>>> name = person.name

Usted inclusive puede construir una consulta (usando operadores como ==, !=, <, >, <=, >=, like, belongs) y almacenar la consulta en una variable q como en:

1
>>> q = name=='Alex'

Cuando llame a db con una consulta, usted define un conjunto de registros. Puede guardarlo en una variable s y escribir:

1
>>> s = db(q)

Note que ninguna consulta a base de datos se ha hecho hasta ahora. DAL + Consulta simplemente define un conjunto de registros en esta base de datos que coinciden con la consulta. Web2py determina desde la consulta cual tabla (o tablas) están involucradas y, de hecho, no hay necesidad de especificar eso.

select

Dado un conjunto s, usted puede buscar todos los registros con el comando de selección select:

1
>>> rows = s.select()

Devuelve un objeto iterable de clase gluon.sql.Rows cuyos elementos son objetos de fila “Row”. Los objetos gluon.sql.Rows actúan como diccionarios, pero sus elementos también puede ser accedidos como atributos, como gluon.storage.Storage. El primero difiere del último porque sus valores son de sólo lectura.

Los objetos de filas “Rows” permiten hacer un bucle sobre el resultado de la selección e imprimir los valores de los campos seleccionas para cada fila:

1
2
3
>>> for row in rows:
        print row.id, row.name
1 Alex

Puede hacer todos los pasos en una sola sentencia:

1
2
3
>>> for row in db(db.person.name=='Alex').select():
        print row.name
Alex

El comando de selección puede tomar argumentos. Todos los argumentos sin nombre son interpretados como los nombres de los campos que quiere buscar. Por ejemplo, usted puede ser explícito al querer buscar los campos “id” y “name”:

1
2
3
4
5
>>> for row in db().select(db.person.id, db.person.name):
        print row.name
Alex
Bob
Carl

El atributo de tabla ALL le permite especificar todos los campos:

1
2
3
4
5
>>> for row in db().select(db.person.ALL):
        print row.name
Alex
Bob
Carl

Y web2py entiende que si usted pide todos los registros de la tabla person (id > 0) sin información adicional, es que usted quiere todos los campos de la tabla person.

Atajos

La DAL soporta varios atajos que simplifiquen el código. En particular:

1
myrecord = db.mytable[id]

Devuelve el registro con el id dado si existe. Si el id no existe, devuelve None. La anterior sentencia es equivalente a

1
myrecord = db(db.mytable.id==id).select().first()

Puede borrar los registros por id:

1
del db.mytable[id]

Y esto es equivalente a

1
db(db.mytable.id==id).delete()

Y borra todos los registros con el id dado, siempre que exista.

Puede insertar registros:

1
db.mytable[0] = dict(myfield='somevalue')

Es equivalente a

1
db.mytable.insert(myfield='somevalue')

Y crea un nuevo registro con los valores de campo especificados por el diccionario en el lado derecho.

Puede actualizar registros:

1
db.mytable[id] = dict(myfield='somevalue')

Que es equivalente a

1
db(db.mytable.id==id).update(myfield='somevalue')

Y actualiza el registro existente con los valores de campo especificados por el diccionario del lado derecho.

Buscando una fila Row

Otra sintaxis conveniente es la siguiente:

1
2
3
record = db.mytable(id)
record = db.mytable(db.mytable.id==id)
record = db.mytable(id,myfield='somevalue')

Aparentemente similar a db.mytable[id], la anterior sintaxis es más flexible y segura. Antes que todo verifica si id es un entero (o str(id) es un entero) y devuelve None de no serlo (nunca levanta una excepción). También permite especificar múltiples condiciones a ser cumplidas por el registro. Si no son cumplidas, también devuelve None.

Selecciones recursivas

Considere la anterior tabla de personas “person” y una nueva tabla “dog” que hace referencia a “person”:

1
>>> db.define_table('dog', Field('name'), Field('owner',db.person))

Por cada fila de “dog” es posible buscar no sólo los campos de la tabla seleccionada (dog) sino también de tablas vinculadas (recursivamente):

1
>>> for dog in dogs: print dog.info, dog.owner.name

Aqui dog.owner.name requiere una selección de base de datos para cada perro (dog) en perros (dog) y por lo tanto es ineficiente. Sugerimos usar uniones cuando sea posibles en ver de selecciones recursivas, sin embargo esto es conveniente y práctico cuando se accede a registros individuales.

También lo puede hacer de la manera inversa, seleccionando los perros referenciados por una persona en la tabla “person”:

1
2
3
person =  db.person(id)
for dog in person.dog.select(orderby=db.dog.name):
    print person.name, 'owns', dog.name

En esta última expresión, person.dog es un atajo para

1
db(db.dog.owner==person.id)

por ejemplo, el conjunto de perros referenciados por la actual persona. Esta sintaxis se rompe si la tabla referenciada tiene múltiples tablas que hacen referencia a ella. En ese caso uno necesita ser más explícito y usar una Consulta completa.

Serializando las filas en las vistas

El resultado de una selección puede ser mostrado en una vista con la siguiente sintaxis:

{{extend 'layout.html'}}
<h1>Records</h2>
{{=db().select(db.person.ALL)}}

Y es convertida automáticamente en una tabla HTML con un encabezado que contiene los nombres de las columnas y una fila por cada registro. Las filas son marcadas como de clase alternante “even” y clase “odd”. Por debajo, la fila es primero convertida en un objeto SQLTABLE (no debe ser confundido con Table) y luego es serializada. Los valores extraídos de la base de datos también son formateados por los validadores asociados con el campo y luego son escapados. (Nota: Usando una base de datos de ésta manera en una vista usualmente no es considerada una buena practica MVC).

Sin embargo es posible y algunas veces conveniente llamar explícitamente a SQLTABLE.

El constructor SQLTABLE toma los siguiente argumentos opcionales:

  • Vincular “linkto”: el URL o una acción a ser usada para vincular campos referenciados (por defecto None)
  • Cargar “upload”: el URL o la acción de descarga para permitir la descarga de archivos cargados (por defecto None)
  • Encabezados “headers”: un diccionario asignando los nombres de campos a sus etiquetas para ser usados como encabezados (por defecto {}). Puede ser también una instrucción. Actualmente soportamos headers=’filename:capitalize’.
  • Truncar “truncate”: el número de caracteres para truncar valores largos en la tabla (por defecto es 16)
  • Columnas “colums”: la lista de nombres de archivos a ser mostrados como columnas. Aquellos no listados no serán mostrados. (por defecto toma todos)
  • Atributos “**attributes”: atributos ayudantes genéricos a ser pasados al objeto TABLE más externo.

He aquí un ejemplo:

{{extend 'layout.html'}}
<h1>Records</h2>
{{=SQLTABLE(db().select(db.person.ALL),
   headers='fieldname:capitalize',
   truncate=100,
   upload=URL('download'))
}}

orderby, groupby, limitby, distinct

El comando select toma cinco argumentos opcionales: ordenar por “orderby”, agrupar por “groupby”, limitar por “limitby”, sobrante “left” y cache “cache”. Aquí discutimos los primeros tres:

Usted puede hacer una búsqueda de los registros ordenados por nombre:

1
2
3
4
>>> for row in db().select(db.person.ALL, orderby=db.person.name):
        print row.name
Alex
Bob

Puede hacer una búsqueda de los registros ordenamos por nombre en orden inverso (note la tilde):

1
2
3
4
5
>>> for row in db().select(db.person.ALL, orderby=~db.person.name):
        print row.name
Carl
Bob
Alex

Y usted también puede hacer una búsqueda de los registros de acuerdo a múltiples campos concatenandolos con un “|”:

1
2
3
4
5
>>> for row in db().select(db.person.ALL, orderby=db.person.name|db.person.id):
        print row.name
Carl
Bob
Alex

Usando groupby junto con orderby usted puede agrupar registros con el mismo valor para el campo especificado (ésto es específico del sistema de la base de datos, y no esta en GAE):

1
2
3
4
5
6
>>> for row in db().select(db.person.ALL, orderby=db.person.name,
                        groupby=db.person.name):
        print row.name
Alex
Bob
Carl

Con el argumento distint=True, usted puede especificar que sólo quiere seleccionar registros distintos. Ésto tiene el mismo efecto que agrupar usando todos los campos específicos excepto que no requiere ordenar. Cuando se usa disctint es importante no seleccionar todos “ALL” los campos, y en particular no seleccionar el campo “id”, de lo contrario todos los registros serán siempre distintos.

He aquí un ejemplo:

1
2
3
4
5
>>> for row in db().select(db.person.name, distinct=True):
        print row.name
Alex
Bob
Carl

Con limitby, usted puede seleccionar un subconjunto de los registros (en este caso, los primeros dos comenzando en cero):

1
2
3
4
>>> for row in db().select(db.person.ALL, limitby=(0, 2)):
        print row.name
Alex
Bob

Actualmente, “limitby” sólo es parcialmente soportado en MSSQL ya que la base de datos de Microsoft no provee un mecanismo para hacer búsqueda de un subconjunto de registros no comenzando por 0.

Operadores lógicos

Los consultas pueden ser combinadas usando el operador binario AND “&”:

1
2
3
>>> rows = db((db.person.name=='Alex') & (db.person.id>3)).select()
>>> for row in rows: print row.id, row.name
4 Alex

Y el operador binario OR “|”:

1
2
3
>>> rows = db((db.person.name=='Alex') | (db.person.id>3)).select()
>>> for row in rows: print row.id, row.name
1 Alex

Puede negar una consulta (o sub-consulta) con el operador binario !=:

1
2
3
4
>>> rows = db((db.person.name!='Alex') | (db.person.id>3)).select()
>>> for row in rows: print row.id, row.name
2 Bob
3 Carl

O negando explícitamente con el operador unario |:

1
2
3
4
>>> rows = db((db.person.name=='Alex') | (db.person.id>3)).select()
>>> for row in rows: print row.id, row.name
2 Bob
3 Carl

Debido a restricciones de Python en la sobrecarga de los operadores “AND” y “OR”, éstos no pueden ser usados en la formación de consultas. Los operadores binarios deben ser usados en su lugar.

count, delete, update

Usted puede contar los registros en un conjunto:

1
2
>>> print db(db.person.id > 0).count()
3

También puede borrar los registros en un conjunto:

1
>>> db(db.person.id > 3).delete()

Y puede actualizar los registros en un conjunto pasando los argumentos con nombre correspondientes a los campos que necesitan ser actualizados:

1
>>> db(db.person.id > 3).update(name='Ken')

Expresiones

El valor asignado a una sentencia de actualización puede ser una expresión. Por ejemplo considere el modelo

1
2
3
4
5
>>> db.define_table('person',
        Field('name'),
        Field('visits', 'integer', default=0))
>>> db(db.person.name == 'Massimo').update(
        visits = db.person.visits + 1)

Los valores usados en las consultas también pueden ser expresiones

1
2
3
4
5
>>> db.define_table('person',
        Field('name'),
        Field('visits', 'integer', default=0),
        Field('clicks', 'integer', default=0))
>>> db(db.person.visits == db.person.clicks + 1).delete()

update_record

web2py también permite actualizar un único registro que ya está en memoria usando update_record

1
2
3
>>> rows = db(db.person.id > 2).select()
>>> row = rows[0]
>>> row.update_record(name='Curt')

Ésto no debe ser confundido con

1
>>> row.update(name='Curt')

Porque para una única fila, el método update actualiza el objeto de fila “row” pero no el registro de la base de datos, como sucede en el caso de update_record.

Primero “first” y último “last

Dado un objeto Rows que contiene registros:

1
2
3
>>> rows = db(query).select()
>>> first_row = rows.first()
>>> last_row = rows.last()

Son equivalentes a:

1
2
>>> first_row = rows[0] if len(rows)>0 else None
>>> last_row = rows[-1] if len(rows)>0 else None

as_dict y as_list

Un objeto Row puede ser serializado en un diccionario regular usando el método as_dict() y un objeto Rows puede ser serializado en una lista de diccionarios usando el método as_list(). Aquí hay unos ejemplos:

1
2
3
>>> rows = db(query).select()
>>> rows_list = rows.as_list()
>>> first_row_dict = rows.first().as_dict()

Estos métodos son convenientes para pasar filas a vistas genéricas y/o para guardar filas en sesiones (ya que los objetos de filas por sí mismos no pueden ser serializados debido a que contienen referencia a una conexión de base de datos abierta):

1
2
3
>>> rows = db(query).select()
>>> session.rows = rows # not allowed!
>>> session.rows = rows.as_list() # allowed!

find, exclude, sort

Hay momentos en los que uno necesita realizar dos selecciones y una contiene un subconjunto de una selección previa. En éste caso no tiene sentido acceder de nuevo a la base de datos. Los objetos find, exclude y sort le permiten manipular los objetos de filas y generar otro sin acceder a la base de datos. Más específicamente:

  • Encontrar “find” devuelve un nuevo conjunto de filas filtradas por una condición y deja el original sin cambios.
  • Excluir “exclude” devuelve un nuevo conjunto de filas filtradas por una condición y las elimina de las filas originales.
  • Ordenar “sort” devuelve un nuevo conjunto de filas ordenadas por una condición y deja el conjunto original sin cambios.

Todos estos métodos toman un sólo argumentos, una función que actúa en cada fila individual.

He aquí un ejemplo de uso:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
>>> db.define_table('person',Field('name'))
>>> db.insert(name='John')
>>> db.insert(name='Max')
>>> db.insert(name='Alex')
>>> rows = db(db.person.id>0).select()
>>> for row in rows.find(lambda row: row.name[0]=='M'): print row.name
Max
>>> print len(rows)
3
>>> for row in rows.extract(lambda row: row.name[0]=='M'): print row.name
Max
>>> print len(rows)
2
>>> for row in rows.sort(lambda row: row.name): print row.name
Alex
John

Ellos pueden ser combinados:

1
2
3
4
5
>>> rows = db(db.person.id>0).select()
>>> rows = rows.find(lambda row: 'x' in row.name).sort(lambda row: row.name)
>>> for row in rows: print row.name
Alex
Max

Campos calculados

Los campos de la DAL pueden tener un atributo compute. Éste debe ser una función (o lambda) que toma un obejto Fila y retorna un valor por el campo. Cuando un nuevo registro es insertado (o actualizado), si un valor para el campo no se provee, web2py trata de calcularlo desde los valores de los otros campos usando la función compute. He aquí un ejemplo:

1
2
3
4
5
6
7
>>> db.define_table('item',
        Field('unit_price','double'),
        Field('quantity','integer'),
        Field('total_price',compute=lambda r: r['unit_price']*r['quantity']))
>>> r = db.item.insert(unit_price=1.99, quantity=5)
>>> print r.total_price
9.95

Note que el valor calculado es almacenado en la base de datos y no es calculado en recuperación, como en el caso de las campos virtuales, descritos más adelante. Dos aplicaciones típicas de los campos calculados son:

  • En aplicaciones wiki, para guardar como html el texto de entrada wiki procesado, para evitar reprocesamiento en cada solicitud.
  • Para buscar, para calcular valores normalizados para un campo, para ser usados en búsqueda.

Campos virtuales

Los campos virtuales son también campos calculados (como en la subsección previa) pero difieren de ellos porq son “virtuales” en el sentido de que no son guardados en la base de datos y son calculados cada vez que los registros son extraídos de la base de datos. Pueden ser usados para simplificar el código del usuario sin usar almacenamiento adicional pero no pueden ser usados para búsqueda.

Para definir uno o más campos virtuales, usted tiene que definir una clase contenedora, instanciarla y vincularla a una tabla o a una selección. Por ejemplo, considere la siguiente tabla:

1
2
3
>>> db.define_table('item',
        Field('unit_price','double'),
        Field('quantity','integer'),

Uno puede definir un campo virtual total price como

1
2
3
4
>>> class MyVirtualFields:
        def total_price(self):
            return self.item.unit_price*self.item.quantity
>>> db.item.virtualfields.append(MyVirtualFields())

Note que cada método de la clase que toma un único argumento (self) es un nuevo campo virtual. self se refiere a cada una de las filas del mismo en la selección. Los valores de los campos son referidos como rutas completas como en self.item.unit_price. La tabla es vinculada al campo virtual agregando una instancia de la clase al atributo de la tabla virtualfields.

Los campos virtuales también pueden accesar campos recursivos como en

1
2
3
4
5
6
7
8
9
>>> db.define_table('item',
        Field('unit_price','double'))
>>> db.define_table('order_item',
        Field('item',db.item),
        Field('quantity','integer'))
>>> class MyVirtualFields:
        def total_price(self):
            return self.order_item.item.unit_price*self.order_item.quantity
>>> db.order_item.virtualfields.append(MyVirtualFields())

Note el acceso a campos recursivos self.order_item.item.unit_price donde self es el registro en bucle.

Ellos también pueden actuar en el resultado de una unión “JOIN”

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
>>> db.define_table('item',
        Field('unit_price','double'))
>>> db.define_table('order_item',
        Field('item',db.item),
        Field('quantity','integer'))
>>> rows =  db(db.order_item.item==db.item.id).select()
>>> class MyVirtualFields:
        def total_price(self):
            return self.item.unit_price*self.order_item.quantity
>>> rows.setvirtualfields(order_item=MyVirtualFields())
>>> for row in rows: print row.order_item.total_price

Note como en éste caso la sintaxis es diferente. El campo virtual accede a self.item.unit_price y a self.order_item.quantity que pertenece a la selección de unión. El campo virtual es adjuntado a las filas de la tabla usando el método setvirtualfields del objeto de filas. Éste método toma un número arbitrario de argumentos nombrados y puede ser usado para establecer múltiples campos virtuales, definidos en múltiples clases, y adjuntarlos a múltiples tablas:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
>>> class MyVirtualFields1:
        def discounted_unit_price(self):
            return self.item.unit_price*0.90
>>> class MyVirtualFields2:
        def total_price(self):
            return self.item.unit_price*self.order_item.quantity
        def discounted_total_price(self):
            return self.item.discounted_unit_price*self.order_item.quantity
>>> rows.setvirtualfields(item=MyVirtualFields1(),order_item=MyVirtualFields2())
>>> for row in rows: print row.order_item.discounted_total_price

Los campos virtuales pueden ser holgazanes, todo lo que necesitan es retornar una función y ser accesados llamando a la función:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
>>> db.define_table('item',
        Field('unit_price','double'),
        Field('quantity','integer'),
>>> class MyVirtualFields:
        def lazy_total_price(self):
            def lazy(self=self):
                return self.item.unit_price*self.item.quantity
            return lazy
>>> db.item.virtualfields.append(MyVirtualFields())
>>> for item in db(db.item.id>0).select(): print item.lazy_total_price()*

De manera más corta usando la función lambda:

1
2
3
>>> class MyVirtualFields:
          def lazy_total_price(self):
               return lambda self=self: self.item.unit_price*self.item.quantity

Relación de una a muchas

Para ilustrar como implementar relaciones de una a muchas con la DAL de web2py, defina otra tabla “dog” que haga referencia a la tabla “person” que redefinimos aquí:

1
2
3
4
5
6
7
>>> db.define_table('person',
                    Field('name'),
                    format='%(name)s')
>>> db.define_table('dog',
                    Field('name'),
                    Field('owner', db.person),
                    format='%(name)s')

La tabla “dog” tiene dos campos, el nombre del perro y el dueño del mismo. Cuando un tipo de campo es otra tabla, se intenta que el campo haga referencia a la otra tabla por su id. De hecho, usted puede imprimir el valor de tipo actual y obtener:

1
2
>>> print db.dog.owner.type
reference person

Ahora inserte tres perros, dos pertenecientes a Alex y uno a Bob:

1
2
3
4
5
6
>>> db.dog.insert(name='Skipper', owner=1)1
1
>>> db.dog.insert(name='Snoopy', owner=1)
2
>>> db.dog.insert(name='Puppy', owner=2)
3

Puede hacer selecciones como las hace para cualquier otra tabla:

1
2
3
4
>>> for row in db(db.dog.owner==1).select():
        print row.name
Skipper
Snoopy

Debido a que un perro hace referencia a una persona, una persona puede tener muchos perros, así un registro de la tabla personas “person” ahora adquiere un nuevo atributo perro, que es un conjunto que define el perro de esa persona. Ésto permite hacer un bucle sobre todas las personas y buscar sus perros fácilmente:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
>>> for person in db().select(db.person.ALL):
        print person.name
        for dog in person.dog.select():
            print '    ', dog.name
Alex
     Skipper
     Snoopy
Bob
     Puppy
Carl

Uniones internas

Otra manera de lograr un resultado similar es usando una unión, específicamente una unión interna “INNER JOIN”. Web2py lleva a cabo uniones de manera automática y transparente cuando la consulta vincula dos o más tablas como en el siguiente ejemplo:

1
2
3
4
5
6
>>> rows = db(db.person.id==db.dog.owner).select()
>>> for row in rows:
        print row.person.name, 'has', row.dog.name
Alex has Skipper
Alex has Snoopy
Bob has Puppy

Observe que web2py hizo una unión, así que rows ahora contiene dos registros, uno de cada tabla, vinculados entre ellos. Debido a que dichos registros pueden tener campos con nombres en conflicto, usted necesita especificar la tabla cuando se extraiga el valor de un campo en una fila. Ésto significa que mientras antes usted podía hacer:

1
row.name

Y era obvio si éste era el nombre de una persona o un perro, en el resultado de una unión usted debe ser más explícito y decir:

1
row.person.name

o

1
row.dog.name

Union externa por la izquierda

Note que Carl no apareció en la anterior lista debido a que no tiene perros. Si usted quiere seleccionar personas (tengan perros o no) y sus perros (si tienen alguno), entonces usted necesita realizar una unión externa por la izquierda (LEFT OUTER JOIN). Ésto se hace usando el argumentos “left” del comando select. He aquí un ejemplo:

1
2
3
4
5
6
7
>>> rows=db().select(db.person.ALL, db.dog.ALL, left=db.dog.on(db.person.id==db.dog.owner))
>>> for row in rows:
        print row.person.name, 'has', row.dog.name
Alex has Skipper
Alex has Snoopy
Bob has Puppy
Carl has None

Donde:

1
left = db.dog.on(...)

Realiza la consulta de unión por la izquierda. Aquí el argumento de db.dog.on es la condición requerida para la union (los mismo usado anteriormente para la unión interna). En el caso de una unión por la izquierda, es necesario ser explícito acerca de cuales campos se debe seleccionar.

Agrupando y Contando

Cuando se hacen uniones, algunas veces usted quiere agrupar filas de acuerdo a ciertos criterios y luego contarlas. Por ejemplo, cuente el número de perros pertenecientes a cada persona. Web2py permite ésto también. Primero, usted necesita un operador contador. Segundo, debe unir la tabla persona con la tabla de perros por persona. Tercero, debe seleccionar todas las filas (persona+perro), agruparlas por persona, y contarlas mientras las agrupa:

1
2
3
4
5
>>> count = db.person.id.count()
>>> for row in db(db.person.id==db.dog.owner).select(db.person.name, count, groupby=db.person.id):
        print row.person.name, row[count]
Alex 2
Bob 1

Note que el operador contador (que está dentro de web2py) es usado como un campo. El único problema aquí es como recuperar la información. Cada fila contiene claramente una persona y la cuenta, pero la cuenta no es un campo de una persona ni tampoco es una tabla. ¿Entonces donde va? Va en el objeto de almacenamiento que representa el registro con una clave igual a la expresión de consulta en sí misma.

Muchos a Muchos

En los ejemplos previos, permitimos que un perro tuviese un dueño pero una persona podía tener muchos perros. ¿Qué pasa si Skipper pertenece a Alex y Curl? Esto requiere de una relación de muchos a muchos, y es realizada por medio de una tabla intermedia que vincula una persona con un perro a través de una relación de propiedad.

Aquí está como se hace:

1
2
3
4
5
6
7
>>> db.define_table('person',
                    Field('name'))
>>> db.define_table('dog',
                    Field('name'))
>>> db.define_table('ownership',
                    Field('person', db.person),
                    Field('dog', db.dog))

La relación de propiedad existente puede ser reescrita ahora como:

1
2
3
>>> db.ownership.insert(person=1, dog=1) # Alex owns Skipper
>>> db.ownership.insert(person=1, dog=2) # Alex owns Snoopy
>>> db.ownership.insert(person=2, dog=3) # Bob owns Puppy

Ahora usted puede agregar la nueva relación de que Curl co-posee a Skipper:

1
>>> db.ownership.insert(person=3, dog=1) # Curt owns Skipper too

Debido a que ahora usted tiene relaciones triples entre tablas, puede ser conveniente definir un nuevo conjunto en el cual realizar las operaciones:

1
>>> persons_and_dogs = db((db.person.id==db.ownership.person) | (db.dog.id==db.ownership.dog))

Ahora resulta sencillo seleccionar todas las personas y sus perros desde el nuevo conjunto:

1
2
3
4
5
6
>>> for row in persons_and_dogs.select():
        print row.person.name, row.dog.name
Alex Skipper
Alex Snoopy
Bob Puppy
Curt Skipper

De manera similar usted puede buscar todos lo perros propiedad de Alex:

1
2
3
4
>>> for row in persons_and_dogs(db.person.name=='Alex').select():
        print row.dog.name
Skipper
Snoopy

Y todos lo dueños de Skipper:

1
2
3
4
>>> for row in persons_and_dogs(db.dog.name=='Skipper').select():
        print row.owner.name
Alex
Curt

Una alternativa más ligera a las relaciones muchos a muchos es el etiquetado. Será discutido en el contexto del validador IS_IN_DB. El etiquetado trabaja incluso en sistemas de base de datos que no soportan uniones, como el motor de aplicaciones de Google.

Muchos a Muchos, list:<type>, y contains

web2py provee los siguientes tipos de campos especiales:

list:string
list:integer
list:reference <table>

Ellos pueden contener listas de cadenas, de enteros y de referencias respectivamente.

En el motor de aplicaciones de Google, list:string es asignado a StringListProperty, los otros dos son asignados a ListProperty(int). En bases de datos relacionales todos son asignados a campos de texto que contienen la lista de los ítem separados por |. Por ejemplo [1,2,3] es asignado a |1|2|3|.

Para listas de cadenas los ítems son escapados de manera que cualquier | en los ítems sea reemplazado por ||. De todos modos ésta es una representación interna y es transparente al usuario.

Puede usar list:string, por ejemplo, de la siguiente manera:

1
2
3
4
5
6
7
8
>>> db.define_table('product',
        Field('name'),
        Field('colors','list:string'))
>>> db.product.colors.requires=IS_IN_SET(('red','blue','green'))
>>> db.product.insert(name='Toy Car',colors=['red','green'])
>>> products = db(db.product.colors.contains('red')).select()
>>> for item in products: print item.name, item.colors
Toy Car ['red', 'green']

list:integer trabaja en la misma manera pero los ítems deben ser enteros.

Como siempre, los requerimientos son forzados a nivel de los formularios, no a nivel del insert.

Para campos list:<type> el operador contains(value) se asigna a una consulta no trivial que busca listas que contengan value. El operador contains tambien trabaja para cadenas “string” regulares y campos de texto “text” y los asigna a LIKE ‘%value%’.

El list:reference y el operador contains(value) son particularmente útiles para des-normalizar relaciones muchos a muchos. He aquí un ejemplo:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
>>> db.define_table('tag',Field('name'),format='%(name)s')
>>> db.define_table('product',
        Field('name'),
        Field('tags','list:reference tag'))
>>> a = db.tag.insert(name='red')
>>> b = db.tag.insert(name='green')
>>> c = db.tag.insert(name='blue')
>>> db.product.insert(name='Toy Car',tags=[a, b, c])
>>> products = db(db.product.tags.contains(b)).select()
>>> for item in products: print item.name, item.tags
Toy Car [1, 2, 3]
>>> for item in products:
        print item.name, db.product.tags.represent(item.tags)
Toy Car red, green, blue

Note que un campo etiqueta list:reference obtiene una restricción por defecto

1
requires = IS_IN_DB(db,'tag.id',db.tag._format,multiple=True)

Que produce un buzón múltiple SELECT/OPTION en los formularios.

También note que este campo saca un atributo por defecto represent que representa la lista de referencias como una lista separada por comas de las referencias formateadas. Ésto es usado en las formularios de lectura y SQLTABLEs.

Mientras list:reference tiene un validador por defecto y una representación por defecto, list:integer y list:string no. Así que éstos dos necesitan un validador IS_IN_SET o IS_IN_DB si quiere usarlos en los formularios.

Otros operadores

web2py tiene otros operadores que proveen un API para accesar operadores SQL equivalentes. Definamos otra tabla “log” para almacenar los eventos de seguridad, su tiempo y severidad, donde severidad es un número entero.

1
2
3
>>> db.define_table('log', Field('event'),
                           Field('event_time', 'datetime'),
                           Field('severity', 'integer'))

Como antes, inserte unos pocos eventos, un escaneo de puerto, una inyección xss y un inicio de sesión no autorizado. Para el ejemplo, puede registrar los eventos con mismo tiempo de evento pero con distinta severidad (1,2,3 respectivamente).

1
2
3
4
5
6
7
8
>>> import datetime
>>> now = datetime.datetime.now()
>>> print db.log.insert(event='port scan', event_time=now, severity=1)
1
>>> print db.log.insert(event='xss injection', event_time=now, severity=2)
2
>>> print db.log.insert(event='unauthorized login', event_time=now, severity=3)
3

like, startswitch, contains, upper, lower

Los campos tienen un operador de comparación que se puede usar para comparar cadenas:

1
2
3
>>> for row in db(db.log.event.like('port%')).select():
        print row.event
port scan

Aquí “%port%” indica una cadena que comienza con “port”. El signo de porcentaje, “%”, es un carácter comodín que significa “cualquier secuencia de caracteres”.

web2py provee algunos atajos:

1
2
db.mytable.myfield.startswith('value')
db.mytable.myfield.contains('value')

Que son equivalente respectivamente a:

1
2
db.mytable.myfield.like('value%')
db.mytable.myfield.like('%value%')

No que contains tiene un significado especial para los campos list:<type> y fue discutido en una sección previa.

De manera similar, puede usar métodos upper y lower para convertir el valor del campo a mayúsculas o minúsculas, y también puedes combinarlos con el operador de comparación.

1
2
3
>>> for row in db(db.log.event.upper().like('PORT%')).select():
        print row.event
port scan

year, month, day, hour, minutes, seconds

Los campos date y datetime tienen métodos dia “day”, mes “month” y año “year”, Los campos datetime y time tienen métodos hora “hour”, minutos “minutes” y segundos “seconds”. He aquí un ejemplo:

1
2
3
4
5
>>> for row in db(db.log.event_time.year()==2009).select():
        print row.event
port scan
xss injection
unauthorized login

belongs

El operador SQL IN se realiza por medio del método de permanencia que devuelve True cuando el valor del campo pertenece a conjunto específico (lista de tuplas):

1
2
3
4
>>> for row in db(db.log.severity.belongs((1, 2))).select():
        print row.event
port scan
xss injection

La DAL también permite una selección anidada como el argumento del operador de pertenencia. La única salvedad es que la selección anidada tiene que ser una _select, no una select, y sólo un campo tiene que ser seleccionado explícitamente, el que define el conjunto.

1
2
3
4
5
6
>>> bad_days = db(db.log.severity==3)._select(db.log.event_time)
>>> for row in db(db.log.event_time.belongs(bad_days)).select():
        print row.event
port scan
xss injection
unauthorized login

Previamente, usted ha usado el operador contador para contar los registros. De manera similar, usted puede usar el operador suma para sumar los valores de un campo específico de un grupo de registros. Como en el caso del contador, el resultado de la suma es recuperado por medio del objeto de almacenamiento:

1
2
3
>>> sum = db.log.severity.sum()
>>> print db().select(sum).first()[sum]
6

Generando SQL crudo

Algunas veces usted necesita generar el SQL pero no ejecutarlo. Ésto es fácil de hacer con web2py ya que todos los comandos que realizan operaciones de entrada y salida de una base de datos tienen un comando equivalente que no lo hace, y que simplemente retornan el SQL que hubiese tenido que ser ejecutado. Estos comandos tienen los mismos nombres y sintaxis que los funcionales, con la salvedad que comienzan con un guión bajo:

Aquí está _insert

1
2
>>> print db.person._insert(name='Alex')
INSERT INTO person(name) VALUES ('Alex');

Aquí _count

1
2
>>> print db(db.person.name=='Alex')._count()
SELECT count(*) FROM person WHERE person.name='Alex';

Aquí _select

1
2
>>> print db(db.person.name=='Alex')._select()
SELECT person.id, person.name FROM person WHERE person.name='Alex';

Ahora _delete

1
2
>>> print db(db.person.name=='Alex')._delete()
DELETE FROM person WHERE person.name='Alex';

Y finalmente, _update

1
2
>>> print db(db.person.name=='Alex')._update()
UPDATE person SET  WHERE person.name='Alex';

Por otra parte usted siempre puede usar db._lastsql para retornar los códigos SQL más recientes, ya sea si fue ejecutado manualmente usando executesql o si fue SQL generado por la DAL.

Exportando e Importando Datos

CSV (una tabla a la vez)

Cuando el objeto DALRows (filas de DAL) es convertido a una cadena, es serializado automáticamente en CSV:

1
2
3
4
5
6
>>> rows = db(db.person.id==db.dog.owner).select()
>>> print rows
person.id,person.name,dog.id,dog.name,dog.owner
1,Alex,1,Skipper,1
1,Alex,2,Snoopy,1
2,Bob,3,Puppy,2

Usted puede serializar una única tabla en CSV y almacenarla en un archivo “test.csv”:

1
>>> open('test.csv', 'w').write(str(db(db.person.id).select()))

Y puede leerla fácilmente con:

1
>>> db.person.import_from_csv_file(open('test.csv', 'r'))

Al importar, web2py busca por los nombres de los campos en el encabezado CSV. En este ejemplo, consigue dos columnas: “person.id” y “person.name”. Ignora el prefijo “person.”, e ignora los campos “id”. Luego todos los registros son agregados y asignados nuevos ids. Ambas operaciones pueden ser llevadas a cabo por medio de la interfaz web de administración de aplicaciones.

CSV (todas la tablas a la vez)

En web2py, usted puede respaldar/restaurar una base de datos completa con dos comandos:

Para exportar:

1
>>> db.export_to_csv_file(open('somefile.csv', 'wb'))

Para importar:

1
>>> db.import_from_csv_file(open('somefile.csv', 'rb'))

Este mecanismo puede usado incluso si la base de datos a importar es de un tipo diferente que la base de datos a exportar. Los datos son almacenados en “somefile.csv” como un archivo CSV donde cada tabla comienza con una línea que indica el nombre de la tabla, y otra línea con los nombres de los archivos:

TABLE tablename
field1, field2, field3, ...

Dos tablas son separadas por rnrn. El archivo termina con la línea

1
END

El archivo no incluye los archivos cargados si éstos no están almacenados en la base de datos. En cualquier caso es suficientemente fácil comprimir en zip la carpeta de cargas de manera separada.

Al importar, los nuevos registros serán agregados a la base de datos si no está vacía. En general los nuevos registros importados no tendrán el mismo id de registro que el registro original (que queda guardado) pero web2py restaurará las referencias para que no estén rotas, incluso si los valores id puedan cambiar.

Si una tabla contiene un campo llamado “uuid”, este campo será usado para identificar los duplicados. También, si un registro importado tiene el mismo “uuid” que un registro existente, el registro previo será actualizado.

CVS y sincronización remota de base de datos

Considere el siguiente modelo

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
db = DAL('sqlite:memory:')
db.define_table('person',
    Field('name'),
    format='%(name)s')
db.define_table('dog',
    Field('owner', db.person),
    Field('name'),
    format='%(name)s')
if not db(db.person.id>0).count():
    id = db.person.insert(name="Massimo")
    db.dog.insert(owner=id, name="Snoopy")

Cada registro es identificado por un ID y referenciado por ese ID. Si usted tiene dos copias de la base de datos usada por distintas aplicaciones web2py, el ID es único solo dentro de cada base de datos y no a través de todas las bases de datos. Éste es un problema al migrar registros desde distintas bases de datos.

A fin de hacer que cada registro sea identificable de manera única a lo largo de las bases de datos, ellas deben:

  • Tener un id único (UUID),
  • Tener una fecha de evento “event_time” (para averiguar cual es más nuevo si hay múltiples copias),
  • Referenciar el UUID en vez del id.

Ésto puede ser logrado sin modificar web2py. Aquí está lo que se debe hacer:

1.Cambie el modelo mostrado arriba a:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
db.define_table('person',
    Field('uuid', length=64, default=uuid.uuid4()),
    Field('modified_on', 'datetime', default=now),
    Field('name'),
    format='%(name)s')
db.define_table('dog',
    Field('uuid', length=64, default=uuid.uuid4()),
    Field('modified_on', 'datetime', default=now),
    Field('owner', length=64),
    Field('name'),
    format='%(name)s')
db.dog.owner.requires = IS_IN_DB(db,'person.uuid','%(name)s')
if not db(db.person.id).count():
    id = uuid.uuid4()
    db.person.insert(name="Massimo", uuid=id)
    db.dog.insert(owner=id, name="Snoopy")

2.Cree una acción de controlador para exportar la base de datos:

1
2
3
4
5
def export():
    s = StringIO.StringIO()
    db.export_to_csv_file(s)
    response.headers['Content-Type'] = 'text/csv'
    return s.getvalue()

3.Cree una acción de controlador para importar una copia guardada de la otra base de datos y sincronice los registros:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
def import_and_sync():
    form = FORM(INPUT(_type='file', _name='data'), INPUT(_type='submit'))
    if form.accepts(request.vars):
        db.import_from_csv_file(form.vars.data.file,unique=False)
        # for every table
        for table in db.tables:
            # for every uuid, delete all but the latest
            items = db(db[table].id>0).select(db[table].id,
                       db[table].uuid,
                       orderby=db[table].modified_on,
                       groupby=db[table].uuid)
            for item in items:
                db((db[table].uuid==item.uuid)&\
                   (db[table].id!=item.id)).delete()
    return dict(form=form)

4.Cree un índice manualmente para hacer que la búsqueda por uuid sea más rápida.

Note que los pasos 2 y 3 funcionan para cada modelo de base de datos; no son específicos de este ejemplo.

De manera alterna, usted puede usar XML-RPC para exportar/importar el archivo.

Si el registro hace referencia a archivos cargados, usted también necesita exportar/importar el contenido de la carpeta de cargas. Note que los archivos allí ya están etiquetados con UUIDs así que usted no necesita preocuparse por conflictos de nombre y referencias.

HTML/XML (una tabla a la vez)

Los objetos DALRows también tienen un método (como ayudantes) xml que lo serializa en XML/HTML:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
>>> rows = db(db.person.id > 0).select()
>>> print rows.xml()
<table>
  <thead>
    <tr>
      <th>person.id</th>
      <th>person.name</th>
      <th>dog.id</th>
      <th>dog.name</th>
      <th>dog.owner</th>
    </tr>
  </thead>
  <tbody>
    <tr class="even">
      <td>1</td>
      <td>Alex</td>
      <td>1</td>
      <td>Skipper</td>
      <td>1</td>
    </tr>
    ...
  </tbody>
</table>

Si usted necesita serializar el objeto DALRows en cualquier otro formato XML con etiquetas personalizadas, usted puede hacerlo fácilmente usando el ayudante universal TAG y la notación *:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
>>> rows = db(db.person.id > 0).select()
>>> print TAG.result(*[TAG.row(*[TAG.field(r[f], _name=f) \
          for f in db.person.fields]) for r in rows])
<result>
  <row>
    <field name="id">1</field>
    <field name="name">Alex</field>
  </row>
  ...
</result>

Representación de los Datos

La función export_to_csv_file acepta un argumento clave llamado represent. Cuando es True, usará las columnas función represent al exportar los datos en vez de usar los datos crudos.

La función también acepta un argumento clave llamado colnames que debe contener una lista de los nombres de las columnas que se desean exportar. Por defecto toma todas las columnas.

Tanto export_to_csv_file como import_from_csv_file aceptan argumentos claves que le dicen al analizador csv el formado de los archivos guardados/cargados:

  • delimiter: un delimitador para separar los valores (por defecto ‘,’)
  • quotechar: carácter a usar para acotar los valores de las cadenas (por defecto usa doble comillas)
  • quoting: sistema de acotación (por defecto csv.QUOTE_MINIMAL)

He aquí unos ejemplos de uso:

1
2
3
4
5
6
>>> import csv
>>> db.export_to_csv_file(open('/tmp/test.txt', 'w'),
        delimiter='|',
        quotechar='"',
        quoting=csv.QOUTE_NONNUMERIC)
        quoting=csv.QUOTE_NONNUMERIC)

Que haría algo similar a

1
"hello"|35|"this is the text description"|"2009-03-03"

Para más información consulte la documentación oficial de Python [64]

Almacenando en Cache las Selecciones

El método de selección también toma un argumento de cache, que por defecto es None. Para propósitos de almacenamiento en cache, debería ser establecido a una tupla donde el primero elemento es el modelo de cache (cache.ram, cache.disk, etc.), y el segundo elemento es el tiempo de expiración en segundos.

En el siguiente ejemplo usted ve un controlador que almacena en cache una selección de la tabla db.log definida anteriormente. La selección actual obtiene datos del sistema de base de datos no más frecuente que cada 60 segundos y almacena el resultado en cache.ram. Si la siguiente llamada a este controlador ocurre en menos de 60 segundos desde la última operación de entrada y salida a la base de datos, simplemente obtiene los datos previos de la cache.ram.

1
2
3
def cache_db_select():
    logs = db().select(db.log.ALL, cache=(cache.ram, 60))
    return dict(logs=logs)

Los resultados de una selección son objetos complejos, no seleccionables; no pueden ser almacenados en una sesión y no pueden ser almacenados en cache de ninguna otra manera que la explicada aquí.

Alias y Auto-Referencias

Es posible definir tablas con campos que hacen referencia a ellos mismos aunque la notación usual puede fallar. El siguiente código estaría mal porque usar una variable db.person antes de ser definida:

1
2
3
4
db.define_table('person',
    Field('name'),
    Field('father_id', db.person),
    Field('mother_id', db.person))

La solución consiste en usar una notación alterna

1
2
3
4
db.define_table('person',
    Field('name'),
    Field('father_id', 'reference person'),
    Field('mother_id', 'reference person'))

De hecho, db.tablename y “reference tablename” son tipos de campos equivalentes.

Si la tabla hace referencia a ella misma, entonces no es posible realizar una unión “JOIN” para seleccionar una persona y sus parientes si usar la palabra clave SQL “AS”. Ésto se logra en web2py usando with_alias. He aquí un ejemplo:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
>>> Father = db.person.with_alias('father')
>>> Mother = db.person.with_alias('mother')
>>> db.person.insert(name='Massimo')
1
>>> db.person.insert(name='Claudia')
2
>>> db.person.insert(name='Marco', father_id=1, mother_id=2)
3
>>> rows = db().select(db.person.name, Father.name, Mother.name,
      left=(Father.on(Father.id==db.person.father_id),
            Mother.on(Mother.id==db.person.mother_id)))
>>> for row in rows:
        print row.person.name, row.father.name, row.mother.name
Massimo None None
Claudia None None
Marco Massimo Claudia

Note que decidimos hacer una distinción entre:

  • “father_id”: el nombre de campo usado en la tabla “person”;
  • “father”: el alias que queremos usar para la tabla referenciada por el anterior campo; ésto es comunicado a todas las bases de datos;
  • “Father”: la variable usada por web2py para referirse a ese alias.

La diferencia es sutil, y no hay nada malo en usar el mismo nombre para los tres:

db.define_table('person',
    Field('name'),
    Field('father', 'reference person'),
    Field('mother', 'reference person'))
>>> father = db.person.with_alias('father')
>>> mother = db.person.with_alias('mother')
>>> db.person.insert(name='Massimo')
1
>>> db.person.insert(name='Claudia')
2
>>> db.person.insert(name='Marco', father=1, mother=2)
3
>>> rows = db().select(db.person.name, father.name, mother.name,
      left=(father.on(father.id==db.person.father),
            mother.on(mother.id==db.person.mother)))
>>> for row in rows:
        print row.person.name, row.father.name, row.mother.name
Massimo None None
Claudia None None
Marco Massimo Claudia

Pero es importante tener la diferencia clara para poder construir correctamente las consultas.

Herencia de Tablas

Es posible crear una tabla que contiene todos los campos de otra tabla. Es suficiente con pasar la otra tabla en el lugar de un campo a define_table. Por ejemplo:

1
2
db.define_table('person', Field('name'))
db.define_table('doctor', db.person, Field('specialization'))

También es posible definir una tabla falsa que no es guardada en una base de datos para usarla en otros muchos lugares. Por ejemplo:

1
2
3
4
5
6
signature = db.Table(db, 'signature',
    Field('created_on', 'datetime', default=request.now),
    Field('created_by', db.auth_user, default=auth.user_id),
    Field('updated_on', 'datetime', default=request.now),
    Field('updated_by', db.auth_user, update=auth.user_id))
db.define_table('payment', signature, Field('amount', 'double'))

Este ejemplo asume que la autenticación estándar de web2py está activada.

Contenidos

Tema anterior

Las Vistas

Próximo tema

Formularios y Validadores

Envíe sus comentarios o correcciones a comunidad@latinuxpress.com

Patrocinado por