Dinaup.Database

PGClient conecta con PostgreSQL y ofrece lecturas tipadas, inserción, actualización, upsert parametrizado, lectura por lotes y opciones de pool y reintentos.

Dinaup.Database es el módulo de acceso a PostgreSQL del paquete Dinaup. Su clase PGClient conecta, ejecuta SQL y devuelve los resultados como valores, listas, diccionarios o modelos, sin DataReader a mano.

Antes de empezar

  • Paquete Dinaup instalado. Ver SDK .NET.
  • Host, puerto, usuario, contraseña y base de datos, o una cadena de conexión. Para leer la base de datos de tu empresa en Dinaup, activa antes PG Sync.
using Dinaup.Database;
using Dinaup.Database.Definitions;   // PGClientOptions, BaseModelConverter, PGResult

PGClient

PGClient es seguro entre hilos una vez conectado: varios hilos llaman a cualquier método Read o Execute a la vez y las conexiones se multiplexan por el pool de Npgsql. Los métodos síncronos y asíncronos están implementados por separado: ninguno envuelve al otro.

PropiedadQué es
IsConnectedtrue si la conexión está abierta.
Host, Port, DatabaseName, CurrentSchemaMetadatos de la conexión, fijados al conectar.
DescriptionNombre de aplicación reportado a PostgreSQL. Alias de Options.ApplicationName.
OptionsEl PGClientOptions con el que se creó el cliente. No cambia después.
DefaultTagsEtiquetas que viajan como comentario en cada consulta de este cliente. Vacío por defecto. Ver Etiquetas de consulta.

Conectar

// Parámetros sueltos
var client = new PGClient();
client.Connect("localhost", 5432, "myuser", "mypassword", "mydatabase");

// Cadena de conexión: key=value o URI postgresql://
client.Connect("postgresql://user:password@host:5432/dbname?sslmode=require");

// Constructores que conectan en el mismo paso
var c1 = new PGClient("postgresql://user:password@host:5432/dbname");
var c2 = new PGClient("localhost", 5432, "user", "password", "mydb");

// Asíncrono
await client.ConnectAsync("localhost", 5432, "user", "password", "mydb");
var ready = await PGClient.CreateAsync("localhost", 5432, "user", "password", "mydb");

EnsureConnectionOk() comprueba la conexión y devuelve bool. DuplicateConnection() y DuplicateConnectionAsync() crean otro PGClient conectado con las mismas opciones y esquema, para trabajadores en paralelo.

SSL está activo por defecto (PGClientOptions.UseSsl = true), así que Connect(host, port, username, password, databaseName) negocia TLS. Con cadena de conexión, controla el modo con sslmode (require, verify-full, disable).

Opciones: PGClientOptions

Pasa un PGClientOptions al constructor para ajustar pool, timeouts y reintentos. Todas las propiedades tienen valor por defecto:

PropiedadDefectoQué controla
MaxPoolSize10Conexiones máximas del pool. No superes max_connections del servidor dividido entre las instancias de la app.
MinPoolSize1Conexiones mínimas del pool.
CommandTimeoutSeconds30Tiempo máximo por comando.
ConnectTimeoutSeconds15Tiempo máximo para abrir la conexión.
MaxRetries3Reintentos ante un fallo transitorio de conexión. Los errores SQL no se reintentan.
RetryBaseDelayMs500Espera base entre reintentos; crece en potencias de 2.
RetryMaxDelayMs8000Espera máxima entre reintentos.
ApplicationName""Nombre visible en pg_stat_activity.
Schema""Esquema por defecto (search_path). Vacío usa el de PostgreSQL, normalmente public.
UseSsltrueNegocia TLS en la conexión por parámetros.
var options = new PGClientOptions
{
    ApplicationName = "importador-nocturno",
    ConnectTimeoutSeconds = 30,
    MaxPoolSize = 20
};

var client = new PGClient(options);
client.Connect("localhost", 5432, "user", "password", "mydb");

Leer

