Domanda

Il nostro progetto attuale non utilizza Hibernate (per vari motivi) e stiamo usando il supporto SimpleJdbc di Spring per eseguire tutte le nostre operazioni DB. Abbiamo una classe di utilità che riassume tutte le operazioni CRUD ma le operazioni complesse vengono eseguite utilizzando query SQL personalizzate.

Attualmente le nostre query sono archiviate come costanti di stringa all'interno delle classi di servizio stesse e vengono inviate a un'utilità che deve essere eseguita da SimpleJdbcTemplate. Siamo in un vicolo cieco in cui la leggibilità deve essere bilanciata con la manutenibilità. Il codice SQL all'interno della classe stessa è più gestibile poiché risiede con il codice che lo utilizza. D'altra parte, se memorizziamo queste query in un file esterno (flat o XML), lo stesso SQL sarebbe più leggibile rispetto alla sintassi di stringa java con escape.

Qualcuno ha riscontrato un problema simile? Che cos'è un buon equilibrio? Dove conservi il tuo SQL personalizzato nel tuo progetto?

Una query di esempio è la seguente:

private static final String FIND_ALL_BY_CHEAPEST_AND_PRODUCT_IDS = 
"    FROM PRODUCT_SKU T \n" +
"    JOIN \n" +
"    ( \n" +
"        SELECT S.PRODUCT_ID, \n" +
"               MIN(S.ID) as minimum_id_for_price \n" +
"          FROM PRODUCT_SKU S \n" +
"         WHERE S.PRODUCT_ID IN (:productIds) \n" +
"      GROUP BY S.PRODUCT_ID, S.SALE_PRICE \n" +
"    ) FI ON (FI.PRODUCT_ID = T.PRODUCT_ID AND FI.minimum_id_for_price = T.ID) \n" +
"    JOIN \n" +
"    ( \n" +
"        SELECT S.PRODUCT_ID, \n" +
"               MIN(S.SALE_PRICE) as minimum_price_for_product \n" +
"          FROM PRODUCT_SKU S \n" +
"         WHERE S.PRODUCT_ID IN (:productIds) \n" +
"      GROUP BY S.PRODUCT_ID \n" +
"    ) FP ON (FP.PRODUCT_ID = T.PRODUCT_ID AND FP.minimum_price_for_product = T.sale_price) \n" +
"WHERE T.PRODUCT_ID IN (:productIds)";

Ecco come sarebbe in un file SQL piatto:

--namedQuery: FIND_ALL_BY_CHEAPEST_AND_PRODUCT_IDS
FROM PRODUCT_SKU T 
JOIN 
( 
    SELECT S.PRODUCT_ID, 
           MIN(S.ID) as minimum_id_for_price 
      FROM PRODUCT_SKU S 
     WHERE S.PRODUCT_ID IN (:productIds) 
  GROUP BY S.PRODUCT_ID, S.SALE_PRICE 
) FI ON (FI.PRODUCT_ID = T.PRODUCT_ID AND FI.minimum_id_for_price = T.ID) 
JOIN 
( 
    SELECT S.PRODUCT_ID, 
           MIN(S.SALE_PRICE) as minimum_price_for_product 
      FROM PRODUCT_SKU S 
     WHERE S.PRODUCT_ID IN (:productIds) 
  GROUP BY S.PRODUCT_ID 
) FP ON (FP.PRODUCT_ID = T.PRODUCT_ID AND FP.minimum_price_for_product = T.sale_price) 
WHERE T.PRODUCT_ID IN (:productIds)
È stato utile?

Soluzione

Ho archiviato SQL sia come stringhe all'interno di una classe Java sia come file separati che sono stati caricati in fase di esecuzione. Preferivo di gran lunga quest'ultima per due ragioni. Innanzitutto, il codice è più leggibile da un ampio margine. In secondo luogo, è più semplice testare l'SQL separatamente se lo si memorizza in un file separato. Inoltre, è stato più facile ottenere qualcuno migliore di me in SQL per aiutarmi con le mie query quando erano in file separati.

Altri suggerimenti

