用数据库中的模板使用Qute

320 阅读6分钟

博客用数据库中的模板使用Qute

2022年6月21日#qute#开发技巧

用数据库中的模板使用Qute

作者:Gwenneg Lepage

简介

我是红帽团队的一员,他们创建了一个多租户通知服务,从许多红帽混合云控制台应用程序(租户)发送通知。我们的服务可以用来发送几种类型的通知,包括电子邮件。每个租户可以根据自己的需要创建尽可能多的电子邮件模板,并将它们与将触发通知的事件联系起来。

我们通过神奇的Qute模板引擎和以文件形式存储在src/main/resources/templates 文件夹中的模板来实现。它允许我们的租户在对Qute了解不多的情况下设计适合他们需要的模板。然而,我们很快意识到,对租户来说,编辑模板是一个缓慢而沉重的过程。事实上,他们必须在我们的存储库中创建一个GitHub拉动请求,等待审查,然后再次等待部署,然后才能测试模板。我们需要使这个过程对租户来说更容易,最好是自我服务。

然后我们决定使用Qute的TemplateLocator ,将模板从文件存储转移到数据库中。它帮助我们为租户提供了一个更容易、无摩擦和自助式的编辑模板的方式。

before after

以下是我们如何做的。

显而易见的部分:将模板持久化到数据库中

在使用Qute数据库中的模板之前,模板显然需要被持久化。如何执行并不重要。任何类型的Hibernate(无论是否反应式,无论是否有Panache)都可以工作。这篇文章将展示基于Hibernate与Panache的例子。

接下来的章节将使用这个JPA实体。

package org.acme;

import io.quarkus.hibernate.orm.panache.PanacheEntityBase;

import javax.persistence.Entity;
import javax.persistence.Id;

@Entity
public class DbTemplate extends PanacheEntityBase {

    @Id
    public String name; (1)

    public String content;
}
1模板名称将是DB的主键。

有趣的部分:连接Qute到数据库

现在模板可以被持久化了,我们需要一种方法来从Qute使用它们。幸运的是,Qute带有一个非常有趣的接口,叫做TemplateLocator ,可以用来从任何地方加载模板,包括从数据库。

下面是它如何与我们前面定义的DbTemplate 实体一起使用。

package org.acme;

import io.quarkus.logging.Log;
import io.quarkus.qute.EngineBuilder;
import io.quarkus.qute.TemplateLocator;
import io.quarkus.qute.Variant;

import javax.enterprise.context.ApplicationScoped;
import javax.enterprise.event.Observes;
import java.io.Reader;
import java.io.StringReader;
import java.util.Optional;

@ApplicationScoped
public class DbTemplateLocator implements TemplateLocator {

    @Override
    public Optional<TemplateLocation> locate(String name) {
        DbTemplate template = DbTemplate.findById(name);
        if (template == null) {
            Log.tracef("Template with [name=%s] not found in the database", name);
            return Optional.empty();
        } else {
            Log.tracef("Template with [name=%s] found in the database", name);
            return Optional.of(buildTemplateLocation(template.getContent()));
        }
    }

    @Override
    public int getPriority() { (1)
        return DEFAULT_PRIORITY - 1;
    }

    void configureEngine(@Observes EngineBuilder builder) { (2)
        builder.addLocator(this);
    }

    private TemplateLocation buildTemplateLocation(String templateContent) {
        return new TemplateLocation() {

            @Override
            public Reader read() {
                return new StringReader(templateContent);
            }

            @Override
            public Optional<Variant> getVariant() {
                return Optional.empty();
            }
        };
    }
}
1如果你的Quarkus应用程序包含从文件系统和数据库加载的模板,你将需要覆盖模板定位器的默认优先级。否则,Quarkus将尝试使用DbTemplateLocator 从文件系统加载模板,这可能导致异常或不可预测的行为。
2在Quarkus 2.10之前,将DbTemplateLocator 与Quarkus提供的Qute引擎实例集成只能通过这样的CDI观察器完成。

Quarkus 2.10最近引入了一个新的@Locate 注释,使模板定位器注册变得更加简单。

现在模板定位器已经注册了,我们准备用Qute从数据库中编译和渲染模板。正如你在下面的例子中看到的,DB模板的使用与文件模板完全一样。

