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
Dinaupinstalado. 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, PGResultPGClient
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.
| Propiedad | Qué es |
|---|---|
IsConnected | true si la conexión está abierta. |
Host, Port, DatabaseName, CurrentSchema | Metadatos de la conexión, fijados al conectar. |
Description | Nombre de aplicación reportado a PostgreSQL. Alias de Options.ApplicationName. |
Options | El PGClientOptions con el que se creó el cliente. No cambia después. |
DefaultTags | Etiquetas 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:
| Propiedad | Defecto | Qué controla |
|---|---|---|
MaxPoolSize | 10 | Conexiones máximas del pool. No superes max_connections del servidor dividido entre las instancias de la app. |
MinPoolSize | 1 | Conexiones mínimas del pool. |
CommandTimeoutSeconds | 30 | Tiempo máximo por comando. |
ConnectTimeoutSeconds | 15 | Tiempo máximo para abrir la conexión. |
MaxRetries | 3 | Reintentos ante un fallo transitorio de conexión. Los errores SQL no se reintentan. |
RetryBaseDelayMs | 500 | Espera base entre reintentos; crece en potencias de 2. |
RetryMaxDelayMs | 8000 | Espera 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. |
UseSsl | true | Negocia 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étodo | Devuelve |
|---|---|
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étodo | Qué 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étodo | Devuelve |
|---|---|
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, BatchReadObjectsAsync | Materializan 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,descriptionniop, 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()copiaDefaultTagsal 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.