MétodoDevuelve
ReadValue(SQL)La primera celda de la primera fila como string, o null si no hay filas.
ReadList(SQL)List<string> con la primera columna de cada fila.
ReadKVDictionary(SQL)Dictionary<string, string> a partir de dos columnas: clave y valor.
ReadDictionaryList(SQL)Un diccionario por fila. La variante síncrona es un IEnumerable que se recorre a medida que llegan las filas; la asíncrona devuelve una List.
ReadObjectList<T>(SQL)Objetos T que heredan de BaseModelConverter, uno por fila.
ReadModel<T>(cols, SQL)Objetos T mapeados desde las columnas indicadas.
ReadModelsUpdated<T>(fromDate)Los T modificados desde una fecha, usando LastModifiedFieldDatetimeUTC del modelo.
Read(SQL, receiveColumns, optimizeMemory = false)Un PGResult con Data, ColumnMappings, ResultCount y navegación fila a fila (ReadNextRow, Item(columna), ToDictionary).
ReadCopy(SQL, optimizeMemory = false)Las celdas como string[] plano por el protocolo COPY, entre 5 y 10 veces más rápido que un lector normal. Sin metadatos.

Cada método de lectura tiene su variante con sufijo Async.

// Leer un valor único
var countStr = client.ReadValue("SELECT COUNT(*) FROM test_table;");
int totalRegistros = int.Parse(countStr);

// Leer una lista (una columna)
var nombres = client.ReadList("SELECT name FROM test_table ORDER BY id;");

// Leer una lista de diccionarios
var registros = client.ReadDictionaryList("SELECT id, name, value FROM test_table WHERE id < 10;");
foreach (var reg in registros)
{
    Console.WriteLine($"ID: {reg["id"]}, Name: {reg["name"]}, Value: {reg["value"]}");
}

Modelos: BaseModelConverter

Una clase que hereda de BaseModelConverter declara la tabla, las columnas y cómo pasar de diccionario a propiedades (FromDic) y al revés (ToDic). Los dos métodos son obligatorios.

public class TestModel : BaseModelConverter
{
    public int Id { get; set; }
    public string Name { get; set; }
    public int Value { get; set; }

    public override string Table => "test_table";
    public override string[] Fields => new[] { "id", "name", "value" };
    public override string LastModifiedFieldDatetimeUTC => "";   // vacío: ReadModelsUpdated no aplica

    public override void FromDic(Dictionary<string, string> dic)
    {
        Id = dic.GetM("id").INT(0);
        Name = dic.GetM("name");
        Value = dic.GetM("value").INT(0);
    }

    public override Dictionary<string, string> ToDic() => new()
    {
        { "id", Id.STR() },
        { "name", Name },
        { "value", Value.STR() }
    };
}

var modelos = client.ReadObjectList<TestModel>("SELECT * FROM test_table ORDER BY id;");
foreach (var m in modelos)
{
    Console.WriteLine($"{m.Id} - {m.Name} - {m.Value}");
}

GetM, INT y STR son extensiones del módulo de Utilidades.

Escribir

Todas las escrituras van parametrizadas: los valores viajan como parámetros de Npgsql y los nombres de tabla y columna solo admiten letras, dígitos, guion bajo y un punto opcional. Un null en el diccionario se envía como NULL.

MétodoQué hace
ExecuteNonQuery(SQL)Ejecuta una sentencia y devuelve las filas afectadas.
InsertRecord(tableName, record)Inserta un diccionario como fila.
InsertRecords(tableName, records)Inserta varias filas en una sentencia.
UpdateRecord(tableName, dataDict, idField, idValue)Actualiza la fila cuyo idField vale idValue.
InsertOrIgnoreRecord(tableName, dataDict)Inserta si no hay conflicto; devuelve bool.
InsertOrUpdateRecord(tableName, dataDict, idField, updateFields = null)INSERT con ON CONFLICT (idField) DO UPDATE. Con updateFields limita las columnas que se actualizan.
InsertOrUpdateRecords(tableName, dataDicts, idField, updateFields = null)El mismo upsert para varias filas en un solo viaje.

Cada método tiene su variante Async.

// Insertar
var nuevoRegistro = new Dictionary<string, string>
{
    { "name", "NuevoNombre" },
    { "value", "123" }
};
int rowsAffected = client.InsertRecord("test_table", nuevoRegistro);

