Notificações de Alterações de Dados em Tempo Real com PostgreSQL e .NET

O PostgreSQL oferece uma poderosa funcionalidade chamada NOTIFY e LISTEN, que permite enviar notificações assíncronas para as aplicações. Isso é extremamente útil para cenários em que precisamos ser informados sobre mudanças nos dados em tempo real, como operações de inserção, atualização ou exclusão em tabelas. Com essa abordagem, podemos garantir que as aplicações estejam sempre sincronizados e atualizados de forma eficiente.

Neste artigo, vou detalhar como implementar essa funcionalidade em .NET utilizando o pacote Npgsql para receber notificações e monitorar alterações no banco de dados. Além disso, vou abordar os benefícios práticos e os ganhos de performance que você pode obter ao utilizar essa técnica em seus projetos.

Por que usar notificações em tempo real?

Em um modelo tradicional de aplicações que utilizam bancos de dados, as consultas geralmente ocorrem de forma periódica, com o objetivo de verificar se houve alterações nas tabelas ou nos registros de interesse. Esse processo, conhecido como polling, pode ser extremamente ineficiente, principalmente em sistemas com grandes volumes de dados ou que requerem alta responsividade. Algumas desvantagens do polling incluem:

  • Uso excessivo de recursos: Consultas repetidas ao banco de dados consomem CPU, memória e I/O de forma desnecessária, sobrecarregando a infraestrutura.
  • Latência: Há um atraso natural entre a alteração nos dados e a próxima consulta, o que pode resultar em uma experiência não responsiva para os usuários.

Por outro lado, com notificações em tempo real, você pode receber alertas instantâneos sempre que uma alteração ocorre, eliminando a necessidade de verificar o banco de dados continuamente. Essa abordagem oferece os seguintes benefícios:

  1. Redução de latência: Você é informado sobre mudanças imediatamente, sem depender de consultas periódicas.
  2. Eficiência de recursos: Menos consultas ao banco de dados significa menor carga no servidor e melhor escalabilidade.
  3. Melhoria na experiência do usuário: A aplicação responde mais rápido a mudanças, resultando em uma interface mais fluida e interativa.

Pré-requisitos

Criando a Tabela e o Trigger no PostgreSQL

Primeiramente, vamos criar uma tabela que queremos monitorar e configurar um trigger para emitir notificações sempre que houver mudanças. A tabela de exemplo será chamada produtos, onde armazenaremos informações básicas sobre produtos, como nome e preço.

Use o comando SQL abaixo para criar a tabela no PostgreSQL:

CREATE TABLE produtos (
    id SERIAL PRIMARY KEY,
    nome VARCHAR(100),
    preco NUMERIC
)

Agora, vamos criar uma função em PL/pgSQL para emitir uma notificação sempre que houver uma alteração na tabela. Usaremos a função pg_notify para enviar a notificação em formato JSON:

CREATE FUNCTION public."NotifyOnDataChange"()
  RETURNS trigger
  LANGUAGE 'plpgsql'
AS $BODY$ 
DECLARE 
  data JSON;
  notification JSON;
BEGIN
  IF (TG_OP = 'DELETE') THEN
    data = row_to_json(OLD);
  ELSE
    data = row_to_json(NEW);
  END IF;

  notification = json_build_object(
            'table',TG_TABLE_NAME,
            'action', TG_OP,
            'data', data);  
            
    PERFORM pg_notify('datachange', notification::TEXT);
  RETURN NEW;
END
$BODY$;

CREATE FUNCTION public."CreateOnDataChangeForAllTables"()
  RETURNS void
  LANGUAGE 'plpgsql'
AS $BODY$
DECLARE  
  createTriggerStatement TEXT;
BEGIN
  FOR createTriggerStatement IN SELECT
    'CREATE TRIGGER OnDataChange AFTER INSERT OR DELETE OR UPDATE ON '
    || tab_name
    || ' FOR EACH ROW EXECUTE PROCEDURE public."NotifyOnDataChange"();' AS trigger_creation_query
  FROM (
    SELECT
      quote_ident(table_schema) || '.' || quote_ident(table_name) as tab_name
    FROM
      information_schema.tables
    WHERE
      table_schema NOT IN ('pg_catalog', 'information_schema')
      AND table_schema NOT LIKE 'pg_toast%'
  ) as TableNames
  LOOP
    EXECUTE  createTriggerStatement;
  END LOOP;
END
$BODY$;

Essa função converte a linha alterada (NEW) em JSON e emite uma notificação através do canal datachange.

Por fim, vamos associar essa função de notificação às operações de inserção, atualização e exclusão da tabela produtos:

CREATE TRIGGER produtos_data_change
AFTER INSERT OR UPDATE OR DELETE ON produtos
FOR EACH ROW EXECUTE FUNCTION NotifyOnDataChange();

Com isso, sempre que houver uma operação de INSERTUPDATE ou DELETE na tabela produtos, a função NotifyOnDataChange() será chamada, emitindo uma notificação para o canal.

Implementando o Listener no .NET

Agora que o banco de dados está configurado para emitir notificações, vamos implementar o código em .NET para ouvir essas notificações e processá-las em tempo real.

Vamos escrever o código em C# para conectar-se ao banco de dados, escutar as notificações e exibi-las no console:

