Полезная библиотека Pebble Templates

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

Предыстория

В процессе реализации "фитчи" появляется потребность искать более новые, изощренные решения. Данная участь настигла и меня. Задача была применить динамические фильтры с фронта к выборке из БД. Усугубляло ситуацию то, что фильтры нужно было применить не в одном месте, а например в блоке 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.pebbletemplates:pebble-spring-boot-starter:3.1.5")

Поддержка AutoConfiguration, это прекрасно, можно настроить в application.yml или application.properties под свои нужды. Для настройки доступно десяток параметров, про них можно почитать подробнее на официальном сайте, в разделе Spring Boot Integration

Но есть два главных параметра, которые пригодится использовать

  • pebble.prefix: где хранятся шаблоны. По умолчанию /templates/

  • pebble.suffix: формат файлов шаблона. По умолчание он .pebble

Сами же переменные обернуты в двойные фигурные скобки и выглядят так {{ name }}

Как это все готовить?

Все просто, для начала создаем Spring Boot проект, подключаем библиотеку, по примеру выше

Вторым шагом будет добавление параметров в application.yml(в моем случае).

Я буду хранить файл .sql в каталоге sql

pebble:
  prefix: /sql/
  suffix: .sql

В каталоге resources создаем необходимый каталог, в моем случае это sql

Структура resources
Структура 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()

Остальные действия остаются точно такими же. Более подробно можно так же почитать на официальном сайте.

Перейдем к тестам

Реализация простого теста на вызов сервиса

@EnableAutoConfiguration
@SpringBootTest(classes = [PebbleTemplateService::class, FilterContextForSqlBuilder::class])
class PebbleTemplateServiceTest {
    @Autowired
    private lateinit var pebbleTemplateService: PebbleTemplateService

    @Test
    fun testSqlTemplate() {
        println(pebbleTemplateService.prepareSqlTemplate())
    }

    @Test
    fun testSqlTemplateFromString() {
        println(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

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


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

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

Больше никогда не тратьте время на настройку гиперпараметров Я стал дата-сайентистом, потому что мне нравится находить решения для сложных задач. Творческая часть работы и информация...
Явление деления разработчиков на уровни очень распространено. Даже в вакансиях чаще всего пишут не просто "Frontend-разработчик", а более развернуто - "Junior/Middle/Seni...
SWAP (своп) — это механизм виртуальной памяти, при котором часть данных из оперативной памяти (ОЗУ) перемещается на хранение на HDD (жёсткий диск), SSD (твёрдотельный накоп...
Часто от программистов PHP можно услышать: «О нет! Только не „Битрикс“!». Многие специалисты не хотят связываться фреймворком, считают его некрасивым и неудобным. Однако вакансий ...
Привет, Хабр! Представляю вам таблицу ресурсов для маркетологов. Материал в Google-таблице подойдёт как профессионалам, так и тем, кто делает первые шаги в маркетинге. Пользуйтесь, прокачивайте н...