44.6. Acceso a la base de datos #

44.6.1. Funciones de acceso a la base de datos
44.6.2. Captura de errores

El módulo de lenguaje PL/Python importa automáticamente un módulo de Python llamado plpy. Las funciones y constantes en este módulo están disponibles en el código de Python como plpy.foo.

44.6.1. Funciones de acceso a la base de datos #

El módulo plpy proporciona varias funciones para ejecutar comandos de base de datos:

plpy.execute(query [, limit])

Llamar a plpy.execute con una cadena de consulta y un argumento de límite de filas opcional hace que se ejecute la consulta y se devuelva el resultado en un objeto de resultado.

Si se especifica limit y es mayor que cero, entonces plpy.execute recupera como máximo limit filas, como si la consulta incluyera una cláusula LIMIT. Omitir limit o especificarlo como cero hace que no haya límite de filas.

El objeto de resultado emula un objeto de lista o diccionario. Se puede acceder al objeto de resultado por número de fila y nombre de columna. Por ejemplo:

rv = plpy.execute("SELECT * FROM my_table", 5)

devuelve hasta 5 filas de my_table. Si my_table tiene una columna my_column, se accedería a ella como:

foo = rv[i]["my_column"]

El número de filas devueltas se puede obtener utilizando la función incorporada len.

El objeto de resultado tiene estos métodos adicionales:

nrows()

Devuelve el número de filas procesadas por el comando. Ten en cuenta que esto no es necesariamente lo mismo que el número de filas devueltas. Por ejemplo, un comando UPDATE establecerá este valor pero no devolverá ninguna fila (a menos que se use RETURNING).

status()

El valor de retorno de SPI_execute().

colnames()
coltypes()
coltypmods()

Devuelven una lista de nombres de columnas, una lista de OIDs de tipos de columnas y una lista de modificadores de tipo específicos para las columnas, respectivamente.

Estos métodos lanzan una excepción cuando se llaman en un objeto de resultado de un comando que no produjo un conjunto de resultados, por ejemplo, UPDATE sin RETURNING, o DROP TABLE. Pero es correcto usar estos métodos en un conjunto de resultados que contenga cero filas.

__str__()

El método estándar __str__ está definido para que sea posible, por ejemplo, depurar los resultados de la ejecución de consultas utilizando plpy.debug(rv).

El objeto de resultado se puede modificar.

Ten en cuenta que llamar a plpy.execute hará que todo el conjunto de resultados se lea en memoria. Solo usa esa función cuando estés seguro de que el conjunto de resultados será relativamente pequeño. Si no quieres correr el riesgo de un uso excesivo de memoria al obtener resultados grandes, usa plpy.cursor en lugar de plpy.execute.

plpy.prepare(query [, argtypes])
plpy.execute(plan [, arguments [, limit]])

plpy.prepare prepara el plan de ejecución para una consulta. Se llama con una cadena de consulta y una lista de tipos de parámetros, si tienes referencias a parámetros en la consulta. Por ejemplo:

plan = plpy.prepare("SELECT last_name FROM my_users WHERE first_name = $1", ["text"])

text es el tipo de la variable que pasarás para $1. El segundo argumento es opcional si no deseas pasar ningún parámetro a la consulta.

Después de preparar una sentencia, utilizas una variante de la función plpy.execute para ejecutarla:

rv = plpy.execute(plan, ["name"], 5)

Pasa el plan como primer argumento (en lugar de la cadena de consulta), y una lista de valores para sustituir en la consulta como segundo argumento. El segundo argumento es opcional si la consulta no espera ningún parámetro. El tercer argumento es el límite de filas opcional como antes.

Alternativamente, puedes llamar al método execute en el objeto de plan:

rv = plan.execute(["name"], 5)

Los parámetros de la consulta y los campos de las filas de resultados se convierten entre los tipos de datos de PostgreSQL y Python como se describe en Section 44.2.

Cuando preparas un plan utilizando el módulo PL/Python, se guarda automáticamente. Lee la documentación de SPI (Chapter 45) para obtener una descripción de lo que esto significa. Para hacer un uso efectivo de esto a través de las llamadas a funciones, se debe usar uno de los diccionarios de almacenamiento persistente SD o GD (consulta Section 44.3). Por ejemplo:

CREATE FUNCTION usesavedplan() RETURNS trigger AS $$
    if "plan" in SD:
        plan = SD["plan"]
    else:
        plan = plpy.prepare("SELECT 1")
        SD["plan"] = plan
    # resto de la función
$$ LANGUAGE plpython3u;

plpy.cursor(query)
plpy.cursor(plan [, arguments])

