La forma más limpia de construir una cadena SQL en Java

107

Quiero construir una cadena SQL para realizar la manipulación de la base de datos (actualizaciones, eliminaciones, inserciones, selecciones, ese tipo de cosas), en lugar del horrible método de cadena concat que usa millones de "+" y comillas, que es ilegible en el mejor de los casos. debe ser una forma mejor.

Pensé en usar MessageFormat, pero se supone que debe usarse para mensajes de usuario, aunque creo que haría un trabajo razonable, pero supongo que debería haber algo más alineado con las operaciones de tipo SQL en las bibliotecas SQL de Java.

¿Groovy sería bueno?

Vidar
fuente

Respuestas:

76

En primer lugar, considere usar parámetros de consulta en declaraciones preparadas:

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

La otra cosa que se puede hacer es mantener todas las consultas en el archivo de propiedades. Por ejemplo, en un archivo queries.properties puede colocar la consulta anterior:

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

Luego, con la ayuda de una clase de utilidad simple:

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);
    }

}

puede utilizar sus consultas de la siguiente manera:

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

Esta es una solución bastante simple, pero funciona bien.

Piotr Kochański
fuente
1
Prefiero usar un constructor SQL limpio como este: mentabean.soliveirajr.com
TraderJoeChicago
2
Puedo sugerirle que ponga el InputStreaminterior de la if (props == null)declaración para que no cree una instancia cuando no sea necesaria.
SyntaxRules
64

Para SQL arbitrario, use jOOQ . jOOQ actualmente soporta SELECT, INSERT, UPDATE, DELETE, TRUNCATE, y MERGE. Puedes crear SQL como este:

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();

En lugar de obtener la cadena SQL, también puede simplemente ejecutarla, usando jOOQ. Ver

http://www.jooq.org

(Descargo de responsabilidad: trabajo para la empresa detrás de jOOQ)

Lukas Eder
fuente
¿No sería esto en muchos casos una mala solución ya que no puede permitir que el dbms analice la declaración de antemano con diferentes valores para "5", "8", etc.? ¿Supongo que ejecutar con jooq lo resolvería?
Vegard
@Vegard: Tienes control total sobre cómo jOOQ debe representar los valores de enlace en su salida SQL: jooq.org/doc/3.1/manual/sql-building/bind-values . En otras palabras, puedes elegir si renderizar "?"o enlazar valores en línea.
Lukas Eder
sí, pero con respecto a las formas limpias de construir sql, este sería un código un poco desordenado en mi opinión si no está usando JOOQ para ejecutar. en este ejemplo, establece A en 1, B en 2, etc., pero debe hacerlo una vez más cuando ejecute si no está ejecutando con JOOQ.
Vegard
1
@Vegard: Nada le impide pasar una variable a la API jOOQ y reconstruir la declaración SQL. Además, puede extraer los valores de enlace en su orden utilizando jooq.org/javadoc/latest/org/jooq/Query.html#getBindValues ​​() , o los valores de enlace nombrados por sus nombres utilizando jooq.org/javadoc/latest/org/jooq /Query.html#getParams () . Mi respuesta solo contiene un ejemplo muy simplista ... aunque no estoy seguro si esto responde a sus preocupaciones.
Lukas Eder
2
Es una solución costosa.
Clasificador
15

Una tecnología que debe considerar es SQLJ , una forma de incrustar declaraciones SQL directamente en Java. Como ejemplo simple, puede tener lo siguiente en un archivo llamado TestQueries.sqlj:

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

Hay un paso adicional de precompilación que toma sus archivos .sqlj y los traduce a Java puro; en resumen, busca los bloques especiales delimitados con

#sql
{
    ...
}

y los convierte en llamadas JDBC. Existen varios beneficios clave de usar SQLJ:

  • abstrae completamente la capa JDBC: los programadores solo necesitan pensar en Java y SQL
  • se puede hacer que el traductor verifique sus consultas de sintaxis, etc.con la base de datos en el momento de la compilación
  • capacidad de vincular directamente variables de Java en consultas utilizando el prefijo ":"

Existen implementaciones del traductor para la mayoría de los principales proveedores de bases de datos, por lo que debería poder encontrar todo lo que necesita fácilmente.

Ashley Mercer
fuente
Este está desactualizado ahora, según wikipedia.
Zeus
1
En el momento de escribir este artículo (enero de 2016), en Wikipedia se hace referencia a SQLJ como "desactualizado" sin ninguna referencia. ¿Ha sido abandonado oficialmente? Si es así, colocaré una advertencia en la parte superior de esta respuesta.
Ashley Mercer
NB La tecnología todavía es compatible, por ejemplo, en la última versión de Oracle, 12c . Admito que no es el estándar más moderno, pero aún funciona y tiene algunos beneficios (como la verificación en tiempo de compilación de consultas en la base de datos) que no están disponibles en otros sistemas.
Ashley Mercer
12