package org.acme;

import io.quarkus.qute.Engine;
import io.quarkus.qute.Template;

import javax.enterprise.context.ApplicationScoped;
import javax.inject.Inject;

@ApplicationScoped
public class EmailSender {

    @Inject
    Engine engine;

    public void sendEmail(String templateName) {
        Template template = engine.getTemplate(templateName);
        if (template != null) {
            String rendered = template.render();
            // Send an email using the template.
        }
    }
}

小心Qute的内部缓存

每当Qute加载一个模板,它就会被存储到一个内部的ConcurrentHashMap ,并永远留在内存中,除非Qute另有指示。这意味着,在数据库中更新或删除DB模板后,你需要从Qute的内部缓存中删除它。

有几种方法可以实现这一点。

package org.acme;

import io.quarkus.qute.Engine;

import javax.enterprise.context.ApplicationScoped;
import javax.inject.Inject;

@ApplicationScoped
public class DbEngineCacheManager {

    @Inject
    Engine engine;

    public void removeTemplates(String name) {
        engine.removeTemplates(templateName -> templateName.equals(name)); (1)
    }

    public void clearAll() {
        engine.clearTemplates(); (2)
    }
}
1这将删除映射ID与给定谓词相匹配的模板。
2这将从缓存中删除所有模板。

如果你的应用程序运行在有多个副本的Kubernetes集群上,清除内部缓存会变得很棘手。你确实需要一种方法来向所有的pods广播(可能使用Kafka主题或DB表),以从缓存中删除已经更新或删除的模板的指令。有一个更便宜的方法(但非常不完美),可以使用一个计划的工作来保持所有pods缓存的同步。

package org.acme;

import io.quarkus.qute.Engine;
import io.quarkus.scheduler.Scheduled;

import javax.enterprise.context.ApplicationScoped;
import javax.inject.Inject;

@ApplicationScoped
public class DbEngineCacheScheduledCleaner {

    @Inject
    Engine engine;

    @Scheduled(every = "5m", delayed = "5m") (1)
    public void clearTemplates() {
        engine.clearTemplates();
    }
}
1所有的模板将每5分钟从内部缓存中被清除。

防止删除一个被包含的模板

一个Qute模板可以被包含在另一个模板中。如果内部模板被删除,那么外部模板的编译就会失败,这显然是在从数据库加载模板时需要防止的。

这里有一个方法,在删除一个模板之前,寻找是否将其包含到另一个模板中。

package org.acme;

import javax.enterprise.context.ApplicationScoped;
import javax.inject.Inject;
import javax.persistence.EntityManager;
import javax.transaction.Transactional;

@ApplicationScoped
public class TemplateRepository {

    @Inject
    EntityManager entityManager;

    @Transactional
    public void deleteTemplate(String name) {
        long count = entityManager.createQuery("SELECT COUNT(*) FROM DbTemplate WHERE name != :name AND content LIKE :include", Long.class)
                .setParameter("name", name)
                .setParameter("include", "%{#include " + name + "%")
                .getSingleResult();
        if (count > 0) {
            throw new IllegalStateException("Included templates can't be deleted, remove the inclusion or delete the outer template first");
        } else {
            entityManager.createQuery("DELETE FROM DbTemplate WHERE name = :name")
                    .setParameter("name", name)
                    .executeUpdate();
        }
    }
}

数据库模板验证

数据库模板有一个明显的缺点。Quarkus不再能够执行类型安全的验证。

语法验证也会从构建时间延迟到运行时间,但这是意料之中的,因为模板可以在运行时间创建或编辑。

特别感谢

感谢Josejulio Martinez Magana和Martin Kouba在我们的通知服务中实现DB模板的过程中对我的帮助!

Quarkus是开放的。这个项目的所有依赖性都可以在Apache软件许可2.0或兼容许可下使用。

这个网站是用Jekyll建立的,托管在GitHub Pages上,是完全开源的。如果你想让它变得更好,请分叉该网站并向我们展示你的成果。

导航

关注我们

获取帮助

语言

Quarkus是由社区项目组成的

CC by 3.0|隐私政策 赞助者