Maneira mais limpa de construir uma string SQL em Java

107

Eu quero construir uma string SQL para fazer manipulação de banco de dados (atualizações, exclusões, inserções, seleções, esse tipo de coisa) - em vez do método concat de string horrível usando milhões de "+" e aspas que é ilegível na melhor das hipóteses - lá deve ser uma maneira melhor.

Eu pensei em usar MessageFormat - mas é suposto ser usado para mensagens do usuário, embora eu ache que faria um trabalho razoável - mas acho que deve haver algo mais alinhado às operações do tipo SQL nas bibliotecas java sql.

Groovy seria bom?

Vidar
fonte

Respostas:

76

Em primeiro lugar, considere o uso de parâmetros de consulta em instruções preparadas:

PreparedStatement stm = c.prepareStatement("UPDATE user_table SET name=? WHERE id=?");
stm.setString(1, "the name");
stm.setInt(2, 345);
stm.executeUpdate();

A outra coisa que pode ser feita é manter todas as consultas no arquivo de propriedades. Por exemplo, em um arquivo queries.properties pode colocar a consulta acima:

update_query=UPDATE user_table SET name=? WHERE id=?

Então, com a ajuda de uma classe de utilitário simples:

public class Queries {

    private static final String propFileName = "queries.properties";
    private static Properties props;

    public static Properties getQueries() throws SQLException {
        InputStream is = 
            Queries.class.getResourceAsStream("/" + propFileName);
        if (is == null){
            throw new SQLException("Unable to load property file: " + propFileName);
        }
        //singleton
        if(props == null){
            props = new Properties();
            try {
                props.load(is);
            } catch (IOException e) {
                throw new SQLException("Unable to load property file: " + propFileName + "\n" + e.getMessage());
            }           
        }
        return props;
    }

    public static String getQuery(String query) throws SQLException{
        return getQueries().getProperty(query);
    }

}

você pode usar suas consultas da seguinte maneira:

PreparedStatement stm = c.prepareStatement(Queries.getQuery("update_query"));

Esta é uma solução bastante simples, mas funciona bem.

Piotr Kochański
fonte
1
Eu prefiro usar um construtor de SQL limpo como este: mentabean.soliveirajr.com
TraderJoeChicago
2
Sugiro que você coloque o InputStreaminterior da if (props == null)instrução para não instanciá-la quando não for necessária.
SyntaxRules de
64

Para SQL arbitrário, use jOOQ . jOOQ atualmente suporta SELECT, INSERT, UPDATE, DELETE, TRUNCATE, e MERGE. Você pode criar SQL assim:

String sql1 = DSL.using(SQLDialect.MYSQL)  
                 .select(A, B, C)
                 .from(MY_TABLE)
                 .where(A.equal(5))
                 .and(B.greaterThan(8))
                 .getSQL();

String sql2 = DSL.using(SQLDialect.MYSQL)  
                 .insertInto(MY_TABLE)
                 .values(A, 1)
                 .values(B, 2)
                 .getSQL();

String sql3 = DSL.using(SQLDialect.MYSQL)  
                 .update(MY_TABLE)
                 .set(A, 1)
                 .set(B, 2)
                 .where(C.greaterThan(5))
                 .getSQL();

Em vez de obter a string SQL, você também pode simplesmente executá-la, usando jOOQ. Vejo

http://www.jooq.org

(Isenção de responsabilidade: eu trabalho para a empresa por trás do jOOQ)

Lukas Eder
fonte
Em muitos casos, isso não seria uma solução ruim, já que você não pode deixar o dbms analisar a instrução de antemão com valores diferentes para "5", "8", etc.? Eu acho que executar com jooq resolveria isso?
Vegard
@Vegard: Você tem controle total sobre como o jOOQ deve renderizar os valores de bind em sua saída SQL: jooq.org/doc/3.1/manual/sql-building/bind-values . Em outras palavras, você pode escolher se deseja renderizar "?"ou vincular os valores em linha.
Lukas Eder
Sim, mas em relação às formas limpas de construir sql, este seria um código um pouco confuso aos meus olhos se você não estiver usando JOOQ para executar. neste exemplo, você configurou A para 1, B para 2 etc, mas terá que fazer isso mais uma vez ao executar, se não estiver executando com JOOQ.
Vegard
1
@Vegard: Nada impede você de passar uma variável para a API jOOQ e reconstruir a instrução SQL. Além disso, você pode extrair valores de bind em sua ordem usando jooq.org/javadoc/latest/org/jooq/Query.html#getBindValues ​​() ou valores de bind nomeados por seus nomes usando jooq.org/javadoc/latest/org/jooq /Query.html#getParams () . Minha resposta contém apenas um exemplo muito simplista ... Não tenho certeza se isso responde às suas preocupações, entretanto?
Lukas Eder de
2
É uma solução cara.
Classificador de
15

