Код SQL внутри классов Java

Наш текущий проект не использует Hibernate (по разным причинам), и мы используем поддержку Spring SimpleJdbc для выполнения всех наших операций с БД. У нас есть класс утилиты, который абстрагирует все операции CRUD, но сложные операции выполняются с использованием пользовательских SQL-запросов.

В настоящее время наши запросы хранятся как константы String внутри самих классов сервиса и передаются в утилиту, которая должна выполняться с помощью SimpleJdbcTemplate. Мы находимся в тупике, где читаемость должна быть сбалансирована с ремонтопригодностью. SQL-код внутри самого класса более удобен в обслуживании, поскольку он содержит код, который его использует. С другой стороны, если мы сохраним эти запросы во внешнем файле (плоском или XML), сам SQL будет более читабельным по сравнению с экранированным синтаксисом строки java.

Кто-нибудь сталкивался с подобной проблемой? Что такое хороший баланс? Где вы держите свой собственный SQL в своем проекте?

Пример запроса выглядит следующим образом:

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

Вот как это выглядело бы в плоском файле SQL:

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

Ответ 1

Я сохранил SQL как строки в классе Java и как отдельные файлы, которые были загружены во время выполнения. Я очень предпочел последнее по двум причинам. Во-первых, код читается с большим отрывом. Во-вторых, проще протестировать SQL в изоляции, если вы сохраните его в отдельном файле. В дополнение к этому, было легче получить кого-то лучше меня на SQL, чтобы помочь мне с моими запросами, когда они были в отдельных файлах.

Ответ 2

Я тоже столкнулся с этим, по той же причине - проект на основе spring jdbc. Мой опыт заключается в том, что, хотя и не очень важно иметь логику в самом sql, на самом деле нет лучшего места для этого, и вложение кода приложения происходит медленнее, чем использование db, и не обязательно более ясное.

Самые большие ошибки, которые я видел с этим, - это то, где sql начинает разрастаться по всему проекту с несколькими вариантами. "Получите A, B, C от FOO". "Получить A, B, C, E от Foo" и т.д. Такое распространение особенно важно, поскольку проект достигает определенной критической массы - может быть, это не проблема с 10 запросами, но когда 500 запросов разбросаны по всему проекту, становится намного сложнее понять, если вы уже что-то сделали, Абстрагирование основных операций CRUD ставит вас перед игрой здесь.

Лучшее решение, AFAIK, должно строго соответствовать кодированному SQL - прокомментированному, проверенному и последовательно. Наш проект имеет 50-строчные запросы без запроса. Что они имеют в виду? Кто знает?

Что касается запросов во внешних файлах, я не вижу, что это покупает. Вы все еще так же зависимы от SQL, и за исключением (сомнительного) эстетического улучшения хранения sql из классов, ваши классы по-прежнему так же зависят от sql -.eg, что вы обычно разделяете ресурсы, чтобы получить гибкость для подключаемых ресурсов замещения, но вы не могли подключать заменяющие sql-запросы, поскольку это изменило бы семантический смысл класса или не работает вообще. Так что это иллюзорная чистота кода.

Ответ 3

Довольно радикальным решением было бы использовать Groovy для указания ваших запросов. Groovy имеет поддержку уровня языка для многострочных строк и интерполяции строк (забавно известный как GStrings).