Mi sono imbattuto anche in questo, attualmente per lo stesso motivo: un progetto basato su Spring jdbc. La mia esperienza è che sebbene non sia grandioso avere la logica in sql stesso, non c'è davvero posto migliore per farlo, e inserire il codice dell'applicazione è più lento che farlo fare dal db e non è necessariamente più chiaro.

Le più grandi insidie ??che ho visto in questo è dove sql inizia a proliferare in tutto il progetto, con più varianti. " Ottieni A, B, C da FOO " ;. " Ottieni A, B, C, E da Foo " ;, ecc. ecc. Questo tipo di proliferazione è particolarmente probabile quando il progetto raggiunge una certa massa critica - potrebbe non sembrare un problema con 10 query, ma quando ci sono 500 query sparse nel progetto diventa molto più difficile capire se hai già fatto qualcosa . Estrarre le operazioni CRUD di base ti porta molto avanti al gioco qui.

La migliore soluzione, AFAIK, deve essere rigorosamente coerente con l'SQL codificato, commentato, testato e in un posto coerente. Il nostro progetto ha query sql senza commenti a 50 righe. Cosa vogliono dire? Chi lo sa?

Per quanto riguarda le query in file esterni, non vedo cosa comprano: tu fai comunque altrettanto affidamento su SQL e, ad eccezione del (discutibile) miglioramento estetico di mantenere sql fuori dalle classi, le tue classi fanno ancora affidamento su sql -.eg in genere separa le risorse per ottenere la flessibilità necessaria per collegare le risorse di sostituzione, ma non puoi collegare query di sostituzione sql in quanto ciò cambierebbe il significato semantico della classe o non funziona affatto. Quindi è un'illusoria pulizia del codice.

Una soluzione abbastanza radicale sarebbe quella di utilizzare Groovy per specificare le tue query. Groovy ha il supporto a livello di linguaggio per stringhe multilinea e interpolazione di stringhe (in modo divertente noto come GStrings).

Ad esempio usando Groovy, la query che hai specificato sopra sarebbe semplicemente:

class Queries
    private static final String PRODUCT_IDS_PARAM = ":productIds"

    public static final String FIND_ALL_BY_CHEAPEST_AND_PRODUCT_IDS = 
    """    FROM PRODUCT_SKU T 
        JOIN 
        ( 
            SELECT S.PRODUCT_ID, 
                   MIN(S.ID) as minimum_id_for_price 
              FROM PRODUCT_SKU S 
             WHERE S.PRODUCT_ID IN ($PRODUCT_IDS_PARAM) 
          GROUP BY S.PRODUCT_ID, S.SALE_PRICE 
        ) FI ON (FI.PRODUCT_ID = T.PRODUCT_ID AND FI.minimum_id_for_price = T.ID) 
        JOIN 
        ( 
            SELECT S.PRODUCT_ID, 
                   MIN(S.SALE_PRICE) as minimum_price_for_product 
              FROM PRODUCT_SKU S 
             WHERE S.PRODUCT_ID IN ($PRODUCT_IDS_PARAM) 
          GROUP BY S.PRODUCT_ID 
        ) FP ON (FP.PRODUCT_ID = T.PRODUCT_ID AND FP.minimum_price_for_product = T.sale_price) 
    WHERE T.PRODUCT_ID IN ($PRODUCT_IDS_PARAM) """

Puoi accedere a questa classe dal codice Java, proprio come faresti se fosse definita in Java, ad esempio

String query = QueryFactory.FIND_ALL_BY_CHEAPEST_AND_PRODUCT_IDS;

Devo ammettere che l'aggiunta di Groovy al tuo percorso di classe solo per rendere più piacevoli le tue query SQL è un po 'un "martello per rompere un dado" soluzione, ma se stai usando Spring, c'è già una buona probabilità che Groovy sia già sul tuo percorso di classe.

Inoltre, ci sono probabilmente molti altri posti nel tuo progetto in cui potresti usare Groovy (invece di Java) per migliorare il tuo codice (in particolare ora che Groovy è di proprietà di Spring). Gli esempi includono la scrittura di casi di prova o la sostituzione di bean Java con bean Groovy.

