В каких случаях использование нативного SQL (сырых запросов) предпочтительнее ORM?

Ответ

Использование нативного SQL оправдано, когда ORM (Hibernate, JPA) генерирует неэффективные запросы или не поддерживает специфичные функции СУБД.

Ключевые сценарии:

  1. Сложные аналитические запросы (OLAP):

    • Оконные функции (ROW_NUMBER(), LAG(), LEAD(), агрегаты с OVER).
    • Рекурсивные запросы (WITH RECURSIVE).
    • Группировки и агрегация с CUBE, ROLLUP, GROUPING SETS.
      -- Пример: ранжирование с помощью оконной функции
      SELECT
      user_id,
      order_date,
      amount,
      SUM(amount) OVER (PARTITION BY user_id ORDER BY order_date) AS running_total
      FROM orders
      WHERE order_date >= '2024-01-01';
  2. Массовые операции (Bulk Updates/Deletes):

    • ORM может загружать сущности в память, что неэффективно. Нативный SQL выполнит одну операцию на стороне БД.
      -- Эффективное обновление
      UPDATE orders SET status = 'ARCHIVED' WHERE created_at < '2023-01-01';
  3. Использование специфичных функций СУБД:

    • Геопространственные функции (PostGIS в PostgreSQL).
    • Полнотекстовый поиск (tsvector/tsquery в PostgreSQL, MATCH ... AGAINST в MySQL).
    • JSON/XML функции для нереляционных данных внутри БД.
  4. Оптимизация производительности критичных запросов:

    • Когда DBA или разработчик должен явно контролировать план выполнения, использовать хинты или определенные индексы.
  5. Создание и модификация схемы БД (DDL):

    • Создание сложных индексов (частичных, составных), триггеров, материализованных представлений.

Как использовать в Java (Spring JPA):

@Repository
public interface OrderRepository extends JpaRepository<Order, Long> {

    // Использование @Query с nativeQuery = true
    @Query(value = 
            "SELECT * FROM orders o " +
            "WHERE o.region_id = :regionId " +
            "AND earth_distance(ll_to_earth(o.lat, o.lng), ll_to_earth(:lat, :lng)) < :radius",
            nativeQuery = true)
    List<Order> findOrdersInRadius(@Param("regionId") Long regionId,
                                   @Param("lat") double latitude,
                                   @Param("lng") double longitude,
                                   @Param("radius") double radiusMeters);

    // Массовое обновление через @Modifying
    @Modifying
    @Query(value = "UPDATE orders SET priority = :newPriority WHERE status = 'PENDING'",
           nativeQuery = true)
    @Transactional
    int bulkUpdatePriority(@Param("newPriority") int priority);
}

Риски: Нативный SQL теряет переносимость между СУБД и может быть подвержен SQL-инъекциям, если не использовать параметризованные запросы.

Ответ 18+ 🔞

А, ну вот, опять про этот ваш нативный SQL! Слушай, а ты знаешь, что ORM — это как тёща: вроде удобно, но иногда такую хуйню сморозит, что волосы дыбом встают. Ну, типа, Hibernate там, JPA... Они же иногда запросы такие строят, что хочется глаза выколоть и бежать в монастырь.

Так вот, когда надо брать в руки этот самый нативный SQL и ебашить как мужик:

  1. Сложные штуки, где мозг ORM плавится. Ну, там, оконные функции эти, ROW_NUMBER(), LAG() — вся эта аналитическая магия. ORM на это смотрит как баран на новые ворота. Или рекурсивные запросы, WITH RECURSIVE — это вообще за гранью его понимания, там у него предохранители выбивает.

    -- Смотри, как красиво: бегущая сумма по пользователю. ORM будет полчаса кряхтеть, а тут — раз, и готово.
    SELECT
        user_id,
        order_date,
        amount,
        SUM(amount) OVER (PARTITION BY user_id ORDER BY order_date) AS running_total
    FROM orders
    WHERE order_date >= '2024-01-01';
  2. Когда надо обновить или удалить ОВЕРДОХУИЩА записей. А ORM что делает? Он их все в память грузит, сущности создаёт, потом по одной штучке обновляет и плачет. Это ж пиздец, а не подход! Нативный SQL приходит и делает одним махом: "Так, всё, что старше 2023 года — в архив!". И БД сама, быстро, без соплей.

    UPDATE orders SET status = 'ARCHIVED' WHERE created_at < '2023-01-01';
  3. Когда нужны фишки конкретной базы. Ну, типа, в PostgreSQL есть PostGIS для работы с координатами. Или полнотекстовый поиск. ORM про это нихуя не знает, он же для всех один. А тут ты можешь выжать из базы все соки, как лимон.

  4. Когда запрос тормозит, а ты его должен вручную отшлифовать. DBA посмотрел на план запроса от Hibernate, перекрестился и сказал: "Чувак, тут только нативный SQL и хинты спасут". Иногда надо взять управление на себя, а не надеяться на автосборку.

  5. Когда надо наколдовать что-то на уровне схемы. Создать хитрый индекс, триггер, материализованное представление — ORM для этого не предназначен, это как молотком гвозди забивать. Бери SQL и делай как надо.

А вот как это в Java (Spring JPA) выглядит, чтоб ты не думал, что это шаманство:

@Repository
public interface OrderRepository extends JpaRepository<Order, Long> {

    // Всё просто: говоришь, что запрос нативный, и пишешь его как есть.
    @Query(value = 
            "SELECT * FROM orders o " +
            "WHERE o.region_id = :regionId " +
            "AND earth_distance(ll_to_earth(o.lat, o.lng), ll_to_earth(:lat, :lng)) < :radius",
            nativeQuery = true) // <-- Вот этот волшебный флажок!
    List<Order> findOrdersInRadius(@Param("regionId") Long regionId,
                                   @Param("lat") double latitude,
                                   @Param("lng") double longitude,
                                   @Param("radius") double radiusMeters);

    // А для массового обновления ещё и @Modifying нужен.
    @Modifying
    @Query(value = "UPDATE orders SET priority = :newPriority WHERE status = 'PENDING'",
           nativeQuery = true)
    @Transactional // Не забудь про транзакцию, а то будешь потом искать, почему ничего не обновилось.
    int bulkUpdatePriority(@Param("newPriority") int priority);
}

НО! Предупреждаю сразу, как мать родная: с великой силой приходит и великая ответственность, ёпта.

  • Переносимость — нахуй улетела. Запустишь этот запрос под другой БД — получишь ошибку размером с КамАЗ.
  • SQL-инъекции — если будешь строки склеивать как дешёвые колготки, типа "WHERE name = '" + userName + "'", то тебя взломают через неделю, и ты будешь объяснять, почему в твоей базе появились таблицы bitcoin_wallets и hackers_rulez. Всегда используй параметры, как в примере выше (:regionId).

Короче, нативный SQL — это как острый нож. В умелых руках — инструмент, в кривых — член себе отрежешь. Используй с умом, и будет тебе счастье.