Например, используя Groovy, указанный выше запрос просто будет:

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) """

Вы можете получить доступ к этому классу из кода Java, как если бы он был определен на Java, например

String query = QueryFactory.FIND_ALL_BY_CHEAPEST_AND_PRODUCT_IDS;

Я соглашусь, что добавление Groovy к вашему пути к классам просто для того, чтобы ваши SQL-запросы выглядели лучше, это немного "решение кувалды, чтобы взломать орех", но если вы используете Spring, шанс, что вы уже Groovy на вашем пути к классам.

Кроме того, в вашем проекте может быть много других мест, где вы могли бы использовать Groovy (вместо Java) для улучшения вашего кода (особенно сейчас, когда Groovy принадлежит Spring). Примеры включают запись тестовых примеров или замену Java beans на Groovy beans.

Ответ 4

Мы используем хранимые процедуры. Это хорошо для нас, потому что мы используем Oracle Fine Grain Access. Это позволяет нам ограничить доступ пользователя к конкретному отчету или результатам поиска, ограничив их доступ к соответствующей процедуре. Это также дает нам немного повышения производительности.

Ответ 5

Я бы предположил, что сохранение запроса во внешнем файле, а затем приложение, прочитав его, когда это необходимо, представляет собой ОГРОМНОЕ отверстие безопасности.

Что произойдет, если злой вдохновитель получит доступ к этому файлу и изменит ваш запрос?

Например, изменения

 select a from A_TABLE;

К

 drop table A_TABLE;

ИЛИ

 update T_ACCOUNT set amount = 1000000

Кроме того, он добавляет сложности, связанных с двумя вещами: Java-кодом и SQL-запросами.

EDIT: Да, вы можете изменить свои запросы, не перекомпилируя приложение. Я не вижу там большого дела. Вы можете перекомпилировать классы, которые содержат/создают только sqlQueries, если проект слишком велик. Кроме того, если документация плохая, возможно, вы в конечном итоге измените неправильный файл, и это превратится в огромную тихую ошибку. Коды ошибок и ошибок не будут выбрасываться, и когда вы поймете, что сделали, может быть, слишком поздно.

Другим подходом было бы иметь какой-то SQLQueryFactory, хорошо документированный и с методами, которые возвращают SQL-запрос, который вы хотите использовать.

Например

public String findCheapest (String tableName){

      //returns query.
}

Ответ 6

Почему бы не использовать хранимые процедуры вместо жестких запросов? Хранимые Procs увеличит ремонтопригодность и обеспечат большую безопасность для таких вещей, как SQL Interjection Attacks.

Ответ 7

В классе, вероятно, лучше всего - если запросы достаточно длинные, что проблема экранирования является проблемой, вы, вероятно, хотите посмотреть на хранимые процедуры или упростить запрос.

Ответ 8

Один из вариантов заключается в использовании iBatis. Он довольно легкий по сравнению с полномасштабным ORM, подобным Hibernate, но предоставляет средства для хранения ваших SQL-запросов за пределами ваших .java файлов.

Ответ 9

Мы сохраняем весь наш SQL в классе как кучу статических окончательных строк. Для удобства чтения мы распределяем его по нескольким линиям, конкатенированным с использованием+. Кроме того, я не уверен, что вам нужно избегать любой вещи - "Строки" заключены в одинарные кавычки в sql.

Ответ 10

У нас был проект, в котором мы использовали точный подход, за исключением того, что мы внесли каждый запрос в отдельный текстовый файл. Каждый файл был прочитан (один раз) с использованием инфраструктуры Spring ResourceLoader, и приложение работало через интерфейс, подобный этому:

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

Явное преимущество этого заключалось в том, что наличие SQL в неэкранированном формате позволило упростить отладку - просто прочитайте файл в инструменте запросов. После того, как у вас есть несколько запросов с умеренной сложностью, работа с un/escaping в/из кода во время тестирования и отладки (особенно для настройки), это было бесценно.

Нам также пришлось поддерживать несколько разных баз данных, поэтому это позволило легко обменивать платформу.

Ответ 11

Основываясь на проблеме, как вы ее объяснили, здесь нет реального выбора, кроме как сохранить ее в коде и избавиться от символов /n. Это единственное, что вы упомянули как влияющее на читаемость, и они абсолютно не нужны.

Если у вас есть другие проблемы с тем, что он находится в коде, ваша проблема легко решена.

Ответ 12

Здесь вам нужно SQLJ, который является Java-препроцессором SQL. К сожалению, видимо, он никогда не снимался, хотя я видел некоторые IBM и Oracle. Но они довольно устарели.

Если бы я был вами и имел много и много запросов в системе, я бы сохранил их в отдельном файле и загружал их во время выполнения.

Ответ 13

Из моего опыта лучше оставить инструкции SQL в коде, не разделяя их, делает вещи более удобными (например, аннотации или файлы конфигурации), но теперь у меня есть дебаты с членом команды об этом.

Ответ 14

У меня есть небольшая программа для доступа к буферу и escape/unscape plain text с строковыми литералами Java.

У меня есть ярлык на панели инструментов быстрого запуска, поэтому мне нужно только

Ctrl+C, Click jar, Ctrl+V

Либо когда я хочу запустить свой "код" в инструменте SQL, либо наоборот.

Итак, у меня обычно есть что-то вроде этого:

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.

Которая преобразуется в:

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

Либо потому, что в некоторых проектах я не могу создавать отдельные файлы, или потому, что я параноик, и я чувствую, что некоторые биты будут вставлены/удалены при чтении из внешнего файла (обычно это невидимый \n, который делает

select a,b,c 
from 

в

select a,b,cfrom 

Идея IntelliJ делает то же самое для вас автоматически, но просто от простого к коду.

Вот старая версия, которую я восстановил. Его немного сломан и не справляется?.

Сообщите мне, если кто-то улучшит его.

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*;$" ) ) {
            return    s.replaceAll("\\\\n\\\"\\s*[+]\n\\s*\\\"","\n")
                       .replaceAll("^\\s*\\\"","")
                       .replaceAll("\\\"\\s*;$","");
        }else{
            return     s.replaceAll("\n","\\\\n\\\" +\n \\\" ")
                        .replaceAll("^"," \\\"")
                        .replaceAll("$"," \\\";");
        }
    }
}

Ответ 15

Поскольку вы уже используете Spring, почему бы не поместить SQL в конфигурационный файл Spring и DI его в класс DAO? Это простой способ для экстернализации строки SQL.

НТН Том

Ответ 16

Я предпочитаю внешний вариант. Я поддерживаю несколько проектов и затрудняю поддержку внутреннего SQL, потому что вы должны скомпилировать и повторно развернуть каждый раз, когда хотите внести небольшое изменение в SQL. Наличие SQL во внешнем файле позволяет сделать большие и небольшие изменения легко с меньшим риском. Если вы просто редактируете SQL, нет никакой возможности помещать опечатку, которая разбивает класс.

Для Java я использую класс Properties, который представляет SQL в файле .properties, который позволяет вам передавать SQL-запросы, если вы хотите повторно использовать запросы, а не читать файл за несколько раз.