Uma tecnologia que você deve considerar é o SQLJ - uma maneira de incorporar instruções SQL diretamente no Java. Como um exemplo simples, você pode ter o seguinte em um arquivo chamado TestQueries.sqlj:

public class TestQueries
{
    public String getUsername(int id)
    {
        String username;
        #sql
        {
            select username into :username
            from users
            where pkey = :id
        };
        return username;
    }
}

Há uma etapa adicional de pré-compilação que pega seus arquivos .sqlj e os converte em Java puro - em suma, procura os blocos especiais delimitados com

#sql
{
    ...
}

e os transforma em chamadas JDBC. Existem vários benefícios importantes em usar SQLJ:

  • abstrai completamente a camada JDBC - os programadores só precisam pensar em Java e SQL
  • o tradutor pode ser feito para verificar suas consultas de sintaxe etc. no banco de dados em tempo de compilação
  • capacidade de vincular diretamente variáveis ​​Java em consultas usando o prefixo ":"

Existem implementações do tradutor para a maioria dos principais fornecedores de banco de dados, então você deve ser capaz de encontrar tudo o que precisa facilmente.

Ashley Mercer
fonte
Este está desatualizado agora, de acordo com a wikipedia.
Zeus
1
No momento da redação (janeiro de 2016), SQLJ é referido na Wikipedia como "desatualizado" sem quaisquer referências. Foi oficialmente abandonado? Nesse caso, colocarei um aviso no início desta resposta.
Ashley Mercer
NB: a tecnologia ainda é suportada, por exemplo, na versão mais recente do Oracle, 12c . Admito que não é o padrão mais moderno, mas ainda funciona e tem alguns benefícios (como verificação em tempo de compilação de consultas no banco de dados) que não estão disponíveis em outros sistemas.
Ashley Mercer
12

Estou me perguntando se você está atrás de algo como o Squiggle . Também algo muito útil é o jDBI . Não vai te ajudar com as dúvidas.

tcurdt
fonte
9

Gostaria de dar uma olhada no Spring JDBC . Eu o uso sempre que preciso executar SQLs de maneira programática. Exemplo:

int countOfActorsNamedJoe
    = jdbcTemplate.queryForInt("select count(0) from t_actors where first_name = ?", new Object[]{"Joe"});

É realmente ótimo para qualquer tipo de execução de sql, especialmente consultas; ele o ajudará a mapear conjuntos de resultados para objetos, sem adicionar a complexidade de um ORM completo.

Bent André Solheim
fonte
como posso obter uma consulta sql real executada? Eu quero registrá-lo.
kodmanyagha
5

Costumo usar os Parâmetros JDBC Nomeados do Spring para que eu possa escrever uma string padrão como "select * from blah where colX = ': someValue'"; Acho que é bem legível.

Uma alternativa seria fornecer a string em um arquivo .sql separado e ler o conteúdo usando um método utilitário.

Ah, também vale a pena dar uma olhada no Squill: https://squill.dev.java.net/docs/tutorial.html

GaryF
fonte
Presumo que você queira dizer que está usando BeanPropertySqlParameterSource? Quase concordo com você, a classe que acabei de mencionar é legal quando se usa estritamente beans, mas, caso contrário, recomendo usar ParameterizedRowMapper customizado para construir objetos.
Esko
Não exatamente. Você pode usar qualquer SqlParameterSource com Parâmetros JDBC Nomeados. É adequado para minhas necessidades usar um MapSqlParameterSource, em vez da variedade de feijão. De qualquer forma, é uma boa solução. Os RowMappers, entretanto, lidam com o outro lado do quebra-cabeça SQL: transformar conjuntos de resultados em objetos.
GaryF
4

Eu apoio as recomendações para usar um ORM como o Hibernate. No entanto, certamente existem situações em que isso não funciona, então aproveitarei esta oportunidade para apresentar algumas coisas que ajudei a escrever: SqlBuilder é uma biblioteca java para construir dinamicamente instruções sql usando o estilo "builder". é bastante poderoso e flexível.