Me pregunto si buscas algo como Squiggle . También algo muy útil es jDBI . Sin embargo, no te ayudará con las consultas.

tcurdt
fuente
9

Echaría un vistazo a Spring JDBC . Lo uso siempre que necesito ejecutar SQL mediante programación. Ejemplo:

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

Es realmente genial para cualquier tipo de ejecución SQL, especialmente para consultas; le ayudará a asignar conjuntos de resultados a objetos, sin agregar la complejidad de un ORM completo.

Bent André Solheim
fuente
¿Cómo puedo obtener una consulta SQL ejecutada real? Quiero registrarlo.
kodmanyagha
5

Tiendo a usar los parámetros JDBC con nombre de Spring para poder escribir una cadena estándar como "seleccionar * de bla donde colX = ': someValue'"; Creo que es bastante legible.

Una alternativa sería proporcionar la cadena en un archivo .sql separado y leer el contenido usando un método de utilidad.

Oh, también vale la pena echarle un vistazo a Squill: https://squill.dev.java.net/docs/tutorial.html

GaryF
fuente
Supongo que quiere decir que está usando BeanPropertySqlParameterSource. Casi estoy de acuerdo contigo, la clase que acabo de mencionar es genial cuando se usan estrictamente beans, pero de lo contrario, recomendaría usar ParameterizedRowMapper personalizado para construir objetos.
Esko
No exactamente. Puede utilizar cualquier SqlParameterSource con parámetros JDBC con nombre. Se adaptaba a mis necesidades para usar MapSqlParameterSource, en lugar de la variedad de frijoles. De cualquier manera, es una buena solución. Los RowMappers, sin embargo, se ocupan del otro lado del rompecabezas SQL: convertir conjuntos de resultados en objetos.
GaryF
4

Secundo las recomendaciones para usar un ORM como Hibernate. Sin embargo, ciertamente hay situaciones en las que eso no funciona, así que aprovecharé esta oportunidad para promocionar algunas cosas que he ayudado a escribir: SqlBuilder es una biblioteca java para construir dinámicamente sentencias sql usando el estilo "constructor". es bastante potente y bastante flexible.

James
fuente
4

He estado trabajando en una aplicación de servlet Java que necesita construir declaraciones SQL muy dinámicas para propósitos de informes ad hoc. La función básica de la aplicación es alimentar un montón de parámetros de solicitud HTTP con nombre en una consulta precodificada y generar una tabla de salida con un formato agradable. Utilicé Spring MVC y el marco de inyección de dependencia para almacenar todas mis consultas SQL en archivos XML y cargarlas en la aplicación de informes, junto con la información de formato de la tabla. Con el tiempo, los requisitos de informes se volvieron más complicados que las capacidades de los marcos de mapeo de parámetros existentes y tuve que escribir el mío. Fue un ejercicio interesante de desarrollo y produjo un marco para el mapeo de parámetros mucho más robusto que cualquier otra cosa que pude encontrar.

Las nuevas asignaciones de parámetros se veían así:

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

La belleza del marco resultante era que podía procesar los parámetros de solicitud HTTP directamente en la consulta con la verificación de tipo y límite adecuada. No se requieren asignaciones adicionales para la validación de entrada. En la consulta de ejemplo anterior, se verificaría el parámetro llamado serverId para asegurarse de que pudiera convertirse en un número entero y estuviera en el rango de 0-50. El parámetro appId se procesará como una matriz de números enteros, con un límite de longitud de 50. Si el campo showOwnerestá presente y establecido en "verdadero", los bits de SQL entre comillas se agregarán a la consulta generada para las asignaciones de campos opcionales. field Hay varias asignaciones de tipos de parámetros más disponibles, incluidos segmentos opcionales de SQL con asignaciones de parámetros adicionales. Permite una asignación de consultas tan compleja como se le ocurra al desarrollador. Incluso tiene controles en la configuración del informe para determinar si una consulta determinada tendrá las asignaciones finales a través de un PreparedStatement o simplemente se ejecutará como una consulta prediseñada.

Para los valores de solicitud Http de muestra:

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

Produciría el siguiente 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

Realmente creo que Spring o Hibernate o uno de esos marcos deberían ofrecer un mecanismo de mapeo más robusto que verifique tipos, permita tipos de datos complejos como matrices y otras características similares. Escribí mi motor solo para mis propósitos, no está del todo leído para el lanzamiento general. Solo funciona con consultas de Oracle en este momento y todo el código pertenece a una gran corporación. Algún día puedo tomar mis ideas y construir un nuevo marco de código abierto, pero espero que uno de los grandes jugadores existentes acepte el desafío.

