Llamar procedimiento SQL en IBM i/DB2 for i en .NET con NTi

Vamos a ver cómo llamar a un procedimiento almacenado SQL alojado en un sistema IBM i desde una aplicación .NET, utilizando NTi.

Imagen ilustrativa del artículo

El uso de procedimientos almacenados es una práctica muy extendida para centralizar y encapsular la lógica empresarial en un entorno seguro y optimizado. Permiten trasladar parte de la lógica de la aplicación al servidor de bases de datos, lo que reduce la complejidad del código del lado del cliente, mejora el rendimiento al reducir el tráfico de red y aumenta la seguridad al limitar el acceso directo a las tablas.

Vamos a ver cómo llamar a un procedimiento almacenado SQL alojado en un sistema IBM i desde una aplicación .NET, utilizando NTi. Utilizaremos un procedimiento almacenado tomado directamente de la documentación oficial IBM i: DB2 for i SQL Reference - (página 1109).

Este procedimiento calcula la mediana de los salarios del personal y devuelve la lista de empleados cuyo salario es superior a esta mediana.

Veremos dos enfoques para llamar desde una aplicación .NET:

  • La primera implica el uso directo de un DataReader.
  • La segunda, aprovechando Dapper, un micro-ORM ligero que simplifica aún más la implementación y el uso de los resultados.

Paso 1 - Preparación del entorno IBM i

Antes de llamar a un procedimiento almacenado desde .NET, necesitamos entender exactamente qué hace este procedimiento, qué elementos se necesitan para ejecutarlo (tablas, datos) y preparar nuestro entorno.

El procedimiento almacenado que vamos a utilizar viene directamente del manual oficial IBM i: DB2 for i SQL Reference - (página 1109). Aquí está el código SQL proporcionado:

CREATE PROCEDURE MEDIAN_RESULT_SET (OUT medianSalary DECIMAL(7,2))
LANGUAGE SQL
DYNAMIC RESULT SETS 1
BEGIN
    DECLARE v_numRecords INTEGER DEFAULT 1;
    DECLARE v_counter INTEGER DEFAULT 0;

    DECLARE c1 CURSOR FOR
         SELECT salary
         FROM staff
         ORDER BY salary;

    DECLARE c2 CURSOR WITH RETURN FOR
         SELECT name, job, salary
         FROM staff
         WHERE salary > medianSalary
         ORDER BY salary;

    DECLARE EXIT HANDLER FOR NOT FOUND
         SET medianSalary = 6666;

    SET medianSalary = 0;
    SELECT COUNT(*) INTO v_numRecords FROM staff;

    OPEN c1;
    WHILE v_counter < (v_numRecords / 2 + 1) DO
         FETCH c1 INTO medianSalary;
         SET v_counter = v_counter + 1;
    END WHILE;
    CLOSE c1;
    OPEN c2;
END

Este procedimiento calcula el salario medio de los empleados de la empresa. Devuelve este salario en forma de un parámetro de salida llamado salariomedio, y abre un cursor c2 que devuelve una lista detallada de los empleados que ganan un salario superior a esta mediana calculada.

💡 Aquí no hay ningún parámetro de entrada, y el procedimiento calcula directamente a partir de una tabla específica: la tabla staff.

Para que el procedimiento funcione, debe existir una tabla llamada staff en el mismo esquema donde se creará el procedimiento. Debe contener al menos una columna DECIMAL salary y otras columnas name y job necesarias para el cursor de resultados.

Ahora que sabemos lo que hace el procedimiento y lo que necesita, podemos preparar nuestro entorno IBM i paso a paso.

1️⃣ Creación de la biblioteca

Creamos una biblioteca MDSALARY para aislar ordenadamente nuestros elementos de prueba. Ejecute el siguiente comando desde su IBM i, ya sea a través de ACS, o de la pantalla 5250

CRTLIB MDSALARY

2️⃣ Definir el esquema de la base de datos

A continuación, definimos el esquema actual como esquema por defecto para que todos los comandos SQL posteriores hagan referencia a él automáticamente:

SET CURRENT SCHEMA = MDSALARY;

A partir de ahora, cada tabla o procedimiento que creemos se colocará automáticamente en la LIB MDSALARY.

3️⃣ Creación de la tabla STAFF

Como vimos anteriormente, el procedimiento almacenado espera una tabla específica llamada staff. Vamos a crearla de acuerdo con las columnas requeridas por nuestro ejemplo:

CREATE TABLE staff (
    name VARCHAR(50),
    job VARCHAR(50),
    salary DECIMAL(7,2)
);
  • Una columna name para el nombre del empleado.
  • Una columna job para el cargo.
  • Una columna salary para el salario del empleado.

4️⃣ Inserción de un conjunto de datos de ejemplo

Para calcular un salario medio relevante, necesitamos insertar algunos datos representativos en esta tabla:

INSERT INTO staff (name, job, salary) VALUES ('Alice', 'Manager', 2000.00);
INSERT INTO staff (name, job, salary) VALUES ('Bob', 'Clerk', 3000.00);
INSERT INTO staff (name, job, salary) VALUES ('Charlie', 'Analyst', 4000.00);
INSERT INTO staff (name, job, salary) VALUES ('David', 'Developer', 5000.00);
INSERT INTO staff (name, job, salary) VALUES ('Eve', 'Designer', 6000.00);
INSERT INTO staff (name, job, salary) VALUES ('Frank', 'Tester', 7000.00);

5️⃣ Script completo listo para ejecutarse en ACS

Aquí tienes un script completo que puedes copiar, pegar y ejecutar de una sola vez desde la herramienta "SQL Script Executor" de ACS

-- Creación de la biblioteca
CL: CRTLIB MDSALARY;

-- Definición del esquema actual
SET CURRENT SCHEMA = MDSALARY;

-- Creación de la tabla STAFF
CREATE TABLE staff (
    name VARCHAR(50),
    job VARCHAR(50),
    salary DECIMAL(7,2)
);

-- Inserción de datos de prueba
INSERT INTO staff (name, job, salary) VALUES ('Alice', 'Manager', 2000.00);
INSERT INTO staff (name, job, salary) VALUES ('Bob', 'Clerk', 3000.00);
INSERT INTO staff (name, job, salary) VALUES ('Charlie', 'Analyst', 4000.00);
INSERT INTO staff (name, job, salary) VALUES ('David', 'Developer', 5000.00);
INSERT INTO staff (name, job, salary) VALUES ('Eve', 'Designer', 6000.00);
INSERT INTO staff (name, job, salary) VALUES ('Frank', 'Tester', 7000.00);

-- Creación del procedimiento almacenado MEDIAN_RESULT_SET
CREATE PROCEDURE MEDIAN_RESULT_SET (OUT medianSalary DECIMAL(7,2))
LANGUAGE SQL
DYNAMIC RESULT SETS 1
BEGIN
    DECLARE v_numRecords INTEGER DEFAULT 1;
    DECLARE v_counter INTEGER DEFAULT 0;

    DECLARE c1 CURSOR FOR
         SELECT salary FROM staff ORDER BY salary;

    DECLARE c2 CURSOR WITH RETURN FOR
         SELECT name, job, salary FROM staff WHERE salary > medianSalary ORDER BY salary;

    DECLARE EXIT HANDLER FOR NOT FOUND SET medianSalary = 6666;

    SET medianSalary = 0;
    SELECT COUNT(*) INTO v_numRecords FROM staff;

    OPEN c1;
    WHILE v_counter < (v_numRecords / 2 + 1) DO
         FETCH c1 INTO medianSalary;
         SET v_counter = v_counter + 1;
    END WHILE;
    CLOSE c1;
    OPEN c2;
END;

6️⃣ Verificación en IBM i

Compruebe en su IBM i que la librería MDSALARY existe, que contiene la tabla staff con los datos insertados, y que contiene el procedimiento almacenado SQL llamado MEDIAN_RESULT_SET (Tipo *PGM).

Biblioteca MDSALARY en IBM i

Paso 2 - Llamada al procedimiento almacenado IBM i desde .NET

Ahora que nuestro entorno IBM i está listo, vamos a llamar al procedimiento desde nuestra aplicación .NET. Creamos un proyecto BlazorWebApp en .NET 8, e instalamos los siguientes paquetes desde NuGet:

dotnet add package Aumerial.Data.Nti
dotnet add package Dapper

1️⃣ Creación de un servicio de conexión

Para simplificar la gestión de las conexiones, hemos creado un servicio sencillo DB2Service.cs.

using Aumerial.Data.Nti;

public class DB2Service
{
    private readonly string _connectionString = "server=myserver;user=myuser;password=mypassword;trim=true";

    public NTiConnection CreateConnection()
    {
        var conn = new NTiConnection(_connectionString);
        conn.Open();
        return conn;
    }
}

A continuación, guarde este servicio en su archivo Program.cs.

builder.Services.AddSingleton();

2️⃣ Llamada al procedimiento almacenado