James
fonte
4

Tenho trabalhado em um aplicativo de servlet Java que precisa construir instruções SQL muito dinâmicas para fins de relatório ad hoc. A função básica do aplicativo é alimentar vários parâmetros de solicitação HTTP nomeados em uma consulta pré-codificada e gerar uma tabela de saída bem formatada. Usei Spring MVC e a estrutura de injeção de dependência para armazenar todas as minhas consultas SQL em arquivos XML e carregá-los no aplicativo de relatório, junto com as informações de formatação da tabela. Eventualmente, os requisitos de relatório tornaram-se mais complicados do que os recursos das estruturas de mapeamento de parâmetros existentes e tive que escrever o meu próprio. Foi um exercício interessante de desenvolvimento e produziu uma estrutura para mapeamento de parâmetros muito mais robusta do que qualquer outra coisa que pude encontrar.

Os novos mapeamentos de parâmetros eram assim:

select app.name as "App", 
       ${optional(" app.owner as "Owner", "):showOwner}
       sv.name as "Server", sum(act.trans_ct) as "Trans"
  from activity_records act, servers sv, applications app
 where act.server_id = sv.id
   and act.app_id = app.id
   and sv.id = ${integer(0,50):serverId}
   and app.id in ${integerList(50):appId}
 group by app.name, ${optional(" app.owner, "):showOwner} sv.name
 order by app.name, sv.name

A beleza da estrutura resultante era que ela podia processar parâmetros de solicitação HTTP diretamente na consulta com verificação de tipo e verificação de limite adequadas. Nenhum mapeamento extra necessário para validação de entrada. Na consulta de exemplo acima, o parâmetro denominado serverId seria verificado para garantir que pudesse ser convertido em um número inteiro e estivesse no intervalo de 0-50. O parâmetro appId seria processado como uma matriz de inteiros, com um limite de comprimento de 50. Se o campo showOwnerestiver presente e definido como "verdadeiro", os bits de SQL nas aspas serão adicionados à consulta gerada para os mapeamentos de campo opcionais. field Vários outros mapeamentos de tipo de parâmetro estão disponíveis, incluindo segmentos opcionais de SQL com outros mapeamentos de parâmetro. Ele permite um mapeamento de consulta tão complexo quanto o desenvolvedor pode criar. Ele ainda possui controles na configuração do relatório para determinar se uma determinada consulta terá os mapeamentos finais por meio de um PreparedStatement ou simplesmente será executada como uma consulta pré-construída.

Para os valores de solicitação Http de amostra:

showOwner: true
serverId: 20
appId: 1,2,3,5,7,11,13

Isso produziria o seguinte SQL:

select app.name as "App", 
       app.owner as "Owner", 
       sv.name as "Server", sum(act.trans_ct) as "Trans"
  from activity_records act, servers sv, applications app
 where act.server_id = sv.id
   and act.app_id = app.id
   and sv.id = 20
   and app.id in (1,2,3,5,7,11,13)
 group by app.name,  app.owner,  sv.name
 order by app.name, sv.name

Eu realmente acho que Spring ou Hibernate ou um desses frameworks deveriam oferecer um mecanismo de mapeamento mais robusto que verifica tipos, permite tipos de dados complexos como arrays e outros recursos. Eu escrevi meu motor apenas para meus propósitos, ele não foi totalmente lido para lançamento geral. Ele só funciona com consultas Oracle no momento e todo o código pertence a uma grande corporação. Algum dia, posso pegar minhas ideias e construir uma nova estrutura de código aberto, mas espero que um dos grandes jogadores existentes aceite o desafio.

Natália
fonte
3

Por que você deseja gerar todo o sql manualmente? Você já olhou para um ORM como o Hibernate Dependendo do seu projeto, ele provavelmente fará pelo menos 95% do que você precisa, de uma forma mais limpa do que o SQL bruto, e se você precisar obter o último pedaço de desempenho, pode criar o Consultas SQL que precisam ser ajustadas manualmente.

Jared
fonte
3

Você também pode dar uma olhada em MyBatis ( www.mybatis.org ). Ele ajuda a escrever instruções SQL fora do seu código java e mapeia os resultados do sql em seus objetos java, entre outras coisas.

Joshua
fonte
3