Natalia
fuente
3

¿Por qué quieres generar todo el sql a mano? ¿Ha mirado un ORM como Hibernate? Dependiendo de su proyecto, probablemente hará al menos el 95% de lo que necesita, hágalo de una manera más limpia que SQL sin formato, y si necesita obtener el último rendimiento, puede crear el Consultas SQL que deben ajustarse a mano.

Jared
fuente
3

También puede echar un vistazo a MyBatis ( www.mybatis.org ). Le ayuda a escribir sentencias SQL fuera de su código java y mapea los resultados de SQL en sus objetos java, entre otras cosas.

Joshua
fuente
3

Google proporciona una biblioteca llamada Room Persitence Library que proporciona una forma muy limpia de escribir SQL para aplicaciones de Android , básicamente una capa de abstracción sobre la base de datos SQLite subyacente . A continuación se muestra un fragmento de código corto del sitio web 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);
}

Hay más ejemplos y mejor documentación en los documentos oficiales de la biblioteca.

También hay uno llamado MentaBean que es un ORM de Java . Tiene buenas características y parece ser una forma bastante simple de escribir SQL.

CasualCoder3
fuente
De acuerdo con la documentación de habitaciones : Room provides an abstraction layer over SQLite to allow fluent database access while harnessing the full power of SQLite. Entonces, no es una biblioteca ORM genérica para RDBMS. Está destinado principalmente a aplicaciones de Android.
RafiAlhamd
2

Leer un archivo XML.

Puede leerlo desde un archivo XML. Es fácil de mantener y trabajar. Hay analizadores STaX, DOM, SAX estándar disponibles para hacer pocas líneas de código en java.

Haz más con los atributos

Puede tener información semántica con atributos en la etiqueta para ayudar a hacer más con SQL. Este puede ser el nombre del método o el tipo de consulta o cualquier cosa que le ayude a codificar menos.

Mantener

Puede colocar el xml fuera del frasco y mantenerlo fácilmente. Los mismos beneficios que un archivo de propiedades.

Conversión

XML es extensible y fácilmente convertible a otros formatos.

Caso de uso

Metamug usa xml para configurar archivos de recursos REST con sql.

Clasificador
fuente
Puedes usar yaml o json si te gustan. Son mejores que almacenarlos en un archivo de propiedades simple
Sorter
La pregunta es cómo CONSTRUIR SQL. Para construir SQL, si necesita usar XML, Analizador, Validación, etc., es sobrecargado. La mayoría de los primeros intentos que involucraron XML para construir SQL están siendo rechazados en favor de Anotación. La respuesta aceptada por Piotr Kochański es simple y elegante y va al grano: resuelve el problema y se puede mantener. NOTA: NO existe una forma alternativa de mantener un mejor SQL en un idioma diferente.
RafiAlhamd
Eliminé mi comentario anterior I don't see a reason to make use of XML. porque no pude editarlo.
RafiAlhamd
1

Si coloca las cadenas SQL en un archivo de propiedades y luego las lee, puede mantener las cadenas SQL en un archivo de texto sin formato.

Eso no resuelve los problemas de tipo SQL, pero al menos hace que copiar y pegar desde TOAD o sqlplus sea mucho más fácil.

Serbal
fuente
0

¿Cómo se obtiene la concatenación de cadenas, además de las cadenas SQL largas en PreparedStatements (que podría proporcionar fácilmente en un archivo de texto y cargar como un recurso de todos modos) que divide en varias líneas?

¿No está creando cadenas SQL directamente, verdad? Ese es el mayor no-no en programación. Utilice PreparedStatements y proporcione los datos como parámetros. Reduce enormemente la posibilidad de inyección SQL.

JeeBee
fuente
Pero si no está exponiendo una página web al público, ¿es la inyección SQL un tema relevante?
Vidar
4
La inyección de SQL siempre es relevante, porque puede ocurrir tanto accidentalmente como intencionalmente.
sleske
1
@Vidar - no podría estar exponiendo a la página web para el público ahora , pero el código aún que "siempre" ser internos a menudo termina recibiendo algún tipo de exposición externa algún punto más abajo de la línea. Y es más rápido y más seguro hacerlo bien la primera vez que tener que auditar todo el código base para detectar problemas más adelante ...
Andrzej Doyle
4
Incluso un PreparedStatement debe crearse a partir de una cadena, ¿no?
Stewart
Sí, pero es seguro crear un PreparedStatement a partir de una cadena, siempre que cree un PreparedStatement seguro. Probablemente debería escribir una clase PreparedStatementBuilder para generarlos, para ocultar el lío de concatenar cosas.
JeeBee