Utilizziamo procedure memorizzate. Questo è positivo per noi perché utilizziamo Oracle Fine Grain Access. Questo ci consente di impedire a un utente di vedere un particolare report o risultati di ricerca limitando il suo accesso alla procedura pertinente. Ci dà anche un po 'di miglioramento delle prestazioni.

Perché non utilizzare Stored procedure anziché query di codifica rigida? Stored Procs aumenterà la manutenibilità e fornirà maggiore sicurezza per cose come SQL Interjection Attacks.

Nella classe è probabilmente il migliore - Se le query sono abbastanza lunghe da rendere un problema l'escaping, è probabile che si desideri esaminare le procedure memorizzate o semplificare la query.

Un'opzione è utilizzare iBatis . È abbastanza leggero rispetto a un ORM completo come Hibernate, ma fornisce un mezzo per archiviare le tue query SQL al di fuori dei tuoi file .java

Conserviamo tutto il nostro SQL in una classe come un gruppo di stringhe finali statiche. Per leggibilità, lo distribuiamo su alcune righe concatenate utilizzando +. Inoltre, non sono sicuro che tu debba sfuggire a qualsiasi cosa - " Stringhe " sono racchiusi tra virgolette singole in sql.

Avevamo un progetto in cui usavamo l'approccio esatto che sei, tranne che abbiamo esternalizzato ogni query in un file di testo separato. Ogni file è stato letto (una volta) utilizzando il framework ResourceLoader di Spring e l'applicazione ha funzionato tramite un'interfaccia come questa:

public interface SqlResourceLoader {
    String loadSql(String resourcePath);
}

Un netto vantaggio di ciò era che avere l'SQL in un formato senza escape consentiva un debug più semplice: basta leggere il file in uno strumento di query. Una volta che hai più di una serie di domande di moderata complessità, hai a che fare con un / escape al / dal codice durante il test & amp; il debug (in particolare per l'ottimizzazione) è stato prezioso.

Abbiamo anche dovuto supportare un paio di database diversi, quindi ha permesso di scambiare la piattaforma più facilmente.

In base al problema come lo hai spiegato, qui non c'è altra scelta se non quella di tenerlo nel codice e sbarazzarsi dei caratteri / n ovunque. Questa è l'unica cosa che hai menzionato come influenza la leggibilità e sono assolutamente inutili.

A meno che tu non abbia altri problemi ad essere nel codice, il tuo problema può essere facilmente risolto.

Immagino che l'archiviazione di una query in un file esterno, quindi l'applicazione legga quando necessario presenta un enorme buco di sicurezza.

Cosa succede se una mente malvagia ha accesso a quel file e cambia la tua query?

Ad esempio modifiche

 select a from A_TABLE;

a

 drop table A_TABLE;

OPPURE

 update T_ACCOUNT set amount = 1000000

Inoltre, aggiunge la complessità di dover mantenere due cose: codice Java e file di query SQL.

MODIFICA: Sì, è possibile modificare le query senza ricompilare l'app. Non vedo il grosso problema lì. È possibile ricompilare le classi che contengono / creano solo sqlQueries, se il progetto è troppo grande. Inoltre, se la documentazione è scadente, potresti finire per modificare il file errato e questo si trasformerà in un enorme bug silenzioso. Non verrà generata alcuna eccezione o codice di errore e, quando ti rendi conto di ciò che hai fatto, potrebbe essere troppo tardi.

Un approccio diverso sarebbe quello di avere una sorta di SQLQueryFactory, ben documentato e con metodi che restituiscono una query SQL che si desidera utilizzare.

Ad esempio

public String findCheapest (String tableName){

      //returns query.
}

Ciò di cui hai bisogno qui è SQLJ che è un preprocessore Java SQL. Purtroppo, a quanto pare non è mai decollato, anche se ho visto alcuni IBM e Oracle . Ma sono abbastanza obsoleti.

Se fossi in te e avessi un sacco di domande sul sistema, le memorizzerei in un file separato e le caricerei in runtime .

Dalla mia esperienza è meglio lasciare le istruzioni SQL nel codice che non le separano rende le cose più gestibili (come annotazioni vs. file di configurazione), ma ora ho un dibattito con un membro del team al riguardo.

