Посчитать запросы spring data jpa + hibernate на 1 rest запрос

Моя цель - предложение широкого ассортимента товаров и услуг на постоянно высоком качестве обслуживания по самым выгодным ценам.

Началось все с желания посчитать, сколько запросов в БД улетает на каждый rest запрос при использовании spring data jpa + hibernate.
Гугл выдал интересное видео про xrebel, но так же сообщил, что xrebel платный.
Дальнейший поиск привел к статье Counting Queries per Request with Hibernate and Spring
Её и взял за основу для своего счетчика. Какого-то ещё примера не нашел, поэтому решил оставить эту заметку

Первое, что нужно создать - класс счетчик:

package ru.counter.utils.querycounter

import org.hibernate.EmptyInterceptor
import org.springframework.stereotype.Component

@Component
class HibernateStatisticsInterceptor : EmptyInterceptor() {
    private val queryCount = ThreadLocal<Long>()

    fun startCounter() {
        queryCount.set(0L)
    }

    fun getQueryCount(): Long {
        return queryCount.get()
    }

    fun clearCounter() {
        queryCount.remove()
    }

    override fun onPrepareStatement(sql: String?): String? {
        val count = queryCount.get()
        if (count != null) {
            queryCount.set(count + 1)
        }
        return super.onPrepareStatement(sql)
    }
}

Т.к. используется обычный mvc а не асинхронный, ThreadLocal будет достаточно

Далее понадобится фильтр http запросов, который будет обнулять счетчик в начале запроса и выводить данные по завершению:

package ru.counter.utils.querycounter

import org.slf4j.Logger
import org.slf4j.LoggerFactory
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty
import org.springframework.core.Ordered
import org.springframework.core.annotation.Order
import org.springframework.stereotype.Component
import javax.servlet.FilterChain
import javax.servlet.http.HttpFilter
import javax.servlet.http.HttpServletRequest
import javax.servlet.http.HttpServletResponse

@Component
@ConditionalOnProperty(name = ["sql.query.count.per.request.log.enable"], havingValue = true.toString())
@Order(Ordered.HIGHEST_PRECEDENCE) // максимальный приоритет, чтобы фильтр был первым
																	// на случай, если другие фильтры тоже кидают запросы в БД
class RequestStatisticsSqlQueryCountFilter(
    private val statisticsInterceptor: HibernateStatisticsInterceptor
) : HttpFilter() {

    private val log: Logger = LoggerFactory.getLogger(javaClass)

    init {
        log.warn(
            "http фильтр для подсчета количества sql запросов на 1 http запрос включен," +
                    " негативно для производительности"
        )
    }

    private val time = ThreadLocal<Long>()

    override fun doFilter(request: HttpServletRequest?, response: HttpServletResponse?, chain: FilterChain?) {
        if (request?.requestURI?.startsWith("/api") != true) { // мне были важны только запросы к api
            chain?.doFilter(request, response)
        } else {
            time.set(System.currentTimeMillis())
            statisticsInterceptor.startCounter()
            chain?.doFilter(request, response)
            val duration = System.currentTimeMillis() - time.get()
            val queryCount: Long = statisticsInterceptor.getQueryCount()
            log.info("[Time: {} ms] [Queries: {}] {} {}", duration, queryCount, request.method, request.requestURI)
            statisticsInterceptor.clearCounter()
            time.remove()
        }
    }
}

Важное тут:

  1. ConditionalOnProperty, считать количество запросов в проме мне не надо, это информация нужна только во время разработки, поэтому добавлена возможность управлять наличием этого фильтра через spring property

  2. На всякий случай вывод в консоль при создании бина о, том что он может негативно влиять на производительность

Третий элемент это регистрация перехватчика запросов для hibernate, который и будет инкрементировать счетчик:

package ru.counter.configuration

import org.slf4j.LoggerFactory
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.beans.factory.annotation.Value
import org.springframework.boot.autoconfigure.orm.jpa.HibernatePropertiesCustomizer
import org.springframework.context.annotation.Configuration
import ru.counter.utils.querycounter.HibernateStatisticsInterceptor

@Configuration
class HibernateInterceptorRegistration : HibernatePropertiesCustomizer {

    private val log = LoggerFactory.getLogger(javaClass)

    @Value("\${sql.query.count.per.request.log.enable:false}")
    private var statisticsInterceptorEnabled: Boolean = false

    @Autowired
    private lateinit var statisticsInterceptor: HibernateStatisticsInterceptor

    override fun customize(hibernateProperties: MutableMap<String, Any>) {
        if (statisticsInterceptorEnabled) {
            log.warn("Счетчик sql запросов на 1 http запрос включен, негативно для производительности")
            hibernateProperties["hibernate.session_factory.interceptor"] = statisticsInterceptor
        }
    }
}
  • Здесь так же он регистрируется, только если при запуске sql.query.count.per.request.log.enable указана и имеет значение true, наверно можно переделать так же под ConditionalOnProperty

  • И точно так же вывод предупреждения в лог.

В итоге запустив приложение со счетчиком можно увидеть данные в логе

INFO RequestStatisticsSqlQueryCountFilter : [Time: 154 ms] [Queries: 18] GET /api/private/v1/auth/check
INFO RequestStatisticsSqlQueryCountFilter : [Time: 149 ms] [Queries: 4] GET /api/private/v1/organisation/employee
INFO RequestStatisticsSqlQueryCountFilter : [Time: 0 ms] [Queries: 0] GET /api/private/v1/auth/check
INFO RequestStatisticsSqlQueryCountFilter : [Time: 62 ms] [Queries: 16] GET /api/private/v1/organisation
INFO RequestStatisticsSqlQueryCountFilter : [Time: 30 ms] [Queries: 1] GET /api/public/v1/image/1001
INFO RequestStatisticsSqlQueryCountFilter : [Time: 30 ms] [Queries: 1] GET /api/public/v1/image/1000

Spring data jpa дает очень удобные интерфейс для работы с БД, но одна ошибка, может стоит 100 или 1000 запросов. Главное, что jpa дает, это скорость.

Возможность быстро сделать прототип и потом его немного допилить, когда интеграции уже зафиксированы и каждая сторона отлаживает и улучшает свою часть.

Про импорты

В коде специально оставил все импорты, некоторые моменты могут быть очевидны, но не все.

Источник: https://habr.com/ru/post/665266/


Интересные статьи

Интересные статьи

Дисклеймер: Эта публикация скорее крик души... я не буду говорить, что являюсь выдающимся экспертом в базах данных, а тема данного поста не для того, чтобы мериться размером дампа. Мне просто больно р...
Боты. В технических кругах о них не писал только ленивый.Мы хотим представить вам свою версию применения этой популярной и такой обсуждаемой темы.Как все начиналось? Мы к...
Не секрет, что Python является одним из самых широко используемых языков для анализа, обработки и визуализации данных, поэтому было логично реализовать функции Data Scien...
Пару дней назад вышел релиз Spring Boot 2.3.0.M1, в описании которого первой строкой упоминается поддержка проекта Cloud Native Buildpacks, являющегося попыткой упростить жизнь разработчика, позв...
Сегодня мы поговорим о перспективах становления Битрикс-разработчика и об этапах этого пути. Статья не претендует на абсолютную истину, но даёт жизненные ориентиры.