Lookup de banco de dados em filtros
Este tutorial mostra como criar um relatório com parâmetros cujas opções são buscadas diretamente de um banco de dados PostgreSQL. O usuário escolhe uma categoria e depois um produto dentro dela — os dois filtros são encadeados.
Funcionalidades cobertas:
LookupConfig.dbcom query parametrizada- Paginação com
pageSizee placeholders{_page_size}/{_offset} - Busca incremental com
{_search} - Cascata entre parâmetros com
dependsOn DbLookupResolverdo pacotesulfite_datasources
Estrutura do banco
CREATE TABLE categories (
id SERIAL PRIMARY KEY,
name TEXT NOT NULL
);
CREATE TABLE products (
id SERIAL PRIMARY KEY,
category_id INT REFERENCES categories(id),
name TEXT NOT NULL,
price NUMERIC(10, 2) NOT NULL
);Definição do relatório
{
"reportId": "products_by_category",
"reportName": "Produtos por Categoria",
"parameters": [
{
"id": "categoryId",
"label": "Categoria",
"type": "select",
"required": true,
"lookup": {
"source": "db",
"connectionRef": "main_db",
"query": "SELECT id, name FROM categories WHERE {_search} = '' OR name ILIKE '%' || {_search} || '%' ORDER BY name LIMIT {_page_size} OFFSET {_offset}",
"valueField": "id",
"labelField": "name",
"pageSize": 20,
"searchOnOpen": true,
"cacheSeconds": 300
}
},
{
"id": "productId",
"label": "Produto",
"type": "select",
"required": false,
"dependsOn": ["categoryId"],
"lookup": {
"source": "db",
"connectionRef": "main_db",
"query": "SELECT id, name FROM products WHERE category_id = {categoryId} AND ({_search} = '' OR name ILIKE '%' || {_search} || '%') ORDER BY name LIMIT {_page_size} OFFSET {_offset}",
"valueField": "id",
"labelField": "name",
"pageSize": 25,
"searchOnOpen": true,
"refreshOnDependencyChange": true,
"cacheSeconds": 60
}
},
{
"id": "startDate",
"label": "Data Início",
"type": "date",
"defaultValue": "2024-01-01",
"required": true
},
{
"id": "endDate",
"label": "Data Fim",
"type": "date",
"defaultValue": "2024-12-31",
"required": true,
"validation": {
"rule": "dateAfter",
"compareWith": "startDate",
"errorMessage": "Data Fim deve ser posterior à Data Início"
}
}
],
"dataSources": [
{
"id": "sales",
"type": "list",
"source": "external",
"url": "https://api.example.com/sales?categoryId={categoryId}&productId={productId}&from={startDate}&to={endDate}",
"schema": {
"product": "string",
"quantity": "integer",
"unitPrice": "number",
"total": "number",
"saleDate": "string"
}
}
],
"bands": [
{
"type": "header",
"id": "header",
"height": 60,
"elements": [
{
"type": "text",
"id": "title",
"x": 0, "y": 0,
"width": 780, "height": 30,
"content": "Relatório de Vendas por Produto",
"fontSize": 16,
"bold": true,
"align": "center"
},
{
"type": "text",
"id": "period",
"x": 0, "y": 35,
"width": 780, "height": 20,
"content": "Período: {startDate} a {endDate}",
"fontSize": 10,
"align": "center",
"color": "#555555"
}
]
},
{
"type": "detail",
"id": "detail",
"dataSourceId": "sales",
"height": 24,
"elements": [
{
"type": "field",
"id": "f_product",
"x": 0, "y": 4,
"width": 280, "height": 16,
"binding": "product",
"fontSize": 10
},
{
"type": "field",
"id": "f_date",
"x": 290, "y": 4,
"width": 100, "height": 16,
"binding": "saleDate",
"format": "date:dd/MM/yyyy",
"fontSize": 10
},
{
"type": "field",
"id": "f_qty",
"x": 400, "y": 4,
"width": 80, "height": 16,
"binding": "quantity",
"format": "integer",
"align": "right",
"fontSize": 10
},
{
"type": "field",
"id": "f_unit",
"x": 490, "y": 4,
"width": 130, "height": 16,
"binding": "unitPrice",
"format": "currency:BRL",
"align": "right",
"fontSize": 10
},
{
"type": "field",
"id": "f_total",
"x": 630, "y": 4,
"width": 150, "height": 16,
"binding": "total",
"format": "currency:BRL",
"align": "right",
"fontSize": 10,
"bold": true
}
]
},
{
"type": "summary",
"id": "summary",
"height": 36,
"elements": [
{
"type": "text",
"id": "lbl_total",
"x": 490, "y": 8,
"width": 130, "height": 20,
"content": "Total geral:",
"bold": true,
"align": "right"
},
{
"type": "aggregate",
"id": "grand_total",
"x": 630, "y": 8,
"width": 150, "height": 20,
"verb": "SUM",
"targetKey": "total",
"dataSourceId": "sales",
"format": "currency:BRL",
"align": "right",
"bold": true
}
]
}
]
}Configurando o DbLookupResolver
Registre a conexão e passe o resolver ao abrir o diálogo de filtros:
import 'package:sulfite_core/sulfite_core.dart';
import 'package:sulfite_datasources/sulfite_datasources.dart';
import 'package:sulfite_studio/sulfite_studio.dart';
// 1. Registrar a conexão (uma vez por sessão)
final registry = ConnectionRegistry();
registry.register(DatabaseConnectionEntry(
id: 'main_db',
host: 'db.example.com',
port: 5432,
database: 'myapp',
username: 'reports_user',
password: 'secret',
ssl: true,
));
// 2. Criar o resolver
final dbResolver = DbLookupResolver(registry: registry);
// 3. Exibir o diálogo de filtros
Future<void> printReport(BuildContext context, ReportDefinition report) async {
final params = await SulfiteFilterScreen.show(
context,
parameters: report.parameters,
lookupResolver: dbResolver,
);
if (params == null) return; // usuário cancelou
// 4. Gerar o relatório com os parâmetros selecionados
final engine = SulfiteEngine();
final pdfBytes = await engine.generate(
report: report,
data: await fetchData(params), // buscar dados conforme parâmetros
params: params,
);
// Exibir ou salvar pdfBytes...
}Como os placeholders são resolvidos
O DbLookupResolver injeta automaticamente os seguintes parâmetros em cada execução de query:
| Placeholder | Preenchido com |
|---|---|
{_search} | Texto digitado na caixa de busca (string vazia se não houver) |
{_page} | Número da página atual (começa em 1) |
{_page_size} | Valor de pageSize da config (ou 0) |
{_offset} | (page - 1) * pageSize |
Os parâmetros do relatório (ex.: {categoryId}) também são substituídos, permitindo cascata.
Nota de segurança: o resolver usa Sql.named do pacote postgres internamente — os valores são passados como parâmetros vinculados, não concatenados na string SQL.
Paginação no diálogo de seleção
Quando pageSize > 0, o diálogo carrega os itens em páginas à medida que o usuário rola a lista. Configure a query para suportar LIMIT / OFFSET:
SELECT id, name
FROM products
WHERE category_id = {categoryId}
AND ({_search} = '' OR name ILIKE '%' || {_search} || '%')
ORDER BY name
LIMIT {_page_size} OFFSET {_offset}Quando pageSize = 0, toda a lista é buscada de uma vez (adequado para tabelas pequenas).
Cache
O resolver mantém um cache em memória por cacheSeconds segundos. A chave de cache inclui connectionRef, a query template e todos os parâmetros — duas chamadas com parâmetros diferentes não colidem.
Para invalidar o cache manualmente:
dbResolver.dispose(); // limpa os caches e encerra conexões abertasUsando múltiplas conexões
O ConnectionRegistry suporta múltiplas entradas — útil quando lookups e dados vêm de bancos diferentes:
registry.register(DatabaseConnectionEntry(
id: 'catalog_db',
host: 'catalog.example.com',
database: 'catalog',
username: 'user',
password: 'pass',
));
registry.register(DatabaseConnectionEntry(
id: 'sales_db',
host: 'sales.example.com',
database: 'sales',
username: 'user',
password: 'pass',
));Cada LookupConfig.db referencia a conexão pelo connectionRef correspondente.