Предыстория
В процессе реализации "фитчи" появляется потребность искать более новые, изощренные решения. Данная участь настигла и меня. Задача была применить динамические фильтры к выборке из БД. Усугубляло ситуацию то, что фильтры нужно было применить не в одном месте, а например в блоке WITH. Реализация через JPA Specification выглядела довольно сложно, а возможно и невыполнимо. При помощи JPA Repository можно было, но опять было бы много лишних операций, маппингов, слияний.
Но тут мой тимлид сказал: "Без паники, есть решение" и полез в браузер, словно волшебник, доставая из кармана эликсир, он кинул мне ссылку на неприметную библиотеку. Ей оказалась довольно удобная библиотека Pebble Templates.
Изучив реализацию, я понял, что реализация собой представляет шаблоны с переменными, куда в дальнейшем будут подставляться нужные данные. По началу кажется, что библиотека подходить исключительно для фронта, но нет.
Подумав, а почему бы нет, не нужно писать свою реализацию замены переменных на значения, реализацию обработки данных и тд, что позволит избежать ошибок, сэкономить время и избежать десятки строк лишнего кода. И да, данная библиотека работает и под Spring Boot, что не может не радовать.
Посмотрим более подробно
Подключить библиотеку в проект очень просто через Maven или Gradle. С сайт берем актуальную версию, на данный момент это версия 3.1.5
Maven
<dependency>
<groupId>io.pebbletemplates</groupId>
<artifactId>pebble-spring-boot-starter</artifactId>
<version>3.1.5</version>
</dependency>
Gradle
implementation("io.pebbletemplatesebble-spring-boot-starter:3.1.5")
Поддержка AutoConfiguration, это прекрасно, можно настроить в application.yml или application.properties под свои нужды. Для настройки доступно десяток параметров, про них можно почитать подробнее на официальном сайте, в разделе Spring Boot Integration
Но есть два главных параметра, которые пригодится использовать
Как это все готовить?
Все просто, для начала создаем Spring Boot проект, подключаем библиотеку, по примеру выше
Вторым шагом будет добавление параметров в application.yml(в моем случае).
Я буду хранить файл .sql в каталоге sql
pebble:
prefix: /sql/
suffix: .sql
В каталоге resources создаем необходимый каталог, в моем случае это sql
Структура resources
Пока все идет прекрасно. Что же дальше, а дальше создать необходимо сам шаблон
Создаем в каталоге нужный файл формата, который вы указали в параметре suffix, в моем случае это .sql
Файл шаблона в каталогеselect f.*
from foo f
where {{ filter }}
Так как это всего на всего пример для демонстрации и понимания, то пример простой, в реалиях шаблоны могут быть намного сложнее.
Пример из реальной реализации
with sla_ticket as (
select tt.id as ticketId
from tickets tt
{{ actual_task_filter }}
where tt.ticket_type = 1 and tt.curator_uuid notnull {{ filter }})
select
u.name as author_name,
u.id as author_id,
count(tt1.id) as amount_of_tt,
round((count(tt1.id)*100)::decimal /((select count (*) from sla_ticket))::decimal,2) as percent
from
tickets tt1
join sla_ticket st on tt1.id = st.ticketId
join users u on tt1.author_uuid = u.id
where tt1.start_time < {{ filter_date }}
group by u.name, u.id
order by u.name {{ limit_offset }}
Как видим, тут необходимо применять несколько фильтров к разным кускам запроса.
Шаблон готов, перейдем к реализации кода, я использую Kotlin. По желанию можете использовать Java, не принципиально.
Для начала необходимо составить Map с переменными шаблона и их значениями, которые в дальнейшем будут подставлены. Реализация очень проста, ключом будет имя переменной, значением ее значение. В ключ filter, положили данные f.count = 10.
@Component
open class FilterContextForSqlBuilder {
open fun filterContextBuild(): Map<String, Any> {
val filterContext = mutableMapOf<String, Any>()
filterContext["filter"] = "f.count = 10"
return filterContext
}
}
Чтобы код был красивее и читабельнее, уберем весь текстовый контекст в константы, опять же, это по желанию, я привык к чистому коду.
const val SQL_TEMPLATE_FILE_NAME = "sql_example"
const val SQL_CONTEXT_FILTER_NAME = "filter"
const val SQL_CONDITION = "f.count = 10"
const val SQL_TEMPLATE_STRING_FORMAT = "select f.id\nfrom foo f\nwhere {{ filter }}"
Теперь наш класс выглядит так
@Component
open class FilterContextForSqlBuilder {
open fun filterContextBuild(): Map<String, Any> {
val filterContext = mutableMapOf<String, Any>()
filterContext[SQL_CONTEXT_FILTER_NAME] = SQL_CONDITION
return filterContext
}
}
Далее реализуем сервис, где будет происходить вся "магия".
Я реализовал в примере два метода. В первом шаблон берется из resources. Во втором шаблон берется в виде строки, это полезно, если, например, шаблоны хранятся в БД, это могут быть шаблоны различных нотификаций.
В первую очередь необходимо заинжектить главный класс библиотеки, это PebbleEngine. При помощи данного класса получаем шаблон, он имеет тип PebbleTemplate.
Делается это просто, путем вызова метода getTemplate у PebbleEngine
pebbleEngine.getTemplate("имя_шаблона"), в качестве параметра передаем имя шаблона без расширения, в моем случае это sql_example.
Далее получаем Map с параметрами, вызвав метод компонента, который реализовали ранее - filterContextForSqlBuilder.filterContextBuild(). После создаем объект типа Writer, с которым в дальнейшем и нужно будет работать.
Последний шаг, это запись в write готового шаблона sqlTemplate.evaluate(writer, filterContext).
Код сервиса
@Service
open class PebbleTemplateService(
private var pebbleEngine: PebbleEngine,
private val filterContextForSqlBuilder: FilterContextForSqlBuilder
) {
open fun prepareSqlTemplate(): String {
val sqlTemplate = pebbleEngine.getTemplate(SQL_TEMPLATE_FILE_NAME)
val filterContext = filterContextForSqlBuilder.filterContextBuild()
val writer: Writer = StringWriter()
sqlTemplate.evaluate(writer, filterContext)
return writer.toString() // or JDBC call
}
open fun prepareSqlTemplateFromString(): String {
pebbleEngine = Builder().loader(StringLoader()).build()
val sqlTemplate = pebbleEngine.getTemplate(SQL_TEMPLATE_STRING_FORMAT)
val filterContext = filterContextForSqlBuilder.filterContextBuild()
val writer: Writer = StringWriter()
sqlTemplate.evaluate(writer, filterContext)
return writer.toString() // or JDBC call
}
}
Далее уже готовый шаблон с готовыми параметрами можно, например, передать в JDBC или отправить по email.
Для загрузки шаблона не из resources, нужно pebbleEngine реализовать кастомным образом через вcтроенный билдер: PebbleEngine.Builder().loader(StringLoader()).build()
Остальные действия остаются точно такими же. Более подробно можно так же почитать на официальном сайте.
Важно помнить, что не стоит вызывать методы с готовыми фильтрами с фронта, иначе можно получить много неприятных проблем. Лучше всего воспользоваться маппингом на стороне бэка.
Например модель фильтра с фронта
{
"curatorId": 123456
}
На выходе маппинга получим curatorId = 123456, это выражение уже можно добавлять в мапу filterContext["curator"] = "curatorId = 123456".
Перейдем к тестам
Реализация простого теста на вызов сервиса
@EnableAutoConfiguration
@SpringBootTest(classes = [PebbleTemplateService::class, FilterContextForSqlBuilder::class])
class PebbleTemplateServiceTest {
@Autowired
private lateinit var pebbleTemplateService: PebbleTemplateService
@Test
fun testSqlTemplate() {
isTrue("select f.*\nfrom foo f\nwhere f.count = 10" == pebbleTemplateService.prepareSqlTemplate(), "")
}
@Test
fun testSqlTemplateFromString() {
isTrue("select f.id\nfrom foo f\nwhere f.count = 10" == pebbleTemplateService.prepareSqlTemplateFromString(), "")
}
}
Вызов первого метода сервиса вернул готовый sql запрос, как видим вместо filter подставилось условие
select f.*
from foo f
where f.count = 10
При вызове второго метода, где шаблон брали из строковой константы "select f.id\nfrom foo f\nwhere {{ filter }}"
select f.id
from foo f
where f.count = 10
Спасибо за внимание, надеюсь материал будет полезен.
Пример проекта на github
В процессе реализации "фитчи" появляется потребность искать более новые, изощренные решения. Данная участь настигла и меня. Задача была применить динамические фильтры к выборке из БД. Усугубляло ситуацию то, что фильтры нужно было применить не в одном месте, а например в блоке WITH. Реализация через JPA Specification выглядела довольно сложно, а возможно и невыполнимо. При помощи JPA Repository можно было, но опять было бы много лишних операций, маппингов, слияний.
Но тут мой тимлид сказал: "Без паники, есть решение" и полез в браузер, словно волшебник, доставая из кармана эликсир, он кинул мне ссылку на неприметную библиотеку. Ей оказалась довольно удобная библиотека Pebble Templates.
Изучив реализацию, я понял, что реализация собой представляет шаблоны с переменными, куда в дальнейшем будут подставляться нужные данные. По началу кажется, что библиотека подходить исключительно для фронта, но нет.
Подумав, а почему бы нет, не нужно писать свою реализацию замены переменных на значения, реализацию обработки данных и тд, что позволит избежать ошибок, сэкономить время и избежать десятки строк лишнего кода. И да, данная библиотека работает и под Spring Boot, что не может не радовать.
Посмотрим более подробно
Подключить библиотеку в проект очень просто через Maven или Gradle. С сайт берем актуальную версию, на данный момент это версия 3.1.5
Maven
<dependency>
<groupId>io.pebbletemplates</groupId>
<artifactId>pebble-spring-boot-starter</artifactId>
<version>3.1.5</version>
</dependency>
Gradle
implementation("io.pebbletemplatesebble-spring-boot-starter:3.1.5")
Поддержка AutoConfiguration, это прекрасно, можно настроить в application.yml или application.properties под свои нужды. Для настройки доступно десяток параметров, про них можно почитать подробнее на официальном сайте, в разделе Spring Boot Integration
Но есть два главных параметра, которые пригодится использовать
- pebble.prefix: где хранятся шаблоны. По умолчанию /templates/
- pebble.suffix: формат файлов шаблона. По умолчание он .pebble
Как это все готовить?
Все просто, для начала создаем Spring Boot проект, подключаем библиотеку, по примеру выше
Вторым шагом будет добавление параметров в application.yml(в моем случае).
Я буду хранить файл .sql в каталоге sql
pebble:
prefix: /sql/
suffix: .sql
В каталоге resources создаем необходимый каталог, в моем случае это sql
Пока все идет прекрасно. Что же дальше, а дальше создать необходимо сам шаблон
Создаем в каталоге нужный файл формата, который вы указали в параметре suffix, в моем случае это .sql
from foo f
where {{ filter }}
Так как это всего на всего пример для демонстрации и понимания, то пример простой, в реалиях шаблоны могут быть намного сложнее.
Пример из реальной реализации
with sla_ticket as (
select tt.id as ticketId
from tickets tt
{{ actual_task_filter }}
where tt.ticket_type = 1 and tt.curator_uuid notnull {{ filter }})
select
u.name as author_name,
u.id as author_id,
count(tt1.id) as amount_of_tt,
round((count(tt1.id)*100)::decimal /((select count (*) from sla_ticket))::decimal,2) as percent
from
tickets tt1
join sla_ticket st on tt1.id = st.ticketId
join users u on tt1.author_uuid = u.id
where tt1.start_time < {{ filter_date }}
group by u.name, u.id
order by u.name {{ limit_offset }}
Как видим, тут необходимо применять несколько фильтров к разным кускам запроса.
Шаблон готов, перейдем к реализации кода, я использую Kotlin. По желанию можете использовать Java, не принципиально.
Для начала необходимо составить Map с переменными шаблона и их значениями, которые в дальнейшем будут подставлены. Реализация очень проста, ключом будет имя переменной, значением ее значение. В ключ filter, положили данные f.count = 10.
@Component
open class FilterContextForSqlBuilder {
open fun filterContextBuild(): Map<String, Any> {
val filterContext = mutableMapOf<String, Any>()
filterContext["filter"] = "f.count = 10"
return filterContext
}
}
Чтобы код был красивее и читабельнее, уберем весь текстовый контекст в константы, опять же, это по желанию, я привык к чистому коду.
const val SQL_TEMPLATE_FILE_NAME = "sql_example"
const val SQL_CONTEXT_FILTER_NAME = "filter"
const val SQL_CONDITION = "f.count = 10"
const val SQL_TEMPLATE_STRING_FORMAT = "select f.id\nfrom foo f\nwhere {{ filter }}"
Теперь наш класс выглядит так
@Component
open class FilterContextForSqlBuilder {
open fun filterContextBuild(): Map<String, Any> {
val filterContext = mutableMapOf<String, Any>()
filterContext[SQL_CONTEXT_FILTER_NAME] = SQL_CONDITION
return filterContext
}
}
Далее реализуем сервис, где будет происходить вся "магия".
Я реализовал в примере два метода. В первом шаблон берется из resources. Во втором шаблон берется в виде строки, это полезно, если, например, шаблоны хранятся в БД, это могут быть шаблоны различных нотификаций.
В первую очередь необходимо заинжектить главный класс библиотеки, это PebbleEngine. При помощи данного класса получаем шаблон, он имеет тип PebbleTemplate.
Делается это просто, путем вызова метода getTemplate у PebbleEngine
pebbleEngine.getTemplate("имя_шаблона"), в качестве параметра передаем имя шаблона без расширения, в моем случае это sql_example.
Далее получаем Map с параметрами, вызвав метод компонента, который реализовали ранее - filterContextForSqlBuilder.filterContextBuild(). После создаем объект типа Writer, с которым в дальнейшем и нужно будет работать.
Последний шаг, это запись в write готового шаблона sqlTemplate.evaluate(writer, filterContext).
Код сервиса
@Service
open class PebbleTemplateService(
private var pebbleEngine: PebbleEngine,
private val filterContextForSqlBuilder: FilterContextForSqlBuilder
) {
open fun prepareSqlTemplate(): String {
val sqlTemplate = pebbleEngine.getTemplate(SQL_TEMPLATE_FILE_NAME)
val filterContext = filterContextForSqlBuilder.filterContextBuild()
val writer: Writer = StringWriter()
sqlTemplate.evaluate(writer, filterContext)
return writer.toString() // or JDBC call
}
open fun prepareSqlTemplateFromString(): String {
pebbleEngine = Builder().loader(StringLoader()).build()
val sqlTemplate = pebbleEngine.getTemplate(SQL_TEMPLATE_STRING_FORMAT)
val filterContext = filterContextForSqlBuilder.filterContextBuild()
val writer: Writer = StringWriter()
sqlTemplate.evaluate(writer, filterContext)
return writer.toString() // or JDBC call
}
}
Далее уже готовый шаблон с готовыми параметрами можно, например, передать в JDBC или отправить по email.
Для загрузки шаблона не из resources, нужно pebbleEngine реализовать кастомным образом через вcтроенный билдер: PebbleEngine.Builder().loader(StringLoader()).build()
Остальные действия остаются точно такими же. Более подробно можно так же почитать на официальном сайте.
Важно помнить, что не стоит вызывать методы с готовыми фильтрами с фронта, иначе можно получить много неприятных проблем. Лучше всего воспользоваться маппингом на стороне бэка.
Например модель фильтра с фронта
{
"curatorId": 123456
}
На выходе маппинга получим curatorId = 123456, это выражение уже можно добавлять в мапу filterContext["curator"] = "curatorId = 123456".
Перейдем к тестам
Реализация простого теста на вызов сервиса
@EnableAutoConfiguration
@SpringBootTest(classes = [PebbleTemplateService::class, FilterContextForSqlBuilder::class])
class PebbleTemplateServiceTest {
@Autowired
private lateinit var pebbleTemplateService: PebbleTemplateService
@Test
fun testSqlTemplate() {
isTrue("select f.*\nfrom foo f\nwhere f.count = 10" == pebbleTemplateService.prepareSqlTemplate(), "")
}
@Test
fun testSqlTemplateFromString() {
isTrue("select f.id\nfrom foo f\nwhere f.count = 10" == pebbleTemplateService.prepareSqlTemplateFromString(), "")
}
}
Вызов первого метода сервиса вернул готовый sql запрос, как видим вместо filter подставилось условие
select f.*
from foo f
where f.count = 10
При вызове второго метода, где шаблон брали из строковой константы "select f.id\nfrom foo f\nwhere {{ filter }}"
select f.id
from foo f
where f.count = 10
Спасибо за внимание, надеюсь материал будет полезен.
Пример проекта на github
Полезная библиотека Pebble Templates
ПредысторияВ процессе реализации "фитчи" появляется потребность искать более новые, изощренные решения. Данная участь настигла и меня. Задача была применить динамические фильтры к выборке из БД....
habr.com