O Google fornece uma biblioteca chamada Room Persitence Library, que fornece uma maneira muito limpa de escrever SQL para aplicativos Android , basicamente uma camada de abstração sobre o banco de dados SQLite subjacente . Abaixo está um pequeno snippet de código do site oficial:

@Dao
public interface UserDao {
    @Query("SELECT * FROM user")
    List<User> getAll();

    @Query("SELECT * FROM user WHERE uid IN (:userIds)")
    List<User> loadAllByIds(int[] userIds);

    @Query("SELECT * FROM user WHERE first_name LIKE :first AND "
           + "last_name LIKE :last LIMIT 1")
    User findByName(String first, String last);

    @Insert
    void insertAll(User... users);

    @Delete
    void delete(User user);
}

Existem mais exemplos e melhores documentações nos documentos oficiais da biblioteca.

Também existe um chamado MentaBean que é um Java ORM . Ele tem recursos interessantes e parece ser uma maneira bastante simples de escrever SQL.

CasualCoder3
fonte
De acordo com a documentação Quarto : Room provides an abstraction layer over SQLite to allow fluent database access while harnessing the full power of SQLite. Portanto, não é uma biblioteca ORM genérica para RDBMS. Destina-se principalmente a aplicativos Android.
RafiAlhamd
2

Leia um arquivo XML.

Você pode lê-lo em um arquivo XML. É fácil de manter e trabalhar. Existem analisadores STaX, DOM, SAX padrão disponíveis para criar poucas linhas de código em java.

Faça mais com atributos

Você pode ter algumas informações semânticas com atributos na tag para ajudar a fazer mais com o SQL. Pode ser o nome do método ou tipo de consulta ou qualquer coisa que ajude você a codificar menos.

Manter

Você pode colocar o xml fora do frasco e mantê-lo facilmente. Mesmos benefícios de um arquivo de propriedades.

Conversão

XML é extensível e facilmente conversível para outros formatos.

Caso de uso

Metamug usa xml para configurar arquivos de recursos REST com sql.

Classificador
fonte
Você pode usar yaml ou json se quiser. Eles são melhores do que armazenar em um arquivo de propriedades simples
Classificador de
A questão é como CONSTRUIR SQL. Para construir SQL, se você precisar usar XML, Parser, Validation, etc., é uma sobrecarga. Muitas das primeiras tentativas que envolviam XML para construir SQL estão sendo rejeitadas em favor da Anotação. A resposta aceita por Piotr Kochański é simples e elegante e direta - resolve o problema e pode ser mantida. NOTA: NÃO existe uma maneira alternativa de manter um SQL melhor em um idioma diferente.
RafiAlhamd
Excluí meu comentário anterior I don't see a reason to make use of XML. , pois não consegui editá-lo.
RafiAlhamd
1

Se você colocar as sequências SQL em um arquivo de propriedades e, em seguida, ler isso, poderá manter as sequências SQL em um arquivo de texto simples.

Isso não resolve os problemas de tipo SQL, mas pelo menos torna muito mais fácil copiar e colar do TOAD ou sqlplus.

Rowan
fonte
0

Como você obtém a concatenação de strings, além de strings SQL longas em PreparedStatements (que você poderia fornecer facilmente em um arquivo de texto e carregar como um recurso de qualquer maneira) que você quebra em várias linhas?

Você não está criando strings SQL diretamente, está? Essa é a maior proibição da programação. Use PreparedStatements e forneça os dados como parâmetros. Isso reduz muito a chance de injeção de SQL.

JeeBee
fonte
Mas se você não está expondo uma página da web ao público - o SQL Injection é um problema relevante?
Vidar
4
A injeção de SQL é sempre relevante, porque pode acontecer acidentalmente ou intencionalmente.
sleske,
1
@Vidar - você pode não estar expondo a página da web ao público agora , mas mesmo o código que "sempre" será interno geralmente acaba recebendo algum tipo de exposição externa em algum ponto mais adiante. E é mais rápido e seguro fazer isso certo na primeira tentativa do que auditar todo o código-base para problemas depois ...
Andrzej Doyle
4
Até mesmo um PreparedStatement precisa ser criado a partir de uma String, não?
Stewart
Sim, mas é seguro construir um PreparedStatement a partir de uma String, contanto que você construa um PreparedStatement seguro. Você provavelmente deve escrever uma classe PreparedStatementBuilder para gerá-los, para esconder a confusão de concatenar coisas.
JeeBee de