Lo haremos:

  • Definir una entidad que represente a nuestros empleados
  • Llamar al procedimiento almacenado para recuperar los resultados
  • Explotación de estos resultados con dos enfoques: DataReader y Dapper
  • Mostrar resultados en una tabla Blazor

3️⃣ Creación de la entidad Empleado

public class Employee
{
    public string Name { get; set; }
    public string Job { get; set; }
    public decimal Salary { get; set; }
}

4️⃣ Método 1 - Enfoque clásico (DataReader)

En este primer enfoque clásico, creamos explícitamente una conexión con el servidor IBM i a través de nuestro servicio dedicado DB2Service y, a continuación, configuramos un comando NTi para llamar a nuestro procedimiento almacenado MEDIAN_RESULT_SET.

Definimos claramente el parámetro de salida medianSalary, que recuperará el salario medio calculado por el procedimiento. Una vez ejecutado el comando, un DataReader nos permite leer secuencialmente cada resultado devuelto por el procedimiento, transformando cada línea en un objeto de tipo Employee. Por último, extraemos directamente el valor del parámetro de salida para averiguar la mediana exacta.

private decimal median;
private List employees = new();

    private async Task LoadDatawhithDataReader()
    {
         // Creación de la conexión a través de DB2Service
        using var conn = Db2Service.CreateConnection();
        using var cmd = new NTiCommand("MDSALARY.MEDIAN_RESULT_SET", conn);

        // Crear el comando para llamar al procedimiento almacenado
        cmd.CommandType = System.Data.CommandType.StoredProcedure;

        // Definición del parámetro de salida "medianSalary
        var param = new NTiParameter();
        param.ParameterName = "medianSalary";
        param.Direction = System.Data.ParameterDirection.Output;

        // Añade el parámetro de salida al comando
        cmd.Parameters.Add(param);

        // Ejecutar el comando y recuperar los resultados a través de DataReader
        using var reader = await cmd.ExecuteReaderAsync();

        // Recuperar el salario medio (parámetro de salida)

        median = Convert.ToDecimal(param.Value);

        // Leer los datos de cada empleado desde el DataReader
        while (await reader.ReadAsync())
        {
            employees.Add(new Employee
                {
                    Name = reader.GetString(0),
                    Job = reader.GetString(1),
                    Salary = reader.GetDecimal(2)
                });
        }
    }

5️⃣ Método 2 - Enfoque simplificado (Dapper)

El segundo enfoque utiliza Dapper, un ORM ligero, para simplificar el código necesario para llamar a nuestro procedimiento almacenado. Basta con definir el parámetro de salida mediante DynamicParameters y, a continuación, Dapper se encarga automáticamente de abrir la conexión, ejecutar el comando y recuperar directamente los resultados en forma de lista de objetos de tipo Employee. Por último, podemos recuperar fácilmente el valor del parámetro de salida correspondiente al salario medio. Este enfoque simplifica enormemente el código manteniendo el rendimiento y la legibilidad.

private decimal median;
private List employees = new();

private async Task LoadDataWithDapper()
{
    // Creación de la conexión a través de DB2Service
    using var conn = Db2Service.CreateConnection();

     // Definir el parámetro de salida 'medianSalary' con DynamicParameters
    var parameters = new DynamicParameters();
    parameters.Add("medianSalary", dbType: DbType.Decimal, direction: ParameterDirection.Output);

    // Dapper gestiona automáticamente la apertura y el cierre de la conexión, así como la lectura de los resultados
    employees = (await conn.QueryAsync(
        "MDSALARY.MEDIAN_RESULT_SET",
        parameters,
        commandType: CommandType.StoredProcedure)).ToList();

     // Obtención directa del salario medio (parámetro de salida)
    median = parameters.Get("medianSalary");
}

6️⃣ Visualización de resultados en un componente Blazor

Una vez recuperados los datos, todo lo que tenemos que hacer es mostrarlos limpiamente en nuestra interfaz Blazor utilizando una tabla.

Mediana de salarios en Blazor

Conclusion

Ya ha visto cómo llamar a un procedimiento almacenado en su IBM i desde una aplicación .NET con NTi: control detallado mediante un DataReader, o implementación rápida con Dapper. En ambos casos, NTi le permite integrar sus procedimientos almacenados SQL de forma sencilla y eficaz en sus aplicaciones .NET. En unas pocas líneas de código, obtendrá un resultado inmediatamente utilizable y adaptado a las prácticas modernas de desarrollo .NET.


Quentin Destrade