UUID (GUID), facilitar o índice de SGBDs

Os SGBDs, como MySQL, MSSQL, PostgreSQL, possuem em seus recursos criadores automáticos de identificação de registros. As mais comuns são o auto incremento e o uso de UUID ou GUID, tema desse artigo.

O UUID é uma sigla que significa ou representa o universally unique identifier, ou identificador único universal. Também é aceita a sigla GUID, globally unique identifier ou identificador único global.

O objetivo desse identificador é oferecer exclusividade de identificação, algo como em lugar nenhum no mundo haverá um identificador igual, mesmo sem estar ligado a qualquer rede de coordenação ou controle. Claro que literalmente isso é impossível, não há como garantir essa exclusividade, mas pelos cálculos de probabilidade, podemos considerar como um método seguro de identificação. Para definição exata desse identificador, podemos usar “identificador praticamente único”.

Para este artigo, eu me concentro não na forma de criação desses identificadores pelos seus SGBDs, mas na forma como são ordenados. Ao passo que para todos os SGBDs a ordenação de registros baseados em identificadores de auto incremento seja lógica, padronizada, do menor ao maior ou vice-versa, a ordenação de registros baseados em UUID não tem um padrão. Cada gerenciador de banco de dados criou seu padrão de ordenação, que poderá ser em ordem alfabética, binária, numérica, por data, por bytes significativos, …. Cada SGBD usa seu próprio sistema de ordenação. Isso acaba sendo um ponto a ser levado em conta quando você trabalha com sistemas ou bibliotecas adaptáveis, ou que usam bancos diferentes na mesma aplicação.

A seguir, eu proponho uma abordagem de criação de UUID de acordo com a ordenação dos SGBDs MySQL e MSSQL. Essa abordagem permite que a ordenação dos registros seja mais eficiente, independente de qual banco eu esteja acessando.

using System;
using System.Security.Cryptography;
namespace Yordi.Ferramentas
{
    public enum TipoGuid
    {
        /// <summary>
        /// Ordem alfabética
        /// </summary>
        MySQL,
        /// <summary>
        /// Ordem binária
        /// </summary>
        Oracle,
        /// <summary>
        /// Ordem baseada nos últimos 6 bytes (12 posições)
        /// </summary>
        MSSQL
    }
 
    public static class NewGuid
    {
        private static TipoGuid _seq = TipoGuid.MSSQL;
        private static readonly RNGCryptoServiceProvider _rng = new RNGCryptoServiceProvider();
 
        public static TipoGuid TipoGuid { set { _seq = value; } }
 
        public static Guid NewSequentialGuid()
        {
            byte[] randomBytes = new byte[10];
            _rng.GetBytes(randomBytes);
 
            long timestamp = DateTime.UtcNow.Ticks / 10000L;
            byte[] timestampBytes = BitConverter.GetBytes(timestamp);
 
            if (BitConverter.IsLittleEndian)
            {
                Array.Reverse(timestampBytes);
            }
 
            byte[] guidBytes = new byte[16];
 
            switch (_seq)
            {
                case TipoGuid.MySQL:
                case TipoGuid.Oracle:
                    Buffer.BlockCopy(timestampBytes, 2, guidBytes, 0, 6);
                    Buffer.BlockCopy(randomBytes, 0, guidBytes, 6, 10);
 
                    // Se o formato for string, reverter a ordem
                    // Se usa o sistema de extremidade...
                    if (_seq == TipoGuid.MySQL && BitConverter.IsLittleEndian)
                    {
                        Array.Reverse(guidBytes, 0, 4);
                        Array.Reverse(guidBytes, 4, 2);
                    }
                    break;
 
                case TipoGuid.MSSQL:
                    Buffer.BlockCopy(randomBytes, 0, guidBytes, 0, 10);
                    Buffer.BlockCopy(timestampBytes, 2, guidBytes, 10, 6);
                    break;
            }
            return new Guid(guidBytes);
        }
    }
}

Segundo os documentos da Microsoft, “computadores diferentes usam diferentes convenções para ordenar os bytes dentro de valores inteiros multibyte.” Exemplo, alguns SGBDs ‘colocam o byte mais significativo primeiro (conhecido como ordem big-endian) e outros colocam o byte menos significativo primeiro (conhecido como ordem little endian)’.

Para trabalhar com esses SGBDs (MySQL e MSSQL), que usam ordenação de bytes diferentes, acabei eu mesmo gerando o UUID, mas levando em conta a forma de ordenação.

Eu uso essa abordagem numa classe base de inserção, onde não importa o SGBD que esteja sendo usado no momento. Veja esse código:

private  int Insert(T obj) // onde T é uma classe
{
    string sql = InsertWithParameters(obj); // Método com reflection para construir o INSERT em TSQL
    bool isAuto = false;
    Guid guid = Guid.Empty;
    int resultado = 0;
    List colunas = Campos(obj); // Método para 'converter' propriedades das classes em colunas de tabela
    //Conexão genérica, que se conecta tanto a MSSQL quanto a MySQL (também aceita outros)
    using (DbConnection conexaoSql = Conexao.ObterConexao())
    {
        DbCommand cmm = conexaoSql.CreateCommand();
        foreach (var c in colunas)
        {
            //Se não for autoincremento carrega o valor enviado pelo cliente
            if (!c.IsAutoIncrement)
                cmm.Parameters.Add(CriaParameter(cmm, c)); // Resumo de CriaParameter: WithValue($"@{c.Nome}", c.Valor);
            //Se for autoincremento, mas do tipo GUID, carrega com valor próprio, 
            // de acordo com o banco usado. 
            else if (c.Tipo == Tipo.GUID)
            {
                guid = NewGuid.NewSequentialGuid(); // <- Este é o foco desse artigo.
                c.Valor = guid;
                cmm.Parameters.Add(CriaParameter(cmm, c)); // WithValue($"@{c.Nome}", c.Valor);
            }
            //Se for auto incremento convencional (de números), usa o do banco
            else
                isAuto = true;
        }
        try
        {
            if (isAuto)
            {
                var undefined = cmm.ExecuteScalar();
                int.TryParse(undefined.ToString(), out resultado);
            }
            else
                resultado = cmm.ExecuteNonQuery();
 
            if (guid != Guid.Empty)
                _msg = guid.ToString();
            else
                _msg = $"Retorno: {resultado}";
        }
        catch (Exception e)
        {
            _msg = e.Message; // Cuidado com esse retorno!
        }
        return resultado;
    }
}

Deixe um comentário

O seu endereço de e-mail não será publicado. Campos obrigatórios são marcados com *