在 CQRS 读取模型中使用 Hibernate 进行快速开发

一则或许对你有用的小广告

欢迎加入小哈的星球 ,你将获得:专属的项目实战 / Java 学习路线 / 一对一提问 / 学习打卡/ 赠书活动

目前,正在 星球 内带小伙伴们做第一个项目:全栈前后端分离博客项目,采用技术栈 Spring Boot + Mybatis Plus + Vue 3.x + Vite 4手把手,前端 + 后端全栈开发,从 0 到 1 讲解每个功能点开发步骤,1v1 答疑,陪伴式直到项目上线,目前已更新了 204 小节,累计 32w+ 字,讲解图:1416 张,还在持续爆肝中,后续还会上新更多项目,目标是将 Java 领域典型的项目都整上,如秒杀系统、在线商城、IM 即时通讯、权限管理等等,已有 870+ 小伙伴加入,欢迎点击围观

在这篇文章中,我将分享一些在 cqrs 读取模型中使用 hibernate 工具以实现快速开发的技巧。

为什么要冬眠?

休眠非常流行。它在外部看起来也很简单,而在内部却相当复杂。它使得在没有深入理解、误用和发现问题时已经为时已晚的情况下非常容易上手。由于所有这些原因,这些天它相当臭名昭着。

但是,它仍然是一项坚实而成熟的技术。久经考验、健壮、文档齐全,并在盒子中提供了许多常见问题的解决方案。它可以让你*非常*高效。如果您包括围绕它的工具和库,则更是如此。最后,只要您知道自己在做什么,它就是安全的。

自动架构生成

保持 sql 模式与 java 类定义同步是相当昂贵的,有点困难。在最好的情况下,这是非常乏味和耗时的活动。犯错的机会很多。

hibernate 带有模式生成器 (hbm2ddl),但其“原生”形式在生产中的使用有限。它只能在创建 sessionfactory 时验证架构、尝试更新或导出它。幸运的是,相同的实用程序可用于自定义编程使用。

我们更进一步,将其与 cqrs 预测相结合。它是这样工作的:

  • 当投影进程线程启动时,验证数据库模式是否与 java 类定义匹配。
  • 如果没有,则删除模式并重新导出它(使用 hbm2ddl)。重新启动投影,从一开始就重新处理事件存储。让投影从头开始。
  • 如果匹配,则继续从当前状态更新模型。

多亏了这一点,我们几乎从不手动输入带有表定义的 sql。它使开发速度更快。它类似于使用 hbm2ddl.auto = create-drop 。但是, 在视图模型中使用它意味着它实际上不会丢失数据 (这在事件存储中是安全的)。此外,它足够聪明,仅在模式实际发生更改时才重新创建模式——这与创建-删除策略不同。

保留数据和避免不必要的重启不仅可以缩短开发周期。它还可以使其在生产中可用。至少在某些条件下,见下文。

有一个警告:并非架构中的所有更改都会导致休眠验证失败。一个例子是更改字段长度——只要它是 varchar 或文本,无论限制如何,验证都会通过。另一个未检测到的变化是可空性。

这些问题可以通过手动重新启动投影来解决(见下文)。另一种可能性是有一个不存储数据但被修改以触发自动重启的虚拟实体。它可以有一个名为 schemaversion 的字段,每次模式更改时(由开发人员)更新 @column(name = "v_4") 注释。

执行

以下是它的实现方式:


 public class hibernateschemaexporter {
    private final entitymanager entitymanager;
public hibernateschemaexporter(entitymanager entitymanager) {
    this.entitymanager = entitymanager;
}

public void validateandexportifneeded(list<class> entityclasses) {
    configuration config = getconfiguration(entityclasses);
    if (!isschemavalid(config)) {
        export(config);
    }
}

private configuration getconfiguration(list<class> entityclasses) {
    sessionfactoryimplementor sessionfactory = (sessionfactoryimplementor) getsessionfactory();
    configuration cfg = new configuration();
    cfg.setproperty("hibernate.dialect", sessionfactory.getdialect().tostring());

    // do this when using a custom naming strategy, e.g. with spring boot:

    object namingstrategy = sessionfactory.getproperties().get("hibernate.ejb.naming_strategy");
    if (namingstrategy instanceof namingstrategy) {
        cfg.setnamingstrategy((namingstrategy) namingstrategy);
    } else if (namingstrategy instanceof string) {
        try {
            log.debug("instantiating naming strategy: " + namingstrategy);
            cfg.setnamingstrategy((namingstrategy) class.forname((string) namingstrategy).newinstance());
        } catch (reflectiveoperationexception ex) {
            log.warn("problem setting naming strategy", ex);
        }
    } else {
        log.warn("using default naming strategy");
    }
    entityclasses.foreach(cfg::addannotatedclass);
    return cfg;
}

private boolean isschemavalid(configuration cfg) {
    try {
        new schemavalidator(getserviceregistry(), cfg).validate();
        return true;
    } catch (hibernateexception e) {
        // yay, exception-driven flow!
        return false;
    }
}

private void export(configuration cfg) {
    new schemaexport(getserviceregistry(), cfg).create(false, true);
    clearcaches(cfg);
}

private serviceregistry getserviceregistry() {
    return getsessionfactory().getsessionfactoryoptions().getserviceregistry();
}

private void clearcaches(configuration cfg) {
    sessionfactory sf = entitymanager.unwrap(session.class).getsessionfactory();
    cache cache = sf.getcache();
    stream(cfg.getclassmappings()).foreach(pc -> {
        if (pc instanceof rootclass) {
            cache.evictentityregion(((rootclass) pc).getcacheregionname());
        }
    });
    stream(cfg.getcollectionmappings()).foreach(coll -> {
        cache.evictcollectionregion(((collection) coll).getcacheregionname());
    });
}

private sessionfactory getsessionfactory() {
    return entitymanager.unwrap(session.class).getsessionfactory();
}

}


该 api 看起来相当过时和繁琐。似乎没有办法从现有的 sessionfactory 中提取 configuration 。它只是用来创建工厂并被丢弃的东西。我们必须从头开始重新创建它。以上就是我们需要让它与 spring boot 和 l2 缓存一起工作所需的全部内容。

重新开始预测

我们还实现了一种手动执行此类重新初始化的方法,在管理控制台中显示为一个按钮。当有关投影的某些内容发生变化但不涉及修改模式时,它会派上用场。例如,如果一个值的计算/格式不同,但它仍然是一个文本字段,则可以使用此机制手动重新处理历史记录。另一个用例是修复错误。

生产用途?

我们在开发过程中一直在使用这种机制并取得了巨大成功。它让我们可以通过仅更改 java 类来自由修改模式,而不必担心表定义。由于与 cqrs 的结合,我们甚至可以维护长期运行的演示或试点客户实例。事件存储中的数据始终是安全的。我们可以逐步开发读取模型模式,并将更改自动部署到正在运行的实例,而不会丢失数据或手动编写 sql 迁移脚本。

显然,这种方法有其局限性。在随机时间点重新处理整个事件存储仅在非常小的实例或事件处理速度足够快的情况下才可行。

否则迁移可能会使用 sql 迁移脚本来解决,但它有其局限性。这通常是有风险和困难的。它可能很慢。最重要的是,如果更改较大并且涉及以前未包含在读取模型中(但在事件中可用)的数据,那么使用 sql 脚本根本不是一种选择。

更好的解决方案 是将投影(使用新代码)指向新数据库。让它重新处理事件日志。当它赶上时,测试视图模型,重定向流量并丢弃旧实例。所提出的解决方案也可以与这种方法完美配合。

这篇文章也出现在 绿洲数字博客 上。