// Actualizar
var datosActualizar = new Dictionary<string, string> { { "value", "999" } };
int filasActualizadas = client.UpdateRecord("test_table", datosActualizar, "id", "1");

// Upsert
var registroUpsert = new Dictionary<string, string>
{
    { "id", "100" },
    { "name", "Registro100" },
    { "value", "1000" }
};
int affected = client.InsertOrUpdateRecord("test_table", registroUpsert, "id");

Leer por lotes

Para volúmenes grandes, los métodos BatchRead piden una consulta de recuento y otra de datos, y devuelven lotes de batchSize filas. startFrom salta las primeras filas.

MétodoDevuelve
BatchReadDictionaries(description, countSQL, dataSQL, batchSize, startFrom = 0)Iterador de lotes, cada lote una List<Dictionary<string, string>>.
BatchReadObjects<T>(description, countSQL, dataSQL, batchSize, startFrom = 0)Igual, con objetos T que heredan de BaseModelConverter.
BatchReadDictionariesAsync, BatchReadObjectsAsyncMaterializan todos los lotes y devuelven la lista completa. Para recorrer los lotes a medida que llegan, usa el iterador síncrono.
var batches = client.BatchReadDictionaries(
    "Lectura en lotes",
    "SELECT COUNT(*) FROM test_table",
    "SELECT id, name, value FROM test_table ORDER BY id",
    1000
);

foreach (var batch in batches)
{
    Console.WriteLine("Lote de " + batch.Count + " registros");
    foreach (var reg in batch)
    {
        Console.WriteLine($"{reg["id"]} - {reg["name"]} - {reg["value"]}");
    }
}

Etiquetas de consulta

PGClient puede añadir a cada consulta un comentario sqlcommenter (/*clave='valor'*/) que las herramientas de monitorización de PostgreSQL usan para filtrar. Las etiquetas fijas del cliente van en DefaultTags. Cada método de lectura y de escritura admite además dos parámetros opcionales: description, el motivo de la consulta, y op, la operación en curso. En los BatchRead, description es el primer parámetro.

client.DefaultTags["app"] = "importador-nocturno";

var nombres = await client.ReadListAsync("SELECT name FROM test_table ORDER BY id;", description: "import.nombres", op: "tick.importar");
// Llega a PostgreSQL como:
// SELECT name FROM test_table ORDER BY id
// /*app='importador-nocturno',description='import.nombres',op='tick.importar'*/;
  • Sin DefaultTags, description ni op, el SQL sale tal cual.
  • Los valores se quedan en letras y dígitos sin tildes, _, -, . y /, con los espacios como _ y hasta 120 caracteres.
  • No etiquetes con datos de una fila, como un id o el nombre de un cliente: una etiqueta con demasiados valores distintos deja de servir para filtrar.
  • DuplicateConnection() copia DefaultTags al cliente nuevo.

Errores y liberación

Los errores SQL llegan como excepciones de Npgsql; los fallos transitorios de conexión se reintentan según MaxRetries antes de lanzar. PGClient implementa IDisposable:

using (var client = new PGClient("localhost", 5432, "user", "pass", "db"))
{
    try
    {
        client.ExecuteNonQuery("INSERT INTO test_table (name, value) VALUES ('Test', 1)");
    }
    catch (Exception ex)
    {
        Console.WriteLine("Error en la inserción: " + ex.Message);
    }
}

Insertar y leer modelos

var client = new PGClient("postgresql://user:pass@host:5432/mydb?sslmode=require");

var nuevo = new Dictionary<string, string>
{
    { "name", "NuevoRegistro" },
    { "value", "100" }
};
client.InsertRecord("test_table", nuevo);

var objetos = client.ReadObjectList<TestModel>("SELECT * FROM test_table WHERE name='NuevoRegistro'");
foreach (var obj in objetos)
{
    Console.WriteLine($"{obj.Id}: {obj.Name} - {obj.Value}");
}

Para leer los datos de tu empresa por SQL, activa la réplica en PG Sync y conecta PGClient a ella.

En esta página