Ho un piccolo programma che accede agli Appunti e scappa / scappa da testo semplice con valori letterali stringa Java.

L'ho come scorciatoia sul " avvio veloce " barra degli strumenti, quindi l'unica cosa che devo fare è

Ctrl+C, Click jar, Ctrl+V

O quando voglio eseguire il mio " codice " nello strumento SQL o viceversa.

Quindi di solito ho qualcosa del genere:

String query = 
    "SELECT a.fieldOne, b.fieldTwo \n"+
    "FROM  TABLE_A a, TABLE b \n"+ 
    "... etc. etc. etc";


logger.info("Executing  " + query  );

PreparedStatement pstmt = connection.prepareStatement( query );
....etc.

Che si trasforma in:

    SELECT a.fieldOne, b.fieldTwo 
    FROM  TABLE_A a, TABLE b
    ... etc. etc. etc

O perché in alcuni progetti non riesco a creare file separati o perché sono paranoico e sento che alcuni bit verranno inseriti / eliminati durante la lettura dal file esterno (di solito un \ n invisibile che rende

select a,b,c 
from 

in

select a,b,cfrom 

L'idea di IntelliJ fa lo stesso per te automagicamente ma solo dal semplice al codice.

Ecco una vecchia versione che ho recuperato. È un po 'rotto e non gestisce?

Fammi sapere se qualcuno lo migliora.

import java.awt.Toolkit;
import java.awt.datatransfer.Clipboard;
import java.awt.datatransfer.DataFlavor;
import java.awt.datatransfer.ClipboardOwner;
import java.awt.datatransfer.Transferable;
import java.awt.datatransfer.StringSelection;
import java.awt.datatransfer.UnsupportedFlavorException;
import java.io.IOException;

/**
 * Transforms a plain string from the clipboard into a Java 
 * String literal and viceversa.
 * @author <a href="http://stackoverflow.com/users/20654/oscar-reyes">Oscar Reyes</a>
 */
public class ClipUtil{

    public static void main( String [] args ) 
                                throws UnsupportedFlavorException,
                                                     IOException {

        // Get clipboard
        Toolkit toolkit = Toolkit.getDefaultToolkit();
        Clipboard clipboard = toolkit.getSystemClipboard();

        // get current content.
        Transferable transferable = clipboard.getContents( new Object() ); 
        String s = ( String ) transferable.getTransferData( 
                                                DataFlavor.stringFlavor );

        // process the content
        String result = process( s );

        // set the result
        StringSelection ss = new StringSelection( result );
        clipboard.setContents( ss, ss );

    }
    /**
     * Transforms the given string into a Java string literal 
     * if it represents plain text and viceversa. 
     */
    private static String process( String  s ){
        if( s.matches( "(?s)^\\s*\\\".*\\\"\\s*;<*>quot; ) ) {
            return    s.replaceAll("\\\\n\\\"\\s*[+]\n\\s*\\\"","\n")
                       .replaceAll("^\\s*\\\"","")
                       .replaceAll("\\\"\\s*;<*>quot;,"");
        }else{
            return     s.replaceAll("\n","\\\\n\\\" +\n \\\" ")
                        .replaceAll("^"," \\\"")
                        .replaceAll("<*>quot;," \\\";");
        }
    }
}

Dato che usi già Spring, perché non inserire l'SQL nel file di configurazione Spring e DI nella classe DAO? Questo è un modo semplice per esternalizzare la stringa SQL.

HTH Tom

Preferisco l'opzione esterna. Supporto più progetti e trovo molto più difficile supportare l'SQL interno perché è necessario compilare e ridistribuire ogni volta che si desidera apportare una leggera modifica all'SQL. Avere l'SQL in un file esterno consente di apportare modifiche grandi e piccole facilmente con meno rischi. Se stai solo modificando l'SQL, non c'è alcuna possibilità di inserire un refuso che rompe la classe.

Per Java, utilizzo la classe Properties che rappresenta l'SQL in un file .properties, che consente di passare le query SQL se si desidera riutilizzare le query anziché leggere il file più volte.

Autorizzato sotto: CC-BY-SA insieme a attribuzione
Non affiliato a StackOverflow
scroll top