Programação Por Equipe DevBras

Capa do artigo: SET no MySQL funcionou no Workbench e sumiu na aplicação: sessão, global e pool

SET no MySQL funcionou no Workbench e sumiu na aplicação: sessão, global e pool

Entenda por que SET time_zone e SET sql_mode valem só para uma conexão, como o pool de conexões espalha ou perde esse estado e onde configurar o que precisa valer sempre.

A clínica reclama que a consulta marcada para quinta às 22h30 aparece na agenda de sexta, à 1h30. Você abre o Workbench, roda SET time_zone = '-03:00', consulta a tabela e tudo parece certo. Fecha o chamado. No dia seguinte, o mesmo erro, porque a aplicação nunca rodou o seu SET: ele valeu só para a conexão do seu Workbench.

A resposta curta: SET variavel = valor sem escopo muda só a sessão atual, ou seja, aquela conexão, até ela fechar. A aplicação usa outras conexões, que nascem com os valores globais do servidor. O que precisa valer para todas as conexões vai para o arquivo de configuração do servidor ou para um comando executado ao abrir cada conexão, num lugar só do código, com teste. SET espalhado no meio das telas é imprevisível.

Por que o ajuste desaparece

O MySQL tem variáveis de sistema em dois escopos. A global é o padrão do servidor; a de sessão é a cópia que cada conexão recebe ao nascer e pode alterar para si. Os comandos fazem coisas diferentes:

  • SET time_zone = ... ou SET SESSION time_zone = ...: altera só a conexão atual.
  • SET GLOBAL time_zone = ...: altera o padrão para conexões novas. As já abertas, inclusive as do pool da aplicação, continuam com o valor antigo. E o valor se perde quando o servidor reinicia.
  • SET PERSIST (MySQL 8.0 em diante): altera o global e grava para sobreviver ao reinício. Não existe no MariaDB.
  • Arquivo de configuração (my.cnf ou my.ini): vale desde a subida do servidor e fica versionado junto com a infraestrutura.

O pool de conexões complica tudo. A aplicação abre, por exemplo, dez conexões e as reaproveita entre requisições. Se uma tela roda SET time_zone numa conexão, as próximas requisições que pegarem aquela conexão herdam o ajuste; as que pegarem as outras nove, não. O resultado é o pior tipo de bug: às vezes certo, às vezes errado. Alguns drivers limpam o estado da sessão ao reaproveitar a conexão, outros não, então o comportamento muda até quando você troca de biblioteca.

Como o fuso da sessão muda o dado gravado

NOW() devolve a hora no fuso da sessão, e colunas TIMESTAMP são convertidas do fuso da sessão para UTC ao gravar e de volta ao ler. Colunas DATETIME não convertem nada: guardam a data como veio. Na clínica, a coluna era TIMESTAMP e o servidor estava em UTC. No Workbench, com a sessão em -03:00, a consulta aparecia às 22h30 de quinta; na aplicação, com a sessão herdando UTC, a mesma linha voltava como 1h30 de sexta, e a tela exibia sem converter. Misturar os dois tipos de coluna com sessões em fusos diferentes piora o quadro: parte das linhas desloca, parte não.

Como configurar o que precisa valer sempre

  1. Descubra os valores atuais. Compare global e sessão numa conexão nova, sem nenhum SET manual.
  2. Decida a regra. Para fuso, a recomendação mais segura é gravar em UTC e converter só na exibição.
  3. Configure no servidor. Ponha a regra no arquivo de configuração e reinicie em janela de manutenção.
  4. Se não controla o servidor, configure ao abrir a conexão, num único ponto do código.
  5. Remova os SET espalhados pelas telas e consultas.
  6. Escreva um teste que pega conexões novas do pool e confere os valores.
  7. Documente a regra no README ou na infraestrutura como código, para o próximo ambiente nascer igual.

Exemplo: diagnóstico e correção na clínica

O diagnóstico, rodado numa conexão recém-aberta:

SELECT @@global.time_zone, @@session.time_zone, @@system_time_zone;
-- +--------------------+---------------------+--------------------+
-- | @@global.time_zone | @@session.time_zone | @@system_time_zone |
-- | SYSTEM             | SYSTEM              | UTC                |
-- +--------------------+---------------------+--------------------+