public class WorkerAuditPostgresql : BackgroundService
{
	protected override async Task ExecuteAsync(CancellationToken stoppingToken)
	{
		while (true)
		{
			await NotifyAsyn();
		}
	}

	private async Task NotifyAsyn()
	{
		try
		{
			var connString = "Host=localhost;Username=postgres;Password=mysecretpassword;Database=mydb";

			await using var connection = new NpgsqlConnection(connString);

			connection.Notification += async (o, e) =>
			{
				try
				{
					var result = JsonSerializer.Deserialize<AuditPostgresqlModel>(e.Payload);

					Log.Information($"Notificação recebida: {result.Json}");
				}
				catch (Exception ex)
				{
					Log.Error(ex, nameof(connection.Notification) + " - " + e?.Payload);
				}
			};

			await using (var cmd = new NpgsqlCommand("LISTEN datachange;", connection))
			{
				await cmd.ExecuteNonQueryAsync();
			}

			while (true)
				await connection.WaitAsync();
		}
		catch (Exception ex)
		{
			if (ex is PostgresException { SqlState: "57P01" })
				NpgsqlConnection.ClearAllPools();

			await Task.Delay(TimeSpan.FromSeconds(2));
			Log.Error(ex, nameof(ExecuteAsync));
		}
	}

}

Neste código, a aplicação se conecta ao PostgreSQL e escuta o canal datachange. Quando uma notificação é recebida, ela é exibida no console.

Em seguida, configure a chamada deste worker service na classe program.cs:

var builder = WebApplication.CreateBuilder(args);

Log.Logger = new LoggerConfiguration()
	.MinimumLevel.Debug()
	.WriteTo.Console(restrictedToMinimumLevel: LogEventLevel.Debug)
	.CreateLogger();

builder.Services.AddRouting(options => options.LowercaseUrls = true);
builder.Services.AddControllers();

builder.Services.AddSwagger(builder.Configuration);

builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();

builder.Services.AddHostedService<WorkerAuditPostgresql>();

var app = builder.Build();

app.UseMiddleware<ErrorHandlingMiddleware>();

app.UseHttpsRedirection();
app.UseRouting();

app.UseSwaggerDoc();

app.MapControllers();

app.Run();

Testando a Implementação

Agora que tudo está configurado, vamos inserir, atualizar e excluir registros na tabela produtos e observar como as notificações são recebidas na aplicação .NET.

Execute o seguinte comando SQL para inserir um produto na tabela:

INSERT INTO produtos (nome, preco) VALUES ('Café', 15.90);

No console do .NET, você verá algo como:

Notificação recebida: {"id":1,"nome":"Café","preco":15.90}

Agora, vamos atualizar o preço do produto:

UPDATE produtos SET preco = 18.50 WHERE id = 1;

A saída será:

Notificação recebida: {"id":1,"nome":"Café","preco":18.50}

. Sempre que ocorre uma alteração no banco de dados, a notificação é recebida e processada em tempo real.

Ganhos de Implementação

Ao utilizar essa abordagem, você pode observar vários benefícios:

  1. Eficiência Operacional: Com o uso de notificações, você economiza recursos de CPU e I/O, liberando capacidade para outras operações.
  2. Escalabilidade Melhorada: Aplicações que precisam monitorar muitas tabelas em paralelo podem escalar mais facilmente, pois o sistema lida apenas com as notificações relevantes, sem a sobrecarga de verificações contínuas.
  3. Experiência de Usuário Aprimorada: Em aplicações que necessitam de atualizações em tempo real, como dashboards de monitoramento, e-commerces ou plataformas de investimento, a agilidade e precisão das atualizações são cruciais para garantir uma experiência fluida e sem atrasos.

Possíveis Desafios e Soluções

Claro, algumas questões podem surgir ao trabalhar com notificações em tempo real:

  • Perda de Conexão: Se a conexão com o PostgreSQL for perdida, você precisará reestabelecer a inscrição no canal de notificações. Para isso, pode ser interessante implementar uma lógica de reconexão automática no aplicativo. (Polly)
  • Contrapressão: Se o volume de notificações for muito alto, você pode enfrentar um problema de contrapressão. Uma estratégia seria processar as notificações em lotes ou configurar mecanismos de fallback para garantir que nenhuma alteração importante seja perdida.

Esses desafios podem ser contornados com boas práticas de programação assíncrona e controle de exceções adequados.

Conclusão

Implementar notificações em tempo real no PostgreSQL com .NET é uma solução poderosa para melhorar a performance, escalabilidade e responsividade das aplicações. O uso de triggers e notificações assíncronas permite que seus aplicativos fiquem sempre sincronizados com as mudanças de dados sem a necessidade de consultas repetitivas. Isso resulta em uma aplicação mais eficiente, economizando recursos e proporcionando uma experiência superior para o usuário.

Espero que este artigo tenha fornecido uma visão clara de como usar LISTEN e NOTIFY com PostgreSQL e .NET. Se você já usou essa abordagem ou tem alguma dúvida, fique à vontade para compartilhar nos comentários. Sua opinião é importante!

Os detalhes completo desta série você encontra no meu GitHubhttps://github.com/hgmauri/sample-notity-postgresql

Leave a Comment

O seu endereço de email não será publicado. Campos obrigatórios marcados com *