Desenvolvido por: Carlos Eduardo Paulino. Youtube: https://www.youtube.com/@delphicleancode (se inscreva e compartilhe para receber projetos como este em primeira mão) :-)
Biblioteca de SQL fluente (Fluent SQL Builder) em Object Pascal com compatibilidade para Delphi e Lazarus.
A biblioteca SQLBuilder oferece uma maneira mais segura, legível e manutenível de criar consultas SQL em seus projetos Object Pascal. Através da interface fluente, você pode construir desde consultas simples até operações complexas com múltiplos JOINs, CTEs, transações e migrações de bancos de dados. Além disso, a biblioteca tem suporte para diferentes bancos de dados.
Exemplos práticos de como utilizar a biblioteca SQLBuilder em projetos Delphi e Lazarus.
- SELECT Básico
- Selecionar todos os registros
- Selecionar campos específicos
- INSERT Simples
- UPDATE Simples
- DELETE Simples
- SELECT com JOIN
- Condições Complexas
- Subconsultas
- Funções de Agregação
- INSERT com Subconsulta
- Múltiplos JOINs
- CTEs (Common Table Expressions)
- Transações
- Paginação
- Migrations
- Criar Tabelas
- Adicionar Índices
- Adicionar Chaves Estrangeiras
- Configuração do Dialeto SQL
- MySQL
- Firebird
- PostgreSQL
- SQLite
- SQL Server
- Oracle
- Formatação e Visualização do SQL
- Usando o Helper para Evitar SQL Injection
Selecionar todos os registros de uma tabela:
var SQL: string;
begin
SQL := TSQL.SELECT('*')
.FROM('clientes')
.WHERE('ativo = ''S''')
.ORDER_BY('nome')
.AsString;
// Resultado:
// SELECT * FROM clientes WHERE ativo = 'S' ORDER BY nome
end;
begin
SQL := TSQL.SELECT(['id', 'nome', 'email'])
.FROM('usuarios')
.LIMIT(10)
.AsString;
// Resultado:
// SELECT id, nome, email FROM usuarios LIMIT 10
end;
begin
SQL := TSQL.INSERT_INTO('produtos')
.COLUMNS(['descricao', 'preco', 'estoque'])
.VALUES(['Teclado Mecânico', 299.90, 15])
.AsString;
// Resultado:
// INSERT INTO produtos (descricao, preco, estoque)
// VALUES ('Teclado Mecânico', 299.90, 15)
end;
begin
.SET('nome', 'Periféricos')
.SET('ativo', True)
.WHERE('id = 5')
.AsString;
// Resultado:
// UPDATE categorias SET nome = 'Periféricos', ativo = 1 WHERE id = 5
end;
begin
SQL := TSQL.DELETE_FROM('log_acessos')
.WHERE('data < ''2023-01-01''')
.AsString;
// Resultado:
// DELETE FROM log_acessos WHERE data < '2023-01-01'
end;
begin
SQL := TSQL.SELECT(['p.id', 'p.descricao', 'c.nome as categoria'])
.FROM('produtos p')
.INNER_JOIN('categorias c', 'c.id = p.categoria_id')
.WHERE('p.ativo = ''S''')
.AND_WHERE('p.preco > 100')
.ORDER_BY('p.descricao')
.AsString;
// Resultado:
// SELECT p.id, p.descricao, c.nome as categoria
// FROM produtos p
// INNER JOIN categorias c ON c.id = p.categoria_id
// WHERE p.ativo = 'S' AND p.preco > 100
// ORDER BY p.descricao
end;
Utilização da classe TCondition para construir condições:
begin
SQL := TSQL.SELECT(['id', 'nome', 'email', 'telefone'])
.FROM('clientes')
.WHERE(TCondition.Field('status').Equal('A'))
.AND_WHERE(TCondition.Field('ultima_compra')
.GreaterThan(EncodeDate(2023, 1, 1)))
.AND_WHERE('(saldo_devedor = 0 OR possui_credito = ''S'')')
.ORDER_BY('nome')
.AsString;
// Resultado:
// SELECT id, nome, email, telefone
// FROM clientes
// WHERE status = 'A' AND ultima_compra > '2023-01-01'
// AND (saldo_devedor = 0 OR possui_credito = 'S')
// ORDER BY nome
end;
begin
var SubQuery := TSQL.SELECT('id')
.FROM('categorias')
.WHERE('tipo = ''PRIORITARIA''')
.AsSubquery();
SQL := TSQL.SELECT(['p.id', 'p.descricao', 'p.preco'])
.FROM('produtos p')
.WHERE('p.categoria_id IN ' + SubQuery)
.ORDER_BY('p.preco DESC')
.AsString;
// Resultado:
// SELECT p.id, p.descricao, p.preco
// FROM produtos p
// WHERE p.categoria_id IN (SELECT id FROM categorias WHERE tipo = 'PRIORITARIA')
// ORDER BY p.preco DESC
end;
begin
SQL := TSQL.SELECT([
'c.id',
'c.nome',
TExpr.Sum('v.valor_total') + ' as valor_total',
TExpr.Count('v.id') + ' as qtd_vendas'
])
.FROM('clientes c')
.LEFT_JOIN('vendas v', 'v.cliente_id = c.id')
.WHERE('v.data BETWEEN ''2023-01-01'' AND ''2023-12-31''')
.GROUP_BY(['c.id', 'c.nome'])
.HAVING(TExpr.Sum('v.valor_total') + ' > 10000')
.ORDER_BY('valor_total DESC')
.AsString;
// Resultado:
// SELECT c.id, c.nome, SUM(v.valor_total) as valor_total, COUNT(v.id) as qtd_vendas
// FROM clientes c
// LEFT JOIN vendas v ON v.cliente_id = c.id
// WHERE v.data BETWEEN '2023-01-01' AND '2023-12-31'
// GROUP BY c.id, c.nome
// HAVING SUM(v.valor_total) > 10000
// ORDER BY valor_total DESC
end;
begin
SQL := TSQL.INSERT_INTO('produtos_promocao')
.COLUMNS(['produto_id', 'descricao', 'preco_promocional'])
.VALUES(
TSQL.SELECT(['id', 'descricao', 'preco * 0.8'])
.FROM('produtos')
.WHERE('estoque > 20')
.AsString
)
.AsString;
// Resultado:
// INSERT INTO produtos_promocao (produto_id, descricao, preco_promocional)
// SELECT id, descricao, preco * 0.8
// FROM produtos
// WHERE estoque > 20
end;
begin
SQL := TSQL.SELECT([
'p.id',
'p.numero_pedido',
'c.nome AS cliente',
'v.nome AS vendedor',
'SUM(i.quantidade * i.valor_unitario) AS valor_total',
'p.data_pedido',
TCoalesce.Value('p.data_entrega', 'Pendente') + ' AS entrega',
'f.descricao AS forma_pagamento'
])
.FROM('pedidos p')
.INNER_JOIN('clientes c', 'c.id = p.cliente_id')
.INNER_JOIN('vendedores v', 'v.id = p.vendedor_id')
.INNER_JOIN('itens_pedido i', 'i.pedido_id = p.id')
.LEFT_JOIN('produtos pr', 'pr.id = i.produto_id')
.LEFT_JOIN('formas_pagamento f', 'f.id = p.forma_pagamento_id')
.WHERE('p.status <> ''C''')
.AND_WHERE(TCondition.Field('p.data_pedido').Between(
EncodeDate(2023, 1, 1),
EncodeDate(2023, 12, 31)
))
.AND_WHERE(TCondition.Field('pr.categoria_id').In_([1, 3, 5, 8]))
.GROUP_BY([
'p.id', 'p.numero_pedido', 'c.nome', 'v.nome',
'p.data_pedido', 'p.data_entrega', 'f.descricao'
])
.HAVING('SUM(i.quantidade * i.valor_unitario) > 500')
.ORDER_BY('p.data_pedido DESC')
.LIMIT(50)
.AsString;
end;
begin
TSQL.MySQLMode; // Assegurar compatibilidade MySQL
SQL := TSQL.WITH('vendas_por_cliente AS (
SELECT
cliente_id,
SUM(valor_total) as total_vendas,
COUNT(*) as qtd_vendas,
MAX(data) as ultima_venda
FROM
vendas
WHERE
data >= ''2023-01-01''
GROUP BY
cliente_id
)')
.SELECT([
'c.id',
'c.nome',
'c.email',
'COALESCE(vpc.total_vendas, 0) as total_compras',
'COALESCE(vpc.qtd_vendas, 0) as quantidade_compras',
'vpc.ultima_venda',
'CASE WHEN vpc.total_vendas > 10000 THEN ''VIP'' ELSE ''Regular'' END as categoria_cliente'
])
.FROM('clientes c')
.LEFT_JOIN('vendas_por_cliente vpc', 'vpc.cliente_id = c.id')
.WHERE('c.ativo = 1')
.ORDER_BY('total_compras DESC')
.AsString;
end;
begin
// Iniciar transação
TSQL.BEGIN_TRANSACTION;
// Atualizar estoque
var SQL1 := TSQL.UPDATE('produtos')
.SET('estoque', TExpr.Sum('estoque') + ' - :quantidade')
.WHERE('id = :produto_id')
.PARAM('quantidade', 5)
.PARAM('produto_id', 123)
.AsString;
// Inserir pedido
var SQL2 := TSQL.INSERT_INTO('pedidos')
.COLUMNS(['cliente_id', 'data', 'valor_total', 'status'])
.VALUES([42, TExpr.CurrentDate, 1250.99, 'P'])
.AsString;
// Obter ID do último pedido inserido (específico MySQL)
var SQL3 := 'SELECT LAST_INSERT_ID() as pedido_id';
// Inserir itens do pedido
var SQL4 := TSQL.INSERT_INTO('itens_pedido')
.COLUMNS(['pedido_id', 'produto_id', 'quantidade', 'valor_unitario'])
.VALUES([':pedido_id', 123, 5, 250.20])
.AsString;
// Completar a transação
var SQL5 := TSQL.COMMIT.AsString;
// Todas as consultas acima seriam executadas em sequência dentro de uma transação
end;
procedure ExemploPaginacao(const APagina, ATamanhoPagina: Integer);
var
SQLCount, SQLData: string;
begin
// Primeiro obter contagem total para calcular número de páginas
SQLCount := TSQL.SELECT('COUNT(*) as total')
.FROM('produtos p')
.INNER_JOIN('categorias c', 'c.id = p.categoria_id')
.INNER_JOIN('fornecedores f', 'f.id = p.fornecedor_id')
.WHERE('p.preco BETWEEN 100 AND 1000')
.AND_WHERE('p.estoque > 0')
.AND_WHERE('c.ativo = 1')
.AsString;
// Depois obter registros da página atual
SQLData := TSQL.SELECT([
'p.id',
'p.codigo',
'p.descricao',
'p.preco',
'p.estoque',
'c.nome as categoria',
'f.nome as fornecedor'
])
.FROM('produtos p')
.INNER_JOIN('categorias c', 'c.id = p.categoria_id')
.INNER_JOIN('fornecedores f', 'f.id = p.fornecedor_id')
.WHERE('p.preco BETWEEN 100 AND 1000')
.AND_WHERE('p.estoque > 0')
.AND_WHERE('c.ativo = 1')
.ORDER_BY('p.descricao')
.PAGINATE(APagina, ATamanhoPagina)
.AsString;
end;
function CriarMigration: string;
var
Migration: TMigration;
begin
Migration := TMigration.Create('criar_tabela_produtos', '1.0.0');
try
// Adicionar comando para criar tabela
Migration.AddCommand(
TSQL.CREATE_TABLE('produtos')
.ADD_COLUMN('id', 'INT', 'NOT NULL AUTO_INCREMENT')
.ADD_COLUMN('codigo', 'VARCHAR(20)', 'NOT NULL')
.ADD_COLUMN('descricao', 'VARCHAR(200)', 'NOT NULL')
.ADD_COLUMN('preco', 'DECIMAL(10,2)', 'NOT NULL DEFAULT 0')
.ADD_COLUMN('estoque', 'INT', 'NOT NULL DEFAULT 0')
.ADD_COLUMN('categoria_id', 'INT', 'NULL')
.ADD_COLUMN('fornecedor_id', 'INT', 'NULL')
.ADD_COLUMN('data_cadastro', 'DATETIME', 'NOT NULL')
.ADD_COLUMN('ativo', 'TINYINT(1)', 'NOT NULL DEFAULT 1')
.PRIMARY_KEY(['id'])
.AsString
);
// Adicionar comandos para índices
Migration.AddCommand(
TSQL.CREATE_INDEX('idx_produto_codigo', 'produtos', ['codigo'], True).AsString
);
Migration.AddCommand(
TSQL.CREATE_INDEX('idx_produto_categoria', 'produtos', ['categoria_id']).AsString
);
// Adicionar comandos para chaves estrangeiras
Migration.AddCommand(
TSQL.ALTER_TABLE('produtos')
.ADD_CONSTRAINT('fk_produto_categoria',
'FOREIGN KEY (categoria_id) REFERENCES categorias(id)')
.AsString
);
Migration.AddCommand(
TSQL.ALTER_TABLE('produtos')
.ADD_CONSTRAINT('fk_produto_fornecedor',
'FOREIGN KEY (fornecedor_id) REFERENCES fornecedores(id)')
.AsString
);
Result := Migration.AsScript;
finally
Migration.Free;
end;
end;
A biblioteca SQLBuilder suporta vários bancos de dados, cada um com suas peculiaridades sintáticas. Veja como configurar:
// Configurar para MySQL (padrão)
TSQL.MySQLMode;
// Configurar para Firebird
TSQL.FirebirdMode;
// Configurar para PostgreSQL
TSQL.PostgreSQLMode;
// Configurar para SQLite
TSQL.SQLiteMode;
// Configurar para SQL Server
TSQL.SQLServerMode;
// Configurar para Oracle
TSQL.OracleMode;begin
var SQL := TSQL.SELECT(['id', 'nome', 'email'])
.FROM('usuarios')
.WHERE('status = ''A''')
.AND_WHERE('ultimo_acesso > ''2023-01-01''')
.ORDER_BY('nome');
// SQL como string simples
Memo1.Lines.Text := SQL.AsString;
// SQL formatado com indentação
Memo2.Lines.Text := SQL.AsFormattedString;
end;begin
var IdUsuario := 10;
var NomeProduto := 'Monitor LED 24"';
// Errado (vulnerável a SQL Injection):
// var SQL := 'SELECT * FROM produtos WHERE id = ' + IntToStr(IdUsuario);
// Correto usando SQLBuilder:
var SQL := TSQL.SELECT('*')
.FROM('produtos')
.WHERE('id = ' + TSQL.FormatValue(IdUsuario))
.AND_WHERE('nome LIKE ' + TSQL.FormatValue('%' + NomeProduto + '%'))
.AsString;
end;begin
var DataVenda: TDateTime := 0;
var Observacao: string := '';
var ValorDesconto: Double := 0;
SQL := TSQL.UPDATE('vendas')
.SET('data_venda', DataVenda) // Se DataVenda = 0, será tratado como NULL
.SET('observacao', Observacao) // Se Observacao estiver vazia, será tratado como NULL
.SET('valor_desconto', ValorDesconto) // Se ValorDesconto = 0, será incluído como 0, não NULL
.WHERE('id = 123')
.AsString;
// Para forçar NULL explicitamente:
SQL := TSQL.UPDATE('vendas')
.SET('data_cancelamento', Null)
.WHERE('id = 123')
.AsString;
end;begin
SQL := TSQL.SELECT([
'id',
'nome',
'CASE WHEN tipo = ''F'' THEN ''Física'' ' +
'WHEN tipo = ''J'' THEN ''Jurídica'' ' +
'ELSE ''Desconhecido'' END AS tipo_pessoa',
TExpr.IIF('ativo = 1', '''Sim''', '''Não''') + ' AS situacao'
])
.FROM('pessoas')
.WHERE(TCondition.Field('cidade').In_(['São Paulo', 'Rio de Janeiro', 'Belo Horizonte']))
.AND_WHERE(TCondition.Field('cadastro').GreaterOrEqual(EncodeDate(2023, 1, 1)))
.ORDER_BY('nome')
.AsString;
end;procedure ConsultaDinamica(const AFiltros: TDictionary<string, Variant>);
var
Query: TSQL;
begin
Query := TSQL.SELECT('*')
.FROM('clientes');
// Aplicar filtros dinamicamente
if AFiltros.ContainsKey('nome') and (AFiltros['nome'] <> '') then
Query.AND_WHERE(TCondition.Field('nome').Contains(AFiltros['nome']));
if AFiltros.ContainsKey('cidade') and (AFiltros['cidade'] <> '') then
Query.AND_WHERE(TCondition.Field('cidade').Equal(AFiltros['cidade']));
if AFiltros.ContainsKey('data_inicio') and (AFiltros['data_inicio'] <> 0) then
Query.AND_WHERE(TCondition.Field('data_cadastro').GreaterOrEqual(AFiltros['data_inicio']));
if AFiltros.ContainsKey('data_fim') and (AFiltros['data_fim'] <> 0) then
Query.AND_WHERE(TCondition.Field('data_cadastro').LessOrEqual(AFiltros['data_fim']));
if AFiltros.ContainsKey('apenas_ativos') and AFiltros['apenas_ativos'] then
Query.AND_WHERE(TCondition.Field('status').Equal('A'));
// Ordenação
if AFiltros.ContainsKey('ordenacao') then
Query.ORDER_BY(AFiltros['ordenacao']);
else
Query.ORDER_BY('nome');
// Paginação
if AFiltros.ContainsKey('pagina') and AFiltros.ContainsKey('registros_por_pagina') then
Query.PAGINATE(AFiltros['pagina'], AFiltros['registros_por_pagina']);
SQL := Query.AsString;
end;