SELECT @@global.sql_mode;

A correção no servidor, no arquivo de configuração:

[mysqld]
default-time-zone = '+00:00'
sql_mode = 'STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_ENGINE_SUBSTITUTION'

O deslocamento numérico '+00:00' funciona sempre. Nomes como 'America/Sao_Paulo' só funcionam se as tabelas de fuso do MySQL tiverem sido carregadas no servidor; sem elas, o servidor recusa o valor.

Se a aplicação roda num banco gerenciado em que você não edita o arquivo, configure ao abrir cada conexão. Em Node com mysql2, o pool emite o evento connection uma vez para cada conexão física criada:

// db.js
const mysql = require('mysql2');

const pool = mysql.createPool({
  host: process.env.DB_HOST,
  user: process.env.DB_USER,
  password: process.env.DB_PASSWORD,
  database: 'clinica',
  connectionLimit: 10
});

pool.on('connection', (conn) => {
  conn.query("SET time_zone = '+00:00'");
});

module.exports = pool.promise();

E o teste que pega duas conexões ao mesmo tempo (para forçar conexões diferentes) e confere:

// fuso.test.js
const db = require('./db');

async function main() {
  const [a, b] = await Promise.all([db.getConnection(), db.getConnection()]);
  try {
    for (const conn of [a, b]) {
      const [linhas] = await conn.query('SELECT @@session.time_zone AS tz');
      if (linhas[0].tz !== '+00:00') throw new Error('fuso inesperado: ' + linhas[0].tz);
    }
    console.log('ok: todas as conexões em UTC');
  } finally {
    a.release();
    b.release();
    await db.end();
  }
}

main().catch((e) => { console.error(e); process.exit(1); });

Outros drivers têm mecanismos equivalentes, como um comando inicial na configuração da conexão. Use o que o seu driver documenta e mantenha esse ponto único. Para o lado da aplicação, com o fuso de quem lê, veja grave o instante em UTC e mostre no fuso de quem lê e, em PHP, por que o pedido nasce ontem.

Variáveis de sessão que causam mais estrago

O fuso é o caso mais visível, mas não o único. sql_mode alterado por sessão faz uma conexão aceitar data zerada e outra rejeitar. SET NAMES diferente entre conexões grava acento quebrado em parte das linhas. E SET foreign_key_checks = 0, usado numa importação e esquecido numa conexão do pool, deixa a aplicação gravar registros órfãos até a conexão ser descartada. Comandos assim só devem existir em scripts de manutenção com conexão própria, que fecha no fim.

Erros comuns

  • Corrigir no cliente SQL e fechar o chamado. O ajuste valeu só para a sua conexão; a aplicação continua errada.
  • Usar SET GLOBAL e achar que resolveu. As conexões já abertas no pool seguem com o valor antigo, e tudo volta no próximo reinício do servidor.
  • SET no meio do código de uma tela. Uma conexão do pool fica diferente das outras e o bug aparece de forma intermitente.
  • Usar fuso por nome sem as tabelas de fuso carregadas. A configuração é recusada e o servidor pode nem subir com ela no arquivo.
  • Desligar checagem de chave estrangeira numa conexão compartilhada. Registros órfãos entram em silêncio.
  • Não documentar. O próximo servidor nasce com outro padrão e o bug volta na migração.

Checklist

  • Você comparou @@global e @@session numa conexão nova, sem SET manual.
  • Fuso e sql_mode estão no arquivo de configuração do servidor ou num único ponto de abertura de conexão.
  • Não há SET de variável de sessão espalhado pelo código de telas.
  • Um teste automatizado confere os valores em mais de uma conexão do pool.
  • Scripts de manutenção que mexem em variáveis usam conexão própria.
  • A regra está documentada junto da infraestrutura.

Abra uma conexão nova com o usuário da aplicação, sem rodar nenhum SET, e grave uma data: ela sai no dia e na hora certos?

Avalie este artigo

Referência: http://www.tecposter.com/fix-errors-for-mysql/