La función plpy.cursor acepta los mismos argumentos que plpy.execute (excepto por el límite de filas) y devuelve un objeto de cursor, que te permite procesar grandes conjuntos de resultados en fragmentos más pequeños. Al igual que con plpy.execute, se puede utilizar una cadena de consulta o un objeto de plan junto con una lista de argumentos, o la función cursor se puede llamar como un método del objeto de plan.

El objeto de cursor proporciona un método fetch que acepta un parámetro entero y devuelve un objeto de resultado. Cada vez que llames a fetch, el objeto devuelto contendrá el siguiente lote de filas, nunca mayor que el valor del parámetro. Una vez que se agoten todas las filas, fetch comenzará a devolver un objeto de resultado vacío. Los objetos de cursor también proporcionan una interfaz de iterador, entregando una fila a la vez hasta que se agoten todas las filas. Los datos obtenidos de esa manera no se devuelven como objetos de resultado, sino más bien como diccionarios, correspondiendo cada diccionario a una sola fila de resultado.

Un ejemplo de dos formas de procesar datos de una tabla grande es:

CREATE FUNCTION count_odd_iterator() RETURNS integer AS $$
odd = 0
for row in plpy.cursor("select num from largetable"):
    if row['num'] % 2:
         odd += 1
return odd
$$ LANGUAGE plpython3u;

CREATE FUNCTION count_odd_fetch(batch_size integer) RETURNS integer AS $$
odd = 0
cursor = plpy.cursor("select num from largetable")
while True:
    rows = cursor.fetch(batch_size)
    if not rows:
        break
    for row in rows:
        if row['num'] % 2:
            odd += 1
return odd
$$ LANGUAGE plpython3u;

CREATE FUNCTION count_odd_prepared() RETURNS integer AS $$
odd = 0
plan = plpy.prepare("select num from largetable where num % $1 <> 0", ["integer"])
rows = list(plpy.cursor(plan, [2]))  # o: = list(plan.cursor([2]))

return len(rows)
$$ LANGUAGE plpython3u;

Los cursores se eliminan automáticamente. Pero si deseas liberar explícitamente todos los recursos retenidos por un cursor, utiliza el método close. Una vez cerrado, ya no se pueden obtener más datos de un cursor.

Tip

No confundas los objetos creados por plpy.cursor con los cursores de DB-API definidos por la especificación del API de base de datos de Python. No tienen nada en común excepto el nombre.

44.6.2. Captura de errores #

Las funciones que acceden a la base de datos pueden encontrar errores, lo que hará que se aborten y lancen una excepción. Tanto plpy.execute como plpy.prepare pueden lanzar una instancia de una subclase de plpy.SPIError, que por defecto terminará la función. Este error se puede manejar como cualquier otra excepción de Python, utilizando la estructura try/except. Por ejemplo:

CREATE FUNCTION try_adding_joe() RETURNS text AS $$
    try:
        plpy.execute("INSERT INTO users(username) VALUES ('joe')")
    except plpy.SPIError:
        return "something went wrong"
    else:
        return "Joe added"
$$ LANGUAGE plpython3u;

La clase real de la excepción lanzada corresponde a la condición específica que causó el error. Consulta la Table A.1 para obtener una lista de las condiciones posibles. El módulo plpy.spiexceptions define una clase de excepción para cada condición de PostgreSQL, derivando sus nombres del nombre de la condición. Por ejemplo, division_by_zero se convierte en DivisionByZero, unique_violation se convierte en UniqueViolation, fdw_error se convierte en FdwError, y así sucesivamente. Cada una de estas clases de excepción hereda de SPIError. Esta separación facilita el manejo de errores específicos, por ejemplo:

CREATE FUNCTION insert_fraction(numerator int, denominator int) RETURNS text AS $$
from plpy import spiexceptions
try:
    plan = plpy.prepare("INSERT INTO fractions (frac) VALUES ($1 / $2)", ["int", "int"])
    plpy.execute(plan, [numerator, denominator])
except spiexceptions.DivisionByZero:
    return "denominator cannot equal zero"
except spiexceptions.UniqueViolation:
    return "already have that fraction"
except plpy.SPIError as e:
    return "other error, SQLSTATE %s" % e.sqlstate
else:
    return "fraction inserted"
$$ LANGUAGE plpython3u;

Ten en cuenta que debido a que todas las excepciones del módulo plpy.spiexceptions heredan de SPIError, una cláusula except que la maneje capturará cualquier error de acceso a la base de datos.

Como una forma alternativa de manejar diferentes condiciones de error, puedes capturar la excepción SPIError y determinar la condición de error específica dentro del bloque except mirando el atributo sqlstate del objeto de excepción. Este atributo es un valor de cadena que contiene el código de error SQLSTATE. Este enfoque proporciona aproximadamente el mismo funcionamiento.