You are on page 1of 32

Manual de Buenas Prácticas de Desarrollo en PL/SQL

Código:

Título: Manual de Buenas Prácticas de Desarrollo en PL/SQL

Objetivo: Describir los principios generales de Buenas Prácticas de Desarrollo en


PLSQL estableciendo el marco necesario para asegurar características
comunes para facilitar el desarrollo y mantenimiento de las aplicaciones.
Vigencia inicial : 12/12/2005

Última revisión : 12/12/2005

Último revisor :

Vigencia hasta :

Autor:

Revisor:

Usuarios:

Dirigir Consultas a:
Índice

........................................................................................................................................................................2
2.Pauta para variables y estructuras de datos..................................................................................................3
3.Pauta para declaración y uso de variables de paquetes..............................................................................10
4.Pautas para el uso de estructuras de Control..............................................................................................14
5.Pautas para el manejo de excepciones.......................................................................................................18
6.Pautas mínimas para uso de SQL dentro de PL/SQL..................................................................................25
7.Reglas generales para performance ...........................................................................................................31
1.
2. Pauta para variables y estructuras de datos

• Declarar, definir el tipo, inicializar y asignar valor por defecto a las estructura de
datos antes de trabajar con ellas.

PL/SQL es un lenguaje fuertemente tipificado. Esto significa que antes de trabajar con
cualquier tipo de estructura de datos, es necesario declarar las mismas, definir el tipo y
opcionalmente inicializarlas o asignarles un valor por defecto.

Las declaraciones deben realizarse en la sección de declaración de variables de un


bloque anónimo, procedimiento, función o paquete.

• En las declaraciones que se relacionan a tablas y columnas utilizar la cláusula


%TYPE y %ROWTYPE.

Esta forma permite al código adaptarse a cambios de estructuras de datos y


autodocumentarse, ya que al leer el código se puede saber a que tipo de dato hace
referencia.

Para el tipo VARCHAR2, Oracle separa la memoria necesaria según la longitud definida.
Es muy común ‘Hardcodear’ la longitud del mismo a su máxima tamaño para evitar
problemas, a costo de utilizar mas memoria de la necesaria. Aquí nuevamente se hace
conveniente el uso de %TYPE o %SUBTYPE para un mejor aprovechamiento de los
recursos de memoria.

Ejemplo:

Mal:
DECLARE
l_apellido VARCHAR2(45);
l_mensaje VARCHAR2(2000);

Bien:
DECLARE
l_apellido personas.nom_apellido%TYPE;
l_mensaje par_mensajes.mensaje%TYPE;
• En los datos numéricos, definir la precisión.

Oracle soporta hasta 38 dígitos de precisión en los tipos NUMBER. Si no se define en


forma correcta se está derrochando memoria.

Ejemplo:

Mal:
DECLARE
l_importe NUMBER;

Bien:
DECLARE
l_importe NUMBER(12,2);

• Usar declaración de constantes para valores que no cambian durante la


ejecución del programa.

Permite que ciertos valores no sean modificados por otros desarrolladores y asegura que
los datos que se consideran confidenciales o que se utilizan en fórmulas o
parametrizaciones no sean manipulados en los códigos.

Ejemplo:

DECLARE
l_fec_actual DATE := TRUNC(SYSDATE);
l_trj_banco VARCHAR2(4) := ‘VISA’;

• Siempre que sea posible, utilizar el tipo de dato RECORD para manejar
estructuras.

La declaración de variables individuales o dispersión de datos, muchas veces, complica


la lectura del código. La agrupación de los mismos bajo estructuras, facilita la
administración y el mantenimiento del código.
Ejemplo:

Mal:
DECLARE
l_nombre personas.nom_nombre%TYPE;
l_apellido personas.nom_apellido%TYPE;
l_edad personas.num_edad%TYPE;
Bien:
DECLARE
TYPE r_persona IS RECORD
(nombre personas.nom_nombre%TYPE,
apellido personas.nom_apellido%TYPE,
edad personas.num_edad%TYPE);

• Realizar inicializaciones complejas de variables en la sección de ejecución de


programa.

Los errores de un código solo pueden ser capturados en la sección ejecutable de un


bloque. Si la inicialización de una variable en la sección de declaración falla, el error no
se puede manejado.
Para ello hay que asegurar que la inicialización lógica no falle y esto se asegura
haciendo las inicializaciones al comienzo de la sección de ejecución. Si se produce un
error, se puede capturar el error y decidir su tratamiento.

Ejemplo:

Mal:
DECLARE
l_last_calificacion calificaciones.valor%TYPE:=last_busqueda (SYSDATE);
l_min_calificacion INTEGER:=a_calificacion.limits(a_calificacion.low);

BEGIN

Bien:
DECLARE

l_last_calificacion calificaciones.valor%TYPE;
l_min_calificacion INTEGER;

PROCEDURE init IS
BEGIN
l_last_calificacion := last_busqueda(SYSDATE);
l_min_calificacion := a_calificacion.limits(a_calificacion.low);
EXCEPTION
-- Manejo de errores en el programa.
END;
BEGIN
init;

• Reemplazar expresiones complejas con variables booleanas y funciones.


Las expresiones booleanas se pueden evaluar con tres valores: TRUE, FALSE o NULL.
Se pueden usar variables de este tipo para ocultar expresiones complejas. Como
consecuencia se puede leer mas fácilmente el código y es mas simple su
mantenimiento.

Ejemplo:

Mal:
IF l_salario_total BETWEEN 10000 AND 50000
AND emp_estado(rec_empleado.numero) = 'N'
AND (MONTHS_BETWEEN (rec_empleado.fec_ingreso, SYSDATE) > 10) THEN
emp_informar(rec_empleado.empno);
END IF;

Bien:
DECLARE
condicion_para_informar BOOLEAN;
BEGIN
condicion_para_informar := total_sal BETWEEN 10000 AND 50000 AND
emp_estado(rec_empleado.numero) = 'N'
AND (MONTHS_BETWEEN (rec_empleado.fec_ingreso, SYSDATE) > 10;
.........
IF condicion_para_informar THEN
emp_informar(rec_empleado.empno);
END IF;
EXCEPTION
WHEN OTHERS THEN -- definir el manejo de errores.

END;

• Remover variables y código no usado.

En determinadas circunstancias los códigos son modificados a medida que transcurre el


tiempo, la lógica cambia y a veces se declaran variables que no se usan, se escribe
código que en un momento era necesario o que se quiere mantener activado por un
momento, pero después no se recuerda cual fue el motivo del mismo.
Para mantener todo mas claro, es conveniente eliminar esos puntos que conducen a
‘zonas muertas’. Una práctica para esto consiste en eliminar las variables que solo
aparecen en la zona de declaración. Existen varios productos que simplifican esta
tarea. Es mucho mas simple entender, realizar seguimientos o mantener códigos que no
tienen ‘zonas muertas’. A modo de ejemplo se incluye un código con algunos errores:

PROCEDURE check_control (
p_nro_cuenta IN cuentas.nro_cuenta%TYPE,
p_importe IN VARCHAR2)
IS
l_contador NUMBER(3);
l_baja BOOLEAN;
l_fecha_movimiento DATE := SYSDATE;
BEGIN
l_fecha_movimiento := r_cuenta.fecha_ult_mov (p_nro_cuenta);
IF ADD_MONTHS (SYSDATE, -60) > l_fecha_movimiento THEN
Informar_cuenta;
ELSIF ADD_MONTHS (SYSDATE, -24) > l_fecha_movimiento THEN
Check_baja (p_nro_cuenta, l_baja);
IF l_baja AND
/* agregado para forzar false */ FALSE THEN
armar_carta (p_nro_cuenta);
END IF;
-- arma proceso para control
-- pkg_procesos_cuenta.iniciar_control (p_nro_cuenta);
END IF;
END;
Lo que se puede observar es lo siguiente:

o p_Importe esta declarado, no se usa y no tiene asignación por defecto, por lo cual el valor
es totalmente ignorado.
o l_contador esta declarada, pero no se usa.
o l_fecha_movimiento está asignado con sysdate e inmediatamente recibe el valor de
r_cuenta.fecha_ult_mov (p_nro_cuenta);.
o La llamada a armar_carta se desactivo con el agregado de FALSE.
o La llamada a pkg_procesos_cuenta.iniciar_control esta comentada.

• Utilizar rutinas de cierre o limpieza cuando los programas terminan (en


ejecución normal o por error)

En algunos escenarios es crucial tener rutinas de cierre o limpieza, las cuales se deben
incluir al finalizar el programa y al final de cada excepción. Generalmente son
aplicables para el uso de cursores y manejo de archivos. A continuación se muestra
ejemplos:
Ejemplo:

Mal:
DECLARE
c_cursor NUMBER(2);
f_file UTL_FILE.FILE_TYPE;
BEGIN
c_cursor := DBMS_SQL.OPEN_CURSOR;
f_file := UTL_FILE.FOPEN (‘c:\temp’,’archivo.txt’, ‘R’);
….
EXCEPTION
WHEN NO_DATA_FOUND THEN
log_error;
RAISE;
END;

Se puede observar que tanto el cursor, como el archivo quedan abiertos en caso de existir alguna
excepción.

Bien:
DECLARE
c_cursor NUMBER(2);
f_file UTL_FILE.FILE_TYPE;

PROCEDURE limpiar
IS
IF c_cursor%ISOPEN THEN
DBMS_SQL.CLOSE (c_cursor);
END IF;

UTL_FILE.FCLOSE(f_file);

END limpiar;

BEGIN
c_cursor := DBMS_SQL.OPEN_CURSOR;
f_file := UTL_FILE.FOPEN (‘c:\temp’,’archivo.txt’, ‘R’);

limpiar;
EXCEPTION
WHEN NO_DATA_FOUND THEN
log_error;
limpiar;
raise;
END;

Ahora se puede ver que se definió la rutina que realiza el cierre de archivo y de cursor. Este tipo de
práctica evita que se produzcan errores que indiquen que el archivo está en uso o que el cursor ya se
encuentra abierto, cuando la rutina es llamada de distintos puntos, además que facilita a otros
desarrolladores agregar nuevas estructuras y contemplar el cierre o limpieza de las mismas.

• Tener cuidado con las conversiones implícitas de tipos de datos.

Si bien es sabido que PL/SQL maneja las conversiones implícitas, existen al menos dos
grandes problemas con esto.

Las conversiones no son intuitivas, a veces se realizan de formas no esperadas y ocurren


problemas, especialmente dentro de sentencias SQL.

Las reglas de conversión no están bajo el control de desarrollador. Pueden cambiar con
el upgrade a una versión de Oracle o con el cambio de parámetros como
NLS_DATE_FORMAT.
Se puede convertir con el uso explícito de funciones como son: TO_DATE, TO_CHAR,
TO_NUMBER y CAST.

Ejemplo:

Mal:
DECLARE
l_fecha_fin DATE := ’01-MAR-04’;

Este código puede dar error si los parámetros de inicialización no corresponden a DD-MON-YY o DD-
MON-RR.

Bien:
DECLARE
l_fecha_fin date := TO_DATE (’01-MAR-04’,’DD-MON-RR’);

En este caso el uso de conversiones explícitas evita errores y no se esta sujeto a condiciones
externas del programa.
3. Pauta para declaración y uso de variables de paquetes.

• Agrupar los tipos de datos, evitar su exposición y controlar su manipulación.

Los paquetes requieren ciertos tipos de recaudos en la forma de declaración y uso de


variables. Agrupar los tipos de datos, evitar su exposición y controlar su manipulación
permiten obtener las ventajas del uso de los mismos.

• Definir las constantes que son referenciadas por toda la aplicación en un


paquete único.
• Definir las constantes que corresponden a un área específica dentro de un
paquete que encapsula esa funcionalidad

Nunca colocar literales ‘en duro’, como ‘SI’ o 150 en el código. Es conveniente crear un
paquete para mantener estos nombres y valores publicados en reemplazo de los
literales.

Ejemplo para paquete general

CREATE OR REPLACE PACKAGE CONSTANTS IS


-- representación de true o false
falso CONSTANT CHAR(1) := ‘F’;
verdadero CONSTANT CHAR(1) := ‘V’;

-- fecha de control (6 meses antes)


fecha_control CONSTANT DATE := ADD_MONTHS (SYSDATE, -6);

Ejemplo para paquete específico

CREATE OR REPLACE PACKAGE cta_constants IS


semana_max CONSTANT INTEGER := 54;
activo CONSTANT CHAR(1) := ‘A’;
inactivo CONSTANT CHAR(1) := ‘I’;
codigo_pais CONSTANT INTEGER := 54;

Este tipo de práctica permite que el código no luzca ‘hard codeado’, lo cual lo hace
mas legible y mantenible y además evita que los literales sean modificados.

• Centralizar las definiciones de Types en las especificaciones del paquete.

A medida que se usan los features del lenguaje, se definirán distintos TYPE entre los
que podemos incluir:

SUBTYPEs que definen tipos específicos de la aplicación.


Collection TYPEs, como lista de números, fechas y records.
Cursores referenciados

Esto permite tener estandarizados los tipos de datos para que sean usados por múltiples
programas. Los desarrolladores pueden escribir más rápidamente y disminuir los bugs.
También simplifica el mantenimiento de los types, ya que solo es necesario modificar
el/los paquetes donde están declarados.

Ejemplo

CREATE OR REPLACE PACKAGE colltype IS


TYPE logicos_tab IS TABLE OF BOOLEAN;
TYPE logicos_tab_idx IS TABLE OF BOOLEAN
INDEX BY BINARY_INTEGER;
TYPE fechas_tab IS TABLE OF DATE;
TYPE fechas_tab_idx IS TABLE OF DATE
INDEX BY BINARY_INTEGER;
END colltype;

• Disminuir el uso de variables globales en paquetes y en caso de hacerlo, solo en


el cuerpo del paquete.

Una variable global es una estructura de datos que se puede referenciar fuera del
alcance o bloque en donde está declarada.
Cuando se declara una variable en un paquete, existe y retiene su valor durante la
duración de la sesión.
Son peligrosas porque crean dependencias ocultas o efectos laterales. El seguimiento de
las mismas es complejo, ya que es necesario ver la implementación para ver sus
instancias.
Una solución general para esto es pasar estas variables globales como un parámetro
para no referenciarlas directamente en el programa.

Ejemplo

FUNCTION obtener_multa ( p_cuenta IN clientes.nro_cuenta%TYPE )


IS
l_dias_atraso NUMBER(3);
BEGIN
l_dias_atraso := determinar_atraso( p_cuenta, SYSDATE);
return (l_dias_atraso * g_tasa_diaria);
END obtener_multa;

En esta función el retorno depende de la variable global g_tasa_diaria. Esto hace que se pierda
flexibilidad ya que la función solo puede trabajar con el valor instanciado y además, si no tiene
valor, se corre el riesgo de que no trabaje en forma correcta.

Para mejorar los resultados y reducir la interdependencia se puede agregar un parámetro:


FUNCTION obtener_multa (
p_cuenta IN clientes.nro_cuenta%TYPE,
P_tasa_diaria IN NUMBER
) IS
l_dias_atraso NUMBER(3);
BEGIN
l_dias_atraso := determinar_atraso( p_cuenta, SYSDATE);
return (l_dias_atraso * g_tasa_diaria);
END obtener_multa;

• Exponer las variables globales de los paquetes usando ‘get and set’.

Cualquier estructura de datos declarada en la especificación del paquete puede ser


referenciada por cualquier programa con autorización de EXECUTE. Esto hace que en
forma deliberada se pueda hacer uso de esas estructuras globales.
Para evitar eso, es conveniente declarar estos datos en el cuerpo del paquete y proveer
al paquete de métodos get y set, declarados en la especificación. Esta forma le permite
a los desarrolladores, acceder a los datos a través de estos programas y manipular los
datos según las reglas de estos procedimientos.

Ejemplo

CREATE OR REPLACE PACKAGE pkg_calculo_gastos IS


g_gasto_mensual NUMBER(3):= 20;

FUNCTION dias_atraso (p_cuenta IN CTA.nro_cuenta%TYPE)


RETURN INTEGER;

END pkg_calculo_gastos;

Facilmente podemos manipular los datos desde cualquier lugar haciendo mención a:
BEGIN
pkg_calculo_gastos.g_gasto_mensual := 15;
….

Para evitar esto y poder manipular los datos según las reglas del negocio se puede reescribir el
código agregando ‘set’ y ‘get’ de esta manera:
CREATE OR REPLACE PACKAGE pkg_calculo_gastos IS
FUNCTION dias_atraso (p_cuenta IN cuentas.nro_cuenta%TYPE)
RETURN INTEGER;

PROCEDURE set_gasto_mensual (p_gasto IN NUMBER);

END pkg_calculo_gastos;

CREATE OR REPLACE PACKAGE BODY pkg_calculo_gastos


IS
g_gasto_mensual := 15;

PROCEDURE set_gasto_mensual (p_gasto IN NUMBER)


IS
BEGIN
g_gasto_mensual := GREATEST (LEAST (p_gasto, 30), 10);
END set_gasto_mensual;

END pkg_calculo_gastos;
4. Pautas para el uso de estructuras de Control

Las estructuras de control pueden convertirse en un punto de complejidad de un


código. La normalización del uso de RETURN y el EXIT garantiza rapidez en la
comprensión de las lógicas escritas y facilidad en su mantenimiento.

• Nunca se sale de una estructura repetitiva con RETURN o con EXIT.

Cada estructura repetitiva tiene un punto de control por donde se debe producir su
salida.

• Un FOR loop itera desde el valor de comienzo hasta el valor de terminación


(ciclo N).
• Un WHILE loop itera mientras no se cumpla la condición de terminación. (Ciclo
0).
• Existe un LOOP con control de la condición al final de ciclo (ciclo 1). Es LOOP …
EXIT WHEN condición lógica.

Combinando estos ciclos se puede hacer que las estructuras tengan un único punto de
control para el ciclo repetitivo. Esto facilita la lectura, comprensión y mantenimiento
del programa y permite hacer modificaciones en forma más simple.

• Una función debe tener un único RETURN exitoso como última línea de la
sección ejecutable. Normalmente, cada manejador de excepciones puede
retornar un valor.

En los programas largos donde se evalúan muchas condiciones, la existencia de


múltiples salidas dificulta el entendimiento y mantenimiento del código.

Ejemplo

LOOP
read_line (file1, line1, file1_eof);
read_line (file2, line2, file2_eof);
IF (file1_eof AND file2_eof) THEN
RETURN (TRUE);
ELSIF (line1 != line2) THEN
RETURN (FALSE);
ELSIF (file1_eof OR file2_eof) THEN
RETURN ( FALSE);
END IF;
END LOOP;

Aquí existen muchas salidas, por lo que esto se puede reemplazar por lo siguiente:
...
LOOP
read_line (file1, line1, file1_eof);
read_line (file2, line2, file2_eof);
IF (file1_eof AND file2_eof) THEN
retval := TRUE;
exit_loop := TRUE;
ELSIF (line1 != line2) THEN
retval := FALSE;
exit_loop := TRUE;
ELSIF (file1_eof OR file2_eof) THEN
retval := FALSE;
exit_loop := TRUE;
END IF;
EXIT WHEN exit_loop;
END LOOP;
RETURN (retval);
En este caso se contemplan las condiciones en muchos puntos, pero se evalúa la salida solo en un
lugar para después realizar el RETURN.

• Nunca declarar el índice de un FOR ..LOOP.

PL/SQL ofrece dos tipos de FOR LOOP: Numéricos y cursores.


Ambos tienen este formato general:

FOR indice_loop IN loop range LOOP


Loop body
END LOOP;

El índice del loop se declara implícitamente durante la ejecución. El ámbito de


existencia está restringido al cuerpo del loop.

Si se declarase el índice, se estaría generando otra variable completamente separada


que en el mejor de los casos nunca será usada, pero si se usa fuera del loop, puede
introducir errores.

Ejemplo

DECLARE
CURSOR c_cuenta IS
SELECT nro_cuenta
FROM cuentas;

r_cuenta cuentas%ROWTYPE;

BEGIN
FOR r_cuenta in c_cuenta LOOP
Calcula_saldo(r_cuenta.nro_cuenta);
END LOOP;
DBMS_OUTPUT.PUT_LINE(r_cuenta.nro_cuenta);
END;
La línea que se encuentra fuera del loop, retorna nulo, ya que está fuera del alcance del mismo.

La línea que se encuentra fuera del loop, retorna nulo, ya que está fuera del alcance
del mismo.

• Al utilizar la cláusula ELSIF, asegurarse que las condiciones son excluyentes

• En el caso de sentencias que deban ingresar en un condicional si o si, se debe


lanzar una excepción en caso de que esto no ocurra y no presuponer que nunca
se cumplirá la condición.
Ejemplo:

DECLARE
IF SEXO = ‘M’ THEN
<<sentencias>>
ELSIF SEXO = ‘F’ THEN

• Reemplazar y simplificar el IF con variables booleanas.

Ejemplo

DECLARE
l_logica BOOLEAN;
BEGIN
IF l_logica = TRUE THEN

ELSIF l_logica = FALSE THEN

END IF;
END;

Reemplazar por

DECLARE
l_logica BOOLEAN;
BEGIN
IF l_logica THEN

ELSIF NOT l_logica THEN

END IF;
END;
5. Pautas para el manejo de excepciones

Aunque se escriba un código perfecto, que no contenga errores y que nunca realice
operaciones inapropiadas, el usuario podría usar el programa incorrectamente,
produciendo una falla que no estaba contemplada.
El uso de excepciones permite capturar y administrar los errores que se pueden
producir dentro del código. Su buen uso trae como resultado un código con menos bugs
y mas fácil de corregir.

• Establecer los lineamientos para el manejo de errores antes de comenzar a


codificar.

Es impráctico definir secciones de excepciones en el código después de que el programa


fue escrito.

La mejor forma de implementar un manejo de errores en toda la aplicación es usando


paquetes que contengan al menos los siguientes elementos.

• Procedimientos que realicen el manejo de tareas de excepciones, como por


ejemplo escribir un log de errores.

• Un programa que oculte la complejidad de RAISE_APPLICATION_ERROR y los


números de application-error.

• Una función que retorne el mensaje de error para un código de error.

• Utilizar el modelo por defecto de manejo de excepciones para propagar la


comunicación de errores.

Aprovechar la arquitectura de manejo de errores de PL/SQL y separar las excepciones


en bloques.

Evitar este tipo de código.

BEGIN
pkg_cuenta.determinar_estado (l_cuenta, error_code, error_msg);
IF error_code != 0 THEN
err.log (...);
GOTO end_of_program;
END IF;
pkg_cuenta.imprimir_estado (l_cuenta, error_code, error_msg);
IF error_code != 0 THEN
err.log (...);
GOTO end_of_program;
END IF;
END;

La sección de código ejecutable debe estar limpia, simple y fácil de seguir. No es


necesario controlar el estado después de cada llamada a un programa. Simplemente
debe incluirse una sección de excepción, capturar el error y determinar la acción a
realizar.

• Capturar todas las excepciones y convertir el significado de los códigos de error


en los retornos a programas que no son PL/SQL.

Cuando los programas PL/SQL son llamados por otros lenguajes de programación (Java,
Visual Basic, etc), al menos es necesario regresar el estado de error (código y mensaje)
para que administren los mismos.

Una manera sencilla de hacer esto es sobrecargar el programa original con otro del
mismo nombre y dos parámetros adicionales.

Ejemplo

CREATE OR REPLACE PACKAGE pkg_cta_control


IS
PROCEDURE estado ( p_cuenta IN cuentas.nro_cuenta%TYPE);
PROCEDURE estado (
p_cuenta IN cuentas.nro_cuenta%TYPE,
error_code OUT INTEGER,
error_msg OUT VARCHAR2);
END pkg_cta_control;

Los desarrolladores pueden llamar al procedimiento que deseen y chequear los errores
de la manera mas apropiada para sus programas.

• Usar procedimientos propios de RAISE en lugar de las llamadas explicitas a


RAISE_APPLICATION_ERROR.

Cuando se está controlando excepciones de sistemas como NO_DATA_FOUND, se usa


RAISE, pero cuando se quiere controlar un error de aplicación específico, se usa
RAISE_APPLICATION_ERROR. Para el último caso, se debe asignar un número de error
y mensaje, lo cual termina en un ‘hardcodeo’.
Para ello se puede utilizar constantes para especificar un número. Una manera mas
clara es proveer un procedimiento que automáticamente controle el número de error y
determina la forma de ejecutar el error.
Ejemplo

RAISE_APPLICATION_ERROR ( -20734, 'Sueldo menor al mínimo');

Se podría escribir de la siguiente forma

err.raise (errnums.sueldo_muy_bajo);

Los desarrolladores no tienen que determinar cual es el número de error que tienen que
usar, solo pasan la constante que desean usar y dejan que la rutina determine cual es el
RAISE correspondiente.

• No sobrecargar una EXCEPTION con múltiples errores al menos que la pérdida


de información sea intencional.

No declarar una excepción genérica como ERROR_GENERAL y luego ejecutar la


excepción en diferentes circunstancias. La lectura del código traerá problemas al
intentar cual fue la causa de problema.

La excepción NO_DATA_FOUND se puede utilizar para indicar que no se encontraron


filas en la ejecución de un query o para indicar que se llegó al final de un archivo. Una
forma de separa esto puede ser de la siguiente manera.

Ejemplo

PROCEDURE lecturas
IS
l_cuenta cuentas.nro_cuenta%TYPE;
l_linea VARCHAR2(1023);
fid UTL_FILE.FILE_TYPE;
no_table_data EXCEPTION;
no_file_data EXCEPTION;
BEGIN
BEGIN
--
SELECT nro_cuenta
INTO l_cuenta
FROM usrdemo.cuentas
WHERE 1 = 2;
EXCEPTION
WHEN NO_DATA_FOUND THEN
RAISE no_table_data;
END;
--
BEGIN
fid := UTL_FILE.FOPEN ('c:\temp', 'texto.txt', 'R');
UTL_FILE.GET_LINE (fid, l_linea);
UTL_FILE.GET_LINE (fid, l_linea);
EXCEPTION
WHEN NO_DATA_FOUND THEN
RAISE no_file_data;
END;
--
EXCEPTION
WHEN no_table_data THEN
pl ('Sin filas!');
WHEN no_file_data THEN
pl ('Leyendo fin de archivo!');
END lecturas;

• No Manejar excepciones que no pueden evitarse, pero si anticiparse.

Si cuando se está escribiendo un programa, se puede predecir que un error va a ocurrir,


se debería incluir un manejador para ese caso.

Ejemplo

PROCEDURE get_sucursal (p_cuenta cuentas.nro_cuenta%TYPE)


IS
l_sucursal cuentas.nro_sucursal%TYPE;
BEGIN
SELECT nro_sucursal
INTO l_sucursal
FROM usrdemo.cuentas
WHERE nro_cuenta=p_cuenta;
RETURN l_sucursal;
EXCEPTION
WHEN NO_DATA_FOUND THEN
RETURN NULL;
END;

• Evitar exponer código en duro para el manejo de errores, reemplazar por


procedimientos que administren los mismos.

Evitar exponer código en duro para el manejo de errores, reemplazar por


procedimientos que administren los mismos.

La mejor manera de registrar en forma consistente y tener mantener la calidad del


seguimiento de errores por toda la aplicación es ofreciendo un conjunto de
procedimientos predefinidos que canalicen el manejo de los mismos.
Es fundamental que el equipo de desarrollo use siempre solamente estos
procedimientos en sus cláusulas WHEN.
Ejemplo

EXCEPTION
WHEN NO_DATA_FOUND THEN
l_msg := ‘Cuenta sin titular ‘|| to_char (p_cuenta);
l_err := SQLCODE;
l_prog := ‘EJEMPLO’ ;
INSERT INTO usrdemo.errlog (cod_error, des_msg, num_linea,
fec_ocurrencia, cod_usuario) VALUES (l_err, l_msg, l_prog,
SYSDATE, USER);
END;

En este ejemplo se ve como se expone el método de logeo, en caso de cambiar la


estructura de la tabla, nos encontramos con un problema grande.
Por otro lado se está hardcodeando el nombre del programa, el cual se puede recuperar
con DBMS_UTILITY.FORMAT_CALL_STACK.

Una mejor forma de hacer esto sería:

EXCEPTION
WHEN NO_DATA_FOUND THEN
Err.handle (‘Cuenta sin titular’ ||to_char(p_cuenta), log => TRUE,
reraise => FALSE);
END;

Esta manera permite que todos manejen el error de la misma forma y hace más simple
el mantenimiento en caso de querer agregar funcionalidad a la administración de los
mismos.

• Usar nombres constantes para flexibilizar los números y mensajes de errores de


aplicación

Oracle define 1000 números de error, entre –20000 y –20999 para usar para nuestra
aplicación.
Se recomienda definir todos los errores en una tabla o un archivo del sistema operativo,
construir paquetes para manipular esos datos y ejecutar el RAISE usando esos nombres
y no códigos en duro.
Ejemplo

PROCEDURE check_sueldol ( p_sueldo personas.sueldo%TYPE)


IS
BEGIN
IF p_sueldo < constantes.sueldo_minimo THEN
RAISE_APPLICATION_ERROR(-20734, ‘El sueldo mínimo es’ ||
constantes.sueldo_minimo);
END IF;
END check_sueldol;

Un código mas apropiado para esto sería:

PROCEDURE check_sueldol ( p_sueldo personas.sueldo%TYPE)


IS
BEGIN
IF p_sueldo < constantes.sueldo_minimo THEN
err.raise(errnums.check_sueldo);
END IF;
END check_sueldol;

Los desarrolladores evitan el conflicto de sobrecargar el mismo número de error con


mensajes distintos y solo tienen que llamar a err.raise del procedimiento que
realizará todo el trabajo.

• Usar WHEN OTHERS solo para excepciones desconocidas que deben ser
ejecutadas.

No es conveniente usar WHEN OTHERS para cualquier error. Si se puede conocer el tipo
de excepción que se quiere ejecutar, es conveniente administrarla en forma específica.
Ejemplo

EXCEPTION
WHEN OTHERS THEN
IF SQLCODE = -1 THEN
Update_instancia(..);
ELSE
Err.log;
RAISE;
END IF;
END …;
Esta forma es mucho mas clara.

EXCEPTION
WHEN DUP_VAL_ON_INDEX THEN
Update_instancia(..);
WHEN OTHERS THEN
Err.log;
RAISE;
END …;

Es mas fácil leer lo que se espera que haga el código y además se evita el hardcodeo de
números de errores.
6. Pautas mínimas para uso de SQL dentro de PL/SQL

La escritura de S QL dentro del código es uno de los aspectos más flexibles de


PL/SQL y también puede ser uno de los más peligrosos. El establecimiento de reglas
claras para su inclusión permite asegurar beneficios y buenos resultados.

• Usar transacciones autónomas para aislar los efectos del ROLLBACK y COMMIT

Permite grabar o realizar rollback dentro de un código PL/SQL sin afectar la sesión
principal. Para hacer una transacción autónomas es necesario incluir en sección de
declaración la siguiente sentencia.

PRAGMA AUTONOMOUS_TRANSACTIONS;

Ejemplo

PROCEDURE log_error (
p_code IN error_log.errcode%TYPE,
p_msg IN error_log.errtext%TYPE)
IS
PRAGMA AUTONOMOUS_TRANSACTION;
BEGIN
INSERT INTO usrdemo.error_log (errcode, errtext, created_on,
created_by) VALUES(code, msg, SYSDATE, USER);
COMMIT;
EXCEPTION
WHEN OTHERS THEN
ROLLBACK;
END log_error;

Con este tipo de transacción se puede logear la transacción sin afectar la sesión
principal.

No utilizar COMMIT o ROLLBACK dentro de los procedimientos PL/SQL que son


utilizados por procesos en otros leguajes. El COMMIT o ROLLBACK debe utilizarse
dentro del proceso que llama los procedimientos PL/SQL.

Cuando los procedimientos PL/SQL son llamados por otros lenguajes de programación
(Java, Visual Basic, etc.) no deben utilizar las sentencias de manejo de transacciones
COMMIT o ROLLBACK de modo de posibilitar un ordenado control de transacciones.
• Usar COUNT solo cuando es necesario saber el número de ocurrencias.

No usar COUNT para saber si hay una o más de una fila para una condición dada.
Solo se debe usar COUNT para saber cuantas filas hay.

Ejemplo

FUNCTION existe_filas(
p_tabla IN VARCHAR2,
p_condicion IN VARCHAR2 DEFAULT NULL,
p_cant_reg IN NUMBER DEFAULT 1)
RETURN BOOLEAN
IS
retval BOOLEAN := FALSE;
l_registros NUMBER;
l_cadena VARCHAR2(500);
BEGIN
BEGIN
l_cadena := 'SELECT count(1) FROM '||p_tabla ||' WHERE ';
IF p_condicion IS NOT NULL THEN
l_cadena := l_cadena ||p_condicion ||' AND ';
END IF;
EXECUTE INMEDIATE l_cadena||' ROWNUM < '||TO_CHAR(p_cant_reg + 1)
INTO l_registros;
IF l_registros >= p_cant_registros THEN
retval :=TRUE;
END IF;
EXCEPTION
WHEN OTHERS THEN
DBMS_OUTPUT.PUT_LINE('error '||SQLERRM);
END;
RETURN retval;
END existe_filas;

Esta forma permite tener una función genérica que evita recorrer toda la tabla y evita
problemas en caso de ser una tabla muy grande.

• Usar cursores implícitos para realizar operaciones sobre muchos registros

Este constructor realiza la apertura y cierre en forma automática. Es fácil de usar y su


lectura es más simple
Ejemplo

DECLARE
CURSOR c_cuentas IS
SELECT nro_cuenta,
fec_alta,
nom_apellido
FROM usrdemo.cuentas
WHERE titular = TRUE;
BEGIN
FOR r_cuentas IN c_cuentas LOOP
pl (r_cuentas.nro_cuenta || ': ' ||r_cuentas.nom_apellido);
END LOOP;
END;

• Nunca usar cursores implícitos para una fila

El FOR LOOP está diseñado para múltiples filas de un cursor. Para un solo registro es
más eficiente un SELECT INTO o un cursor explícito.

• Parametrizar cursores explícitos

Los cursores retornan información como lo haría una función, por lo cual también
pueden aceptar parámetros. Si se define un cursor con parámetros, se puede hacer que
sea reutilizable en diferentes circunstancias de un programa. Esto es mejor si se define
en un paquete.
Ejemplo

CREATE OR REPLACE PACKAGE pkg_cta


IS
CURSOR c_titular (p_cuenta IN cuentas.nro_cuenta%TYPE) IS
SELECT nom_nombre,
nom_apellido
FROM usrdemo.personas per
WHERE EXISTS (
SELECT 1
FROM usrdemo.titular tit
WHERE tit.per_id = per.per_id
AND tit.nro_cuenta = p_cuenta);

Y se invoca de la siguiente forma:

BEGIN
OPEN pkg_cta.c_titular(10);
• Usar RETURNING para obtener información sobre filas modificadas

Esta cláusula permite obtener información de las filas que se modificaron por INSERT,
UPDATE o DELETE.

Ejemplo

INSERT INTO usrdemo.personas (


per_id, nombre, apellido
) VALUES (
per_id.NEXTVAL, p_nombre, p_apellido);

SELECT per_id
INTO l_per_id
FROM usrdemo.personas
WHERE apellido = p_apellido;

Con RETURNING se puede realizar las dos sentencias en una

INSERT INTO usrdemo.personas (


per_id, nombre, apellido
) VALUES (
per_id.NEXTVAL, p_nombre, p_apellido)
RETURNING per_id INTO l_per_id;

Esto mejora notablemente la performance en las aplicaciones y el código se reduce


notablemente.

• Referenciar atributos del cursor inmediatamente después de ejecutar la


operación de SQL.

Las sentencias INSERT, UPDATE y DELETE se ejecutan como cursores implícitos en


PL/SQL. Es decir, no se puede declarar, abrir y procesar este tipo de operaciones en
forma explícita. El motor de Oracle SQL toma el control y administración del cursor.
Se puede obtener información acerca de los resultados de estas operaciones implícitas
más recientemente utilizada consultando los siguientes atributos de cursor.

SQL%ROWCOUNT: Números de filas afectadas por la instrucción DML.


SQL%ISOPEN: Siempre es falso en los manejos de cursores implícitos.
SQL%FOUND: Retorna TRUE si la operación afecta al menos una fila.
SQL%NOTFOUND: Retorna FALSE si la operación afecta al menos una fila.

Hay un solo set de atributos SQL% en una sesión, el cual refleja la última operación
implícita realizada. Por lo cual el uso de estos atributos deberá hacerse con el mínimo
uso de código entre la operación y la referencia al atributo. De otra manera el valor
retornado por el atributo podría no corresponder al SQL deseado.

• Usar BULK COLLECT para mejorar la performance de los querys multiregistros.

En algunas oportunidades es necesario recuperar un gran número de filas desde la base


de datos. BULK COLLECT permite obtener el set de filas en una sola llamada al motor
y recuperar el resultado en un arreglo (COLLECTION).
Para usar este método es necesario declarar un arreglo para mantener los datos
recuperados y luego anteponer al INTO la cláusula BULK COLLECT.

Ejemplo

PROCEDURE procesar_clientes(p_sucursal_id IN sucurales.sucursal_id%TYPE)


RETURN t_clientes
IS
TYPE t_nro_cliente IS TABLE OF clientes.nro_cliente%TYPE;
TYPE t_nombre IS TABLE OF clientes.nombre%TYPE;
TYPE t_fecha IS TABLE OF clientes.fecha_alta%TYPE;
l_nro_clientes t_nro_cliente;
l_nombres t_nombre;
l_fechas t_fecha;

BEGIN
SELECT nro_cliente,
nombre,
fecha_alta
BULK COLLECT INTO
l_nro_clientes,
l_nombres,
l_fechas
FROM usrdemo.clientes
WHERE sucursal_id = p_sucursal_id;
...
END procesar_clientes;

Esta forma de recuperación de datos mejora (en algunos casos enormemente) la


performance del query.

Se debe tener cuidado cuando el SELECT retorna miles de filas ya que si el mismo
programa está corriendo con diferentes usuarios en una misma instancia, puede haber
problemas de memoria.
Se podría restringir usando ROWNUM o LIMIT para cada instancia.

• Usar FORALL para mejorar la performance de DML basados en arreglos.


Para los casos en que es necesario modificar (INSERT, DELETE o UPDATE) un gran
número de filas en una base de datos dentro de un PL/SQL, se recomienda la sentencia
FORALL. La performance mejora notablemente ya que se reduce el número de llamadas
entre PL/SQL y el SQL engine.
Ejemplo

Uso individual de update dentro de un for

BEGIN
FOR r_cuentas IN pkg_ctas.c_cuentas LOOP
UPDATE usrdemo.prestamos
SET fecha_alta = SYSDATE,
Cliente_id = r_cuentas.cliente_id
WHERE cuenta_debito = r_cuentas.cuenta_id;
END LOOP;
END;
Uso individual de update dentro de un for

Ahora el mismo ejemplo implementado con FORALL. Realiza los mismo, pero con una sola llamada
al SQL engine. (Versión para 8i)
DECLARE
TYPE t_cliente IS TABLE OF cuentas.cliente_id%TYPE;
l_clientes t_cliente := t_cliente();
TYPE t_cuenta IS TABLE OF cuentas.cuenta_id%TYPE;
l_cuentas t_cuenta := t_cuenta();
BEGIN
FOR r_cuentas IN pkg_ctas.c_cuentas LOOP
l_clientes.EXTEND;
l_cuentas.EXTEND;
l_clientes(l_cliente.LAST):= rec_cuentas.cliente_id;
l_cuentas(l_cuentas.LAST) := rec_cuentas.cuenta_id;
END LOOP
FORALL indx IN l_clientes.FIRST .. l_clientes.LAST
UPDATE usrdemo.prestamos
SET fecha_alta = SYSDATE,
cliente_id = l_clientes(indx)
WHERE cuenta_debito = l_cuentas(indx);
END;

Como se observa es necesario un loop para cargar el arreglo, sin embargo la


reducción de llamadas al SQL Engine reduce notablemente los tiempos.

Para versiones 9i existen mejoras para este tipo de código y para el manejo de
excepciones.
7. Reglas generales para performance

Para el manejo de datos dentro de un código PL/SQL es necesario tener en cuenta las
siguientes premisas.

• Una sentencia SQL es más rápida que un programa.

Siempre es preferible utilizar la potencia del SQL antes que emular su funcionamiento
con un PL/SQL largo y complejo.

• Un programa pequeño es más rápido que uno extenso.

El uso de programas largos agrega más tiempo de ejecución.


El uso de paquetes pequeños permite al servidor disponer de ellos de manera mas
rápida y eficiente

• Evitar llamadas repetitivas al sql engine

Mientras menos solicitudes de servicios se hagan al sql engine, más rápido se ejecutará
un programa.
Para el caso de operaciones de SQL similares o repetitivas, utilizar FORALL, BULKS,
variables BIND, etc.

• Los queries deben ser optimizados

Es conveniente al escribir un query evaluar su explain plan.


En caso de desconocer como mejorar la performance del mismo, consultar con algún
dba para solicitar ayuda.

• Utilizar los features de performance de SQL y de PL/SQL de las versiones que se


están utilizando solamente para manejo de grandes volúmenes de dato o
cuando la aplicación lo justifique.

La evolución de las versiones generalmente vienen acompañadas de nuevos mecanismos


para mejorar la performance para grandes cantidades de datos, sin embargo, a veces su
utilización requiere código extenso y complejo y los resultados en volúmenes pequeños
no reflejan el esfuerzo invertido.
• Aprender los features de cada versión

Es bueno conocer las mejoras existentes, ya que cada versión trae operaciones que
disminuyen el costo de escritura o brindan funcionalidades que reducen el tiempo de
desarrollo y mejoran la performance.

You might also like