最近文章

JPA实体映射——多对一关系映射进一步学习

我们先看看第一种情况,如何持计划多对一的关系,以及如何删除这样的关系?首先上代码:public class ManyToOneTest extends AbstractTest { @Override protected Class[] entities() { return new Class[]{ Post.class,
标签:

JPA实体映射——多对一关系映射

前几节我们介绍了一对多的关系和一对一关系,今天我们学习多对一关系以及这种映射方式的最佳实践,先上业务实例图。在我们的业务关系图中,部门和研究所实体是多对一的关系,同时我们还是采用双向关联来说明问题Bidirectional @ManyToOne部门实体import javax.persistence.*; import java.io.Serializable; import java.util.
标签:

深入理解关系数据表之间的关系

首先我们需要理解的是,关系是怎么产生的?在关系表中,关系是属于不同表的相应的行来构建形成的。当子表定义了一个外键引用了父表的主键的时候,表关系就形成。所以理解表关系,需要理解上述四个关键因素以及它们之间的关系。表关系建立在外键的基础上,因此,每一类表关系有三类关系类型: 一对多是最普通的关系,它通过将父表的一行与子表的多行关联起来形成这种关系。 一对一关系需要子表的主键通过外键
标签:

BTree的一种简单实现

最近在学习和理解红黑树的过程中,发现想要深入理解红黑树的本质离不开一种重要的数据结构,这种数据结构就是平衡树(BTree),今天开始学习BTree的一些基本操作。require("@fatso83/mini-mocha").install(); const { expect } = require('chai'); class BTreeNode { constructor(isLeaf){
标签:

单链表的实现

最近学习了RxJs的一些知识,发现RxJs中的Observable其实是一种链表结构,因此想试着温习一下链表的基础知识。require("@fatso83/mini-mocha").install(); const sinon = require("sinon"); const { expect } = require('chai'); class LinkNode { constructo
标签:

Graphs 简单学习

最近学习数据结构的时候,发现一个有意思的规律,那就是复杂的数据结构一定是对简单数据结构的封装和抽象。因此,今天学习一个稍微复杂的数据结构,那就是Graphs。require("@fatso83/mini-mocha").install(); const { expect } = require('chai'); class Node { constructor(data) { this
标签:

Union Find 数据结构和算法理解

最近开始学习一些不那么简单的算法,比如上一篇中的Graph就是一种不那么容易理解和学习的数据结构,今天来学习另一种数据结构和算法。Union-Find这种算法的有哪些特点呢?1、用最少的代码解决相对复杂的问题,而且时间和空间复杂度还相当优异。2、这种算法能解决类似问题,比如:给定一组顶点和边,找出哪些组件属于同一个组,及存在多少个这样的组?3、这种算法的另一个名称:"Disjoint Set"。U
标签:

BST的简单实现

最基础的数据结构往往会有最关键的作用,理解这些基础的数据结构能做什么,以及它们不能做什么对于我们理解数据结构的本质有关键的作用。const Random = require("random-js").Random; const MersenneTwister19937 = require("random-js").MersenneTwister19937; const random = new R
标签:

Queue BFS 初步学习

最近在学习数据结构的时候,学到了树的广度优先搜索可以使用队列来实现,今天尝试着写一下:定义一棵树:let tree = { "10": { value: "10", left: "4", right: "17", }, "4": { value: "4", left: "1", right: "9", }, "17": { value: "17", lef
标签:

RxJs基础

Rxjs基础我们知道在Rxjs中有几类基础组件,它们分别是:1、Observables2、Observers3、Operators4、Subjects5、Schedulers以上这些组件是Rxjs的核心构建块。我们需要理解它们如何协同工作以便提供强大的功能。一旦对它们有了更高层次的理解,使用起来将会得心应手。那么,让我们一一来看看它们。先来看一个例子://import { from } from
标签:

AVLTree简单实现

今天突然感觉AVL树在Tree这种数据结构中有比较重要的作用,特别是理解如何将不平衡的树转化为平衡树的左旋和右旋操作是理解和学习AVL树的基础。先上代码://定义树的基本节点 const Node = function(item){ //基本元素 this.item = item; //节点的高度 this.height = 1; this.left = null; this.ri
标签:

理解CKEditor5的schema

我们知道,CKEditor5是一个用MVC架构设计的富文本编辑器。如上图所示,三层分别是:Model,Controller, View首先,第一个问题是schema属于那一层?经过官方文档的初步学习,我们可以看到:editor.model.schema; // -> The model's schema.因此,我们可以得出结论:schema属于模型层:其次我们需

Injector框架理解(二)

今天我们继续学习didi这个依赖注入框架,上一节我们知道了怎么定义组件,怎么定义模块,怎么通过Injector这个类来载入模块和获取组件。今天我们来学习如何重用模块:我们首先定义好组件和模块:const {Injector} = require('didi'); class FooType { constructor() { this.name = 'foo'; } }
标签:

Injector框架理解(一)

在可测试js的框架学习过程中,我们不断提到了依赖注入,依赖注入使用最多的就是在java中的spring,为了在js中使用依赖注入,我们今天学习另一个不那么常用的框架:didi首先看看安装命令npm install didi这个时候,我看到项目下的package.json{ "name": "babelwebpack", "version": "1.0.0", "description"
标签:

Hibernate映射计算的属性

在一般的实体映射中,一般都是一个属性对应数据库的某一列。今天我们来看看如何映射通过其他属性计算出来的属性。好了先看看具体的两个领域类:新建一个Account.java@Entity(name = "Account") @Table(name = "account") public class Account { @Id private Long id; @ManyToO
标签:

JPA——persistence.xml深入理解

在本文中,我们会解释 JPA persistence.xml 配置文件的用途,以及如何使用可用的 XML 标记或属性设置 Java Persistence 应用程序。虽然 Spring 应用程序可以在不需要 XML JPA 配置文件的情况下进行引导,但理解每个配置选项的含义仍然很重要,因为 Spring 在构建 Java Persistence LocalContainerEntityManage
标签:

JPA实体状态学习-(瞬时态:Transient)

为了学习实体的状态,我们还是贴出这张实体状态转换迁移图:Transient(瞬时态)按照上图的描述,java对象在内存中被赋值后,没有调用entityManager.persist()方法之前实体对象所处的状态就是瞬時態举个例子:Teacher teacher = new Teacher("email@dot.com");此时,实例teacher就处于new/transient态(备注:这里的ne
标签:

JPA实体状态深入理解

我们在学习JPA实体状态的时候,常常会问,JPA的实体有多少状态呢?相信这个问题不难回答:瞬时态(transient)托管态(persistent)游离态(detached)移除态(removed)注意:这里最后一个移除态,有的时候也叫删除态(deleted),至于它和移除态有啥区别,暂时没有想到,如果您对此有更加深刻的理解,请留言回复。为什么会有这四种状态呢?啥,这个也有为啥,网上不是都这么说的
标签:

如何集成多个限界上下文?

在进行领域驱动设计时,发现有两个情境很容易产生误解?他们是:多个限界上下文集成和聚合设计。在本文中,主要介绍多个限界上下文集成。通用语言任何研究过 DDD 的人都已经了解到与其一起使用的许多概念:限界上下文、实体、聚合、值对象等。其中一些概念(实体,聚合,值对象)与代码直接关联,很容易理解和使用,而另一些(限界上下文)则由于其抽象性而难以吸收应用。通用语言就是这样一种情况,尽管它简单而强大,但很容
标签:

JPA实现自定义类型(一)

我们知道,Hibernate是一个JPA的默认实现,因此,在本文中,我们用hibernate来实现一个自定义类型。自定义类型有多种情况,比如:基本类型——基本 Java 类型的映射 可嵌入——复合 java 类型/POJO 的映射 集合——映射基本和复合 java 类型的集合先来实现一种自定义的基本类型,具体的用例就是有一个基本类型LocalDate,我们希望将这个本地日期字段
标签:

JPA实现自定义类型(二)

今天继续昨天的内容,JPA实现一个自定义类型,昨天我们说了怎么实现一个基本类型,今天学习一个复杂一点的。我们的用例场景是将一个领域对象映射到一个类型,比如有一个PhoneNumber的对象,它包含三个字段分别是countryCode,cityCode,number,现在需要将这个对象映射到数据库,我们看怎么来实现:第一步,创建一个PhoneNumber类public class PhoneNumb
标签:

Spring hibernate JPA日志记录问题

今天我们来学习一下如何使用JPA的日志信息来记录执行的SQL语句,当然这里的SQL语句包括查询和修改。我们先了解一般情况下的SQL是如何记录的,看看下面的配置spring: jpa: database-platform: org.hibernate.dialect.H2Dialect hibernate: ddl-auto: update propertie
标签:

如何使用 Hibernate 5 访问数据库表元数据

在本文中,我们简单介绍如何在hibernate中访问数据库表的元数据。IntegratorHibernate 非常灵活,因此它定义了许多 SPI(服务提供者接口),您可以注册这些 SPI 以自定义 Hibernate 内部结构。其中一个接口是 org.hibernate.integrator.spi.Integrator,它被许多与 Hibernate ORM 集成的技术使用,比如 Bean Va
标签:

如何在没有 persistence.xml 配置文件的情况下以编程方式引导 JPA

上一篇我们知道了如何用编程的方式创建一个EntityManagerFactory;今天我们用另外一种更加通用方式来创建EntityManagerFactory。这种方式是基于标准的java持久化API。在上一节,我们用到了一个接口,这个类就是PersistenceUnitInfo,JPA 规范 PersistenceUnitInfo 接口封装了引导 EntityManagerFactory 所需的
标签:

没有persistence.xml初始化一个EntityManagerFactory

上一节我们介绍了通过persistence.xml初始化一个EntityManagerFactory;但是在很多种情况下是不需要使用xml配置文件来配置持久化单元的,因此,我们今天来学习如何通过编程来自动初始化一个EntityManagerFactoryHibernate早就为我们考虑到了这一点,它允许您完全以编程方式构建一个 EntityManagerFactory,只需几行代码:protect
标签:

Mockito and Fluent APIs

Fluent API 是一种基于方法链的软件工程设计技术,用于构建简洁、易读和健壮的接口。它们通常用于建造者、工厂和其他创造性的设计模式。最近,随着 Java 的发展,它们变得越来越流行,并且可以在 Java Stream API 和 Mockito 测试框架等流行的 API 中找到。然而,模拟 Fluent API 可能会很痛苦,因为我们经常需要设置一个复杂的模拟对象层次结构。在本文中,我们将看
标签:

Mockito ArgumentCaptor @Captor

Mockito ArgumentCaptor 用于捕获模拟方法的参数。ArgumentCaptor 与 Mockito verify() 方法一起使用,以在调用任何方法时获取传递的参数。这样,我们可以为我们的测试提供额外的 JUnit 断言。Mockito ArgumentCaptor我们可以为任何类创建 ArgumentCaptor 实例,然后它的 capture() 方法与 verify()
标签:

Mockito——Resetting Mock

Mockito 提供了重置Mock的功能,以便以后可以重用。看看下面的代码片段。//reset mock reset(calcService);在这里,我们重置了Mock对象。 MathApplication 使用 calcService 并在重置模拟后,使用模拟方法将失败测试。举一个例子:// @RunWith attaches a runner with the test class to i
标签:

Mockito——回调

CallbackMockito 提供了一个 Answer 接口,它允许使用通用接口进行stubbing。语法//add the behavior to add numbers when(calcService.add(20.0,10.0)).thenAnswer(new Answer<Double>() { @Override public Double answer(In
标签:

Mockito——创建Mock

Mock创建到目前为止,我们已经使用注解来创建Mock。 Mockito 提供了各种方法来创建模拟对象。 mock() 创建模拟,而不用担心Mock将在适当的时候进行的方法调用的顺序。创建语法:calcService = mock(CalculatorService.class);我们再来看看一个具体的例子:// @RunWith attaches a runner with the test c
标签:

Mockito基本测试功能

在上一篇中,我们知道了如何测试了如何验证mock的方法是否被调用,以及mock出来的方法的返回值等。今天我们继续讨论这些话题。预期调用计数Mockito 提供了以下附加方法来改变预期的调用计数atLeast (int min) - 期望最小调用。 atLeastOnce () - 至少需要一个调用。 atMost (int max) - 期望最大调用次数。我们看看代码的例子:/
标签:

Mockito整合JUnit

在本文中,我们将学习如何将 JUnit 和 Mockito 集成在一起。在这里,我们将创建一个数学应用程序,它使用 CalculatorService 执行基本的数学运算,例如加法、减法、乘法和除法。我们将使用 Mockito 来模拟 CalculatorService 的虚拟实现。此外,我们还广泛使用注解来展示它们与 JUnit 和 Mockito 的兼容性。下面将逐步讨论该过程。第 1 步 -
标签:

Mockito第一个demo

在深入了解 Mockito 框架的细节之前,让我们先看看一个正在运行的应用程序。在这个例子中,我们创建了一个股票服务的模拟来获取一些股票的虚拟价格,并对一个名为 Portfolio 的 java 类进行了单元测试。下面将逐步讨论该过程。第 1 步 - 创建一个 JAVA 类来表示股票Stock.javapublic class Stock { private String stockId;
标签:

Mockito初步认识

Mockito 是一个模拟框架,基于 JAVA 的库,用于对 JAVA 应用程序进行有效的单元测试。Mockito 用于模拟接口,以便可以将虚拟功能添加到可用于单元测试的模拟接口中。本笔记将帮助您了解如何使用 Mockito 创建单元测试以及如何以简单直观的方式使用其 API。本系列文章适用于希望通过单元测试和测试驱动开发来提高软件质量的从新手到专家级别的 Java 开发人员。学习完成本系利文章后
标签:

函数的命令和查询分裂

上一节,我们知道了什么是命令,以及什么是查询。今天我们继续前面的例子,来试着将一个功能太多的函数进行命令和查询的分离。      /** * 查询函数(用于设置值并返回) * */ function configure(values) { var config = { docRoot: '/somewhere' }; var
标签:

EventStorming

EventStorming 词汇表和备忘单EventStorming 是最智能的协作方法,它能打破孤立的边界。EventStorming的能力来自多元化、多学科的人群,他们共同拥有丰富的智慧和知识。它最初是为领域驱动设计聚合建模的研讨会发明的,但它现在具有更广泛的适用范围。从获得整个领域的全局问题空间到深入了解整个软件交付流程并制定长期规划。这些研讨会中的每一个都有相同的基本要求和需求。在这里,您
标签:

sinon最佳实践

我们对可测试js代码的学习已经有了一些理解,也分析了一些关于测试替身的问题,比如什么时候使用spies?什么时候使用stubs,什么时候使用mocks。今天我们用一个测试框架sinonjs来做具体的说明。简介测试使用了Ajax、网络、超时、数据库或其他依赖项的代码可能很困难。例如,如果您使用 Ajax 或网络,您需要有一个服务器来响应您的请求。使用数据库,您需要使用测试数据设置测试数据库。所有这一
标签:

js设计模式(抽象工厂和建造器)

什么是设计模式首先,需要了解设计模式的真正定义。作为软件开发人员,您可以“以任何方式”编写代码。但是,设计模式将是最佳实践,这将对您维护代码的方式产生重大影响。以最大的技巧编写的代码将比业余代码持续更长时间。这意味着当您选择正确的编码风格时,您无需担心可扩展性或维护。设计模式的作用是帮助您构建不会使整体问题复杂化的解决方案。模式将帮助您构建交互式对象和高度可重用的设计。设计模式是面向对象编程中不可
标签:

js设计模式(工厂,原型,单例)

我们知道,在设计模式中,创建型模式有五种,今天我们继续讨论剩下的模式。 工厂模式工厂的作用是生产具有相同特性的相似物体。这有助于轻松管理、维护和操作对象。例如,在我们的玩具工厂,每个玩具都会有一定的信息。它将包含购买日期、原产地和类别。这里我首先定义了一个玩具叫做StarWars,然后定义了一个玩具工厂,然后根据工厂函数创建一个具体的玩具。 var StarWars = fun
标签:

耦合

耦合上一节我们重点关注了扇出的内容,以及用例子说明了如何才能将一个函数或者模块的代码通过重构来达到一个合理的扇出值。今天我们来理解耦合:耦合是关注模块如何组合在一起的,增加子模块或许可以减少扇出的值,但是不能减少原始模块对最初依赖之间的耦合度;本质是将显示依赖变成了间接依赖。虽然今天我们不讲解内聚,但是我们也说说什么是内聚,以及什么是耦合? 内聚是从功能角度来度量模块内的联系,一个好的内
标签:

JPA实体映射——多对一关系映射进一步学习

发布于 2023.05.19 22分钟阅读 0 评论 5 推荐

    作者:

我们先看看第一种情况,如何持计划多对一的关系,以及如何删除这样的关系?

首先上代码:

public class ManyToOneTest extends AbstractTest {


    @Override
    protected Class[] entities() {
        return new Class[]{
                Post.class,
                PostComment.class,
        };
    }

    @Test
    public void testLifecycle() {
        //第一步先持久化一个Post实体
        doInJPA(entityManager -> {
            Post post = new Post()
                    .setId(1L)
                    .setTitle("First post");
            entityManager.persist(post);
        });
        //通过查询出主表post,然后持计划子表post_comment
        doInJPA(entityManager -> {
            Post post = entityManager.find(Post.class, 1L);

            entityManager.persist(
                    new PostComment()
                            .setId(1L)
                            .setReview("My first review")
                            .setPost(post)
            );
        });
        //解除与post的关系
        doInJPA(entityManager -> {
            PostComment comment = entityManager.find(PostComment.class, 1L);

            comment.setPost(null);
            entityManager.remove(comment);
        });
        //删除post_comment表的数据
        doInJPA(entityManager -> {
            PostComment comment = entityManager.getReference(PostComment.class, 1L);

            entityManager.remove(comment);
        });
    }

    @Entity(name = "Post")
    @Table(name = "post")
    public static class Post {

        @Id
        private Long id;

        private String title;

        public Long getId() {
            return id;
        }

        public Post setId(Long id) {
            this.id = id;
            return this;
        }

        public String getTitle() {
            return title;
        }

        public Post setTitle(String title) {
            this.title = title;
            return this;
        }
    }

    @Entity(name = "PostComment")
    @Table(name = "post_comment")
    public static class PostComment {

        @Id
        private Long id;

        private String review;

        @ManyToOne(fetch = FetchType.LAZY)
        @JoinColumn(name = "post_id")
        private Post post;

        public Long getId() {
            return id;
        }

        public PostComment setId(Long id) {
            this.id = id;
            return this;
        }

        public String getReview() {
            return review;
        }

        public PostComment setReview(String review) {
            this.review = review;
            return this;
        }

        public Post getPost() {
            return post;
        }

        public PostComment setPost(Post post) {
            this.post = post;
            return this;
        }
    }
}

首先分析一下上面的代码:主表显然是post,主键是post表的id;子表是post_detail,外键是post_id;

有了以上分析,我们可以看出,持久化主表post是最简单的,直接调用entityManager.persist()就可以了,而持久化子表就不那么容易,需要建立表外键关系,就是查询出post,然后设置到子表的post属性中,然后才持计划。

同理,在删除的时候,你不能直接删除post表,因为它的主键被post_detail的外键引用;需要先删除post_detail后才能删除;而删除post_detail的时候,需要先解除外键关系,然后再删除。

我们看看以上测试执行的SQL:

["insert into post (title, id) values (?, ?)"], Params:[(First post,1)]
["select manytoonet0_.id as id1_0_0_, manytoonet0_.title as title2_0_0_ from post manytoonet0_ where manytoonet0_.id=?"], Params:[(1)]
["insert into post_comment (post_id, review, id) values (?, ?, ?)"], Params:[(1,My first review,1)]
["select manytoonet0_.id as id1_1_0_, manytoonet0_.post_id as post_id3_1_0_, manytoonet0_.review as review2_1_0_ from post_comment manytoonet0_ where manytoonet0_.id=?"], Params:[(1)]
["update post_comment set post_id=?, review=? where id=?"], Params:[(NULL(BIGINT),My first review,1)]
["select manytoonet0_.id as id1_1_0_, manytoonet0_.post_id as post_id3_1_0_, manytoonet0_.review as review2_1_0_ from post_comment manytoonet0_ where manytoonet0_.id=?"], Params:[(1)]
["delete from post_comment where id=?"], Params:[(1)]

好了,从以上代码可以看出在这样的多对一的映射下,该如何操作了吧?同时您也可以看到,是不是有三个不必要的查询呢?这里是否可以优化呢?

 

我们看看下面的代码:

@Test
public void testThreePostComments() {
    doInJPA(entityManager -> {
        Post post = new Post()
            .setId(1L)
            .setTitle("First post");
        entityManager.persist(post);
    });
    doInJPA(entityManager -> {
        Post post = entityManager.getReference(Post.class, 1L);

        entityManager.persist(
            new PostComment()
                .setId(1L)
                
                .setReview("My first review")
                .setPost(post)
        );

        entityManager.persist(
            new PostComment()
                .setId(2L)
                .setReview("My second review")
                .setPost(post)
        );

        entityManager.persist(
            new PostComment()
                .setId(3L)
                .setReview("My third review")
                .setPost(post)
        );
    });

    doInJPA(entityManager -> {
        PostComment comment1 = entityManager.getReference(PostComment.class, 2L);

        entityManager.remove(comment1);
    });

    doInJPA(entityManager -> {
        List comments = entityManager.createQuery(
            "select pc " +
            "from PostComment pc " +
            "where pc.post.id = :postId", PostComment.class)
            .setParameter("postId", 1L)
            .getResultList();

        assertEquals(2, comments.size());
    });
}

这个例子和上一个例子最大的区别是,在持久化子表post_detail的时候,查询的时候使用了entityManager.getReference(),使用这个方法的好处是避免了对主表post的查询,从而少了一次查询。我们看看日志:

["insert into post (title, id) values (?, ?)"], Params:[(First post,1)]
["insert into post_comment (post_id, review, id) values (?, ?, ?)"], Params:[(1,My first review,1)]
["insert into post_comment (post_id, review, id) values (?, ?, ?)"], Params:[(1,My second review,2)]
["insert into post_comment (post_id, review, id) values (?, ?, ?)"], Params:[(1,My third review,3)]
["select manytoonet0_.id as id1_1_0_, manytoonet0_.post_id as post_id3_1_0_, manytoonet0_.review as review2_1_0_ from post_comment manytoonet0_ where manytoonet0_.id=?"], Params:[(2)]
["delete from post_comment where id=?"], Params:[(2)]
["select manytoonet0_.id as id1_1_, manytoonet0_.post_id as post_id3_1_, manytoonet0_.review as review2_1_ from post_comment manytoonet0_ where manytoonet0_.post_id=?"], Params:[(1)]

从日志我们可以看出,在插入子表部分进行了优化,当然,如果这里可以批量插入,那么这个优化就太棒了,不知道有没有啥好的办法做到批量插入子表;同时这里的删除如果能直接删除不用查询就更好了。

 

 

JPA实体映射——多对一关系映射

更新于 2023.05.19 21分钟阅读 0 评论 12 推荐

    作者:

前几节我们介绍了一对多的关系和一对一关系,今天我们学习多对一关系以及这种映射方式的最佳实践,先上业务实例图。

在我们的业务关系图中,部门和研究所实体是多对一的关系,同时我们还是采用双向关联来说明问题

Bidirectional @ManyToOne

部门实体

import javax.persistence.*;
import java.io.Serializable;
import java.util.Objects;
@Entity(name = "Department")
@Table(name = "departments")
public class Department implements Serializable {
    @Id
    @GeneratedValue
    private Long id = 0L;

    private String name;

    @ManyToOne(fetch = FetchType.LAZY)
    private Institute institute;

    public Department(){}

    public void setId(Long id) {
        this.id = id;
    }

    public void setName(String name) {
        this.name = name;
    }

    public void setInstitute(Institute institute) {
        this.institute = institute;
    }

    public Department(String name){
        this.name = name;
    }

    public Long getId() {
        return id;
    }

    public String getName() {
        return name;
    }

    public Institute getInstitute() {
        return institute;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof Department)) return false;
        Department that = (Department) o;
        return name.equals(that.name);
    }

    @Override
    public int hashCode() {
        return Objects.hash(name);
    }
}

DepartmentDAO

import com.jpa.demo.model.bidirectional.Department;
import com.jpa.demo.model.bidirectional.Institute;
import com.jpa.demo.utils.JPAUtil;

import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.EntityTransaction;

public class DepartmentDAO {

    private EntityManagerFactory entityManagerFactory = JPAUtil.getEntityManagerFactory();


    public void saveDepartment(Long instituteId) {
        EntityManager entityManager = null;
        Long id = null;
        try {
            entityManager = this.entityManagerFactory.createEntityManager();
            EntityTransaction tx = entityManager.getTransaction();
            tx.begin();
            Institute institute = entityManager.find(Institute.class, instituteId);
            Department department = new Department("深圳研究所-第一部门");
            department.setInstitute(institute);
            entityManager.persist(department);
            tx.commit();
        } finally {
            entityManager.close();
        }
    }
}

测试代码

@Test
public void testSaveDepartment() {
    Institute institute = new Institute("深圳研究所");
    institute.addDepartment(
        new Department("深圳研究所1部")
    );
    InstituteDAO dao = new InstituteDAO();
    dao.save(institute);
    Long instituteId = institute.getId();

    DepartmentDAO departmentDAO = new DepartmentDAO();
    departmentDAO.saveDepartment(instituteId);
}

日志信息

Hibernate: 
    select
        institute0_.id as id1_1_0_,
        institute0_.name as name2_1_0_ 
    from
        institutes institute0_ 
    where
        institute0_.id=?
Hibernate: 
    call next value for hibernate_sequence
Hibernate: 
    insert 
    into
        departments
        (institute_id, name, id) 
    values
        (?, ?, ?)

可以看出,我在保存部门信息的时候,同时还查询除了研究所信息,这里可以优化为直接保存部门信息。

只需要修改一行代码

Institute institute = entityManager.getReference(Institute.class, instituteId);

修改成getReference()方法后,就不用去查询研究所的实体啦。

Hibernate: 
   insert 
   into
       departments
       (institute_id, name, id) 
   values
       (?, ?, ?)

 

查询多对一多方的一条记录,查询方法如下:

public void queryDepartment(Long departmentId) {
    EntityManager entityManager = null;
    try {
        entityManager = this.entityManagerFactory.createEntityManager();
        EntityTransaction tx = entityManager.getTransaction();
        tx.begin();
        Department department = entityManager.find(Department.class, departmentId);
        department.getInstitute().getName();

        tx.commit();
    } finally {
        entityManager.close();
    }
 
}

测试代码

@Test
public void testQueryDepartment() {
    Institute institute = new Institute("深圳研究所");
    institute.addDepartment(
            new Department("深圳研究所1部")
    );
    InstituteDAO dao = new InstituteDAO();
    dao.save(institute);
    Long instituteId = institute.getId();

    DepartmentDAO departmentDAO = new DepartmentDAO();
    Department department = departmentDAO.saveDepartment(instituteId);
    Long departmentId = department.getId();
    departmentDAO.queryDepartment(departmentId);
}

日志信息:

Hibernate: 
   select
       department0_.id as id1_0_0_,
       department0_.institute_id as institut3_0_0_,
       department0_.name as name2_0_0_ 
   from
       departments department0_ 
   where
       department0_.id=?
Hibernate: 
   select
       institute0_.id as id1_1_0_,
       institute0_.name as name2_1_0_ 
   from
       institutes institute0_ 
   where
       institute0_.id=?

可以看出,为了获取研究所的信息,同时也查询了研究所实体,在此情况下,可以使用另一种方法不用查询研究所实体。

public void queryDepartmentByJPQL(Long departmentId) {
        EntityManager entityManager = null;
        try {
            entityManager = this.entityManagerFactory.createEntityManager();
            EntityTransaction tx = entityManager.getTransaction();
            tx.begin();
            Department department = entityManager.createQuery("select dt from Department dt join fetch\n" +
                            "                     dt.institute where dt.id =:id", Department.class)
                    .setParameter("id",departmentId)
                    .getSingleResult();;
            department.getInstitute().getName();
            entityManager.persist(department);
            tx.commit();
        } finally {
            entityManager.close();
        }
    }

测试方法只需要修改一行

departmentDAO.queryDepartmentByJPQL(departmentId);

日志信息如下:

Hibernate: 
   select
       department0_.id as id1_0_0_,
       institute1_.id as id1_1_1_,
       department0_.institute_id as institut3_0_0_,
       department0_.name as name2_0_0_,
       institute1_.name as name2_1_1_ 
   from
       departments department0_ 
   inner join
       institutes institute1_ 
           on department0_.institute_id=institute1_.id 
   where
       department0_.id=?

可以看出,这次只发出了一条SQL,不同于上一种方式发出了两条SQL,优化了性能,同时也取得了相应的值。

深入理解关系数据表之间的关系

更新于 2023.05.19 1分钟阅读 0 评论 5 推荐

    作者:

首先我们需要理解的是,关系是怎么产生的?在关系表中,关系是属于不同表的相应的行来构建形成的。

子表定义了一个外键引用了父表主键的时候,表关系就形成。所以理解表关系,需要理解上述四个关键因素以及它们之间的关系。

表关系建立在外键的基础上,因此,每一类表关系有三类关系类型:

  1.  一对多是最普通的关系,它通过将父表的一行与子表的多行关联起来形成这种关系。
  2.  一对一关系需要子表的主键通过外键的形式与父表的主键引用而关联起来。(这里是所谓的共享主键)
  3.  多对多关系需要一个包含两个外键的中间表来引用两个父表的主键,从而关联起来形成这种关系。

这里分别用三张图来说明三种关系:

One-To-Many

 

One-To-One

 

Many-To-Many

 

以上图示简单明了,如果不能理解的话,那么就试着寻找关系的四个要素:父表,主键,子表,外键。理解了这四个要素,那么你就理解的关系数据库表之间的各种关系。

 

 

BTree的一种简单实现

更新于 2023.05.15 36分钟阅读 0 评论 15 推荐

    作者:

最近在学习和理解红黑树的过程中,发现想要深入理解红黑树的本质离不开一种重要的数据结构,这种数据结构就是平衡树(BTree),今天开始学习BTree的一些基本操作。

require("@fatso83/mini-mocha").install();
const { expect } = require('chai');
class BTreeNode {
  constructor(isLeaf){
    //是否属于叶子节点
    this.isLeaf = isLeaf;
    //节点用于存储具体数据的数组,且数据从前到后有顺序
    this.values = [];
    //存储节点对应的分支,或者子节点
    this.children = [];
    //存储节点所在的树节点
    this.tree = null;
    //节点对应的父节点
    this.parent = null;
  }

  /**
   * 获取节点存储数据的长度
   */
  get n() {
    return this.values.length;
  }

  /**
   * 在某个节点增加一个值
   */
  addValue(value) {
    if(!value) {
      return;
    }
    let pos = 0;
    while(pos < this.n && this.values[pos] < value) {
      pos++;
    }
    return this.values.splice(pos,0,value);
  }
  removeValue(pos) {
    if (pos >= this.n) {
      return null;
    }
    return this.values.splice(pos, 1)[0];
  }
  
  /**
   * 在指定位置添加一个子节点
   */
  addChild(node, pos) {
    this.children.splice(pos,0,node);
    node.parent = this;
  }

  /**
   * 删除指定位置的节点
   */
  removeChild(pos) {
    return this.children.splice(pos,1)[0];
  }
}

describe('BTreeNode test',()=>{
  it('BTreeNode init test',()=>{
    let bTreeNode = new BTreeNode(false);
    expect(bTreeNode.isLeaf).to.equal(false);
    expect(bTreeNode.n).to.equal(0);
    expect(bTreeNode.children.length).to.equal(0);
    expect(bTreeNode.tree).to.equal(null);
    expect(bTreeNode.parent).to.equal(null);
  });

  it('BTreeNode addValue test',()=>{
    let bTreeNode = new BTreeNode(false);
    bTreeNode.addValue(5);
    bTreeNode.addValue(8);
    bTreeNode.addValue(2);
    expect(bTreeNode.n).to.equal(3);
    const values = bTreeNode.values;
    expect(values.join('')).to.equal('258')
  });

  it('BTreeNode removeValue test',()=>{
    let bTreeNode = new BTreeNode(false);
    bTreeNode.addValue(3);
    bTreeNode.addValue(9);
    bTreeNode.addValue(7);
    bTreeNode.addValue(6);
    expect(bTreeNode.n).to.equal(4);
    const values = bTreeNode.values;
    expect(values.join('')).to.equal('3679');
    bTreeNode.removeValue(2);
    expect(bTreeNode.n).to.equal(3);
    const values1 = bTreeNode.values;
    expect(values1.join('')).to.equal('369');
  })
  
});

class BTree {
  constructor(order){
    this.order = order;
    this.root = null;
  }

  /**
   * Search a value in the Tree and return the node. O(log N)
   * @param {number} value 
   * @returns {BTreeNode}
   */
  searchValue(node,value) {
    if (node.values.includes(value)) {
      return node;
    }
    if (node.leaf) {
      return null;
    }
    let child = 0;
    while(child <= node.n && node.values[child]< parseInt(value,10)) {
      child++;
    }
    return this.searchValue(node.children[child],value);
  }

  /**
   * Insert a new value in the tree O(log N)
   * @param {number} value
   */
  insert(value) {
    const actual = this.root;
    if (actual.n === 2 * this.order - 1) {
      // Create a new node to become the root
      // Append the old root to the new one
      const temp = new BTreeNode(false);
      temp.tree = this;
      this.root = temp;
      temp.addChild(actual, 0);
      this.split(actual, temp, 1);
      this.insertNonFull(temp, parseInt(value));
    } else {
      this.insertNonFull(actual, parseInt(value));
    }
  }

  /**
   * Insert a value in a not-full node. O(1)
   * @param {BTreeNode} node 
   * @param {number} value
   */
  insertNonFull(node, value) {
    if (node.leaf) {
      node.addValue(value);
      return;
    }
    let temp = node.n;
    //找到需要插入的位置
    while(temp >1 && value < node.values[temp-1]) {
      temp = temp-1;
    }
    if(node.children[temp].n === (this.order * 2 -1) ) {
      this.split(node.children[temp], node, temp + 1);
      if (value  > node.values[temp]) {
        temp = temp + 1;
      }
    }
    this.insertNonFull(node.children[temp], value);
  }

  /**
   * Divide child node from parent into parent.values[pos-1] and parent.values[pos]
   * O(1)
   * @param {BTreeNode} child 
   * @param {BTreeNode} parent 
   * @param {number} pos 
   */
  split(child, parent, pos) {
    const newChild = new BTreeNode(child.leaf);
    newChild.tree = this.root.tree;
    // Create a new child
    // Pass values from the old child to the new
    for (let k = 1; k < this.order; k++) {
      newChild.addValue(child.removeValue(this.order));
    }
    // Trasspass child nodes from the old child to the new
    if (!child.leaf) {
      for (let k = 1; k <= this.order; k++) {
        newChild.addChild(child.deleteChild(this.order), k - 1);
      }
    }
    // Add new child to the parent
    parent.addChild(newChild, pos);
    // Pass value to parent
    parent.addValue(child.removeValue(this.order - 1));
    parent.leaf = false;
  }

  /**
   * Deletes the value from the Tree. O(log N)
   * @param {number} value 
   */
  delete(value) {
    if (this.root.n === 1 && !this.root.leaf &&
      this.root.children[0].n === this.order-1 && this.root.children[1].n === this.order -1) {
      // Check if the root can shrink the tree into its childs
      this.mergeNodes(this.root.children[1], this.root.children[0]);
      this.root = this.root.children[0];
    }
    // Start looking for the value to delete
    this.deleteFromNode(this.root, parseInt(value, 10));
  }

  /**
   * Delete a value from a node
   * @param {BTreeNode} node 
   * @param {number} value 
   */
  deleteFromNode(node, value) {
    const index = node.values.indexOf(value);
    if (index >=0) {
      // Value present in the node simple case1 叶子节点且数目也足够,直接删除
      if (node.leaf && node.n > this.order - 1) {
        // If the node is a leaf and has more than order-1 values, just delete it
        node.removeValue(node.values.indexOf(value));
        return true;
      }
      if(node.children[index].n > this.order-1 || 
        node.children[index+1].n > this.order-1) {
        // One of the immediate children has enough values to transfer
        if (node.children[index].n > this.order - 1) {
          // Replace the target value for the higher of left node.
          // Then delete that value from the child
          const predecessor = this.getMinMaxFromSubTree(node.children[index], 1);
          node.values[index] = predecessor;
          return this.deleteFromNode(node.children[index], predecessor);
        }
        const successor = this.getMinMaxFromSubTree(node.children[index+1], 0);
        node.values[index] = successor;
        return this.deleteFromNode(node.children[index+1], successor);
      }
      // Children has not enough values to transfer. Do a merge
      this.mergeNodes(node.children[index + 1], node.children[index]);
      return this.deleteFromNode(node.children[index], value);
    }
    // Value is not present in the node
    if (node.leaf) {
      // Value is not in the tree
      return false;
    }
    // Value is not present in the node, search in the children
    let nextNode = 0;
    while (nextNode < node.n && node.values[nextNode] < value) {
      nextNode++;
    }
    if (node.children[nextNode].n > this.order - 1) {
      // Child node has enough values to continue
      return this.deleteFromNode(node.children[nextNode], value);
    }
    // Child node has not enough values to continue
    // Before visiting next node transfer a value or merge it with a brother
    if ((nextNode > 0 && node.children[nextNode - 1].n > this.order - 1) ||
      (nextNode < node.n && node.children[nextNode + 1].n > this.order - 1)) {
      // One of the immediate children has enough values to transfer
      if (nextNode > 0 && node.children[nextNode - 1].n > this.order - 1) {
        this.transferValue(node.children[nextNode - 1], node.children[nextNode]);
      } else {
        this.transferValue(node.children[nextNode + 1], node.children[nextNode]);
      }
      return this.deleteFromNode(node.children[nextNode], value);
    }
    // No immediate brother with enough values.
    // Merge node with immediate one brother
    this.mergeNodes(
      nextNode > 0 ? node.children[nextNode - 1] : node.children[nextNode + 1],
      node.children[nextNode]);
    return this.deleteFromNode(node.children[nextNode], value);
  }

  /**
   * Get the lower or higher value in a sub-tree. O(log N)
   * @param {BTreeNode} node 
   * @param { 0 | 1 } max 1 for find max, 0 for min
   * @returns {number}
   */
  getMinMaxFromSubTree(node, max) {
    while (!node.leaf) {
      node = node.children[max ? node.n : 0];
    }
    return node.values[max ? node.n - 1 : 0];
  }

  /**
   * Transfer one value from the origin to the target. O(1)
   * @param {BTreeNode} origin 
   * @param {BTreeNode} target 
   */
  transferValue(origin, target) {
    const indexo = origin.parent.children.indexOf(origin);
    const indext = origin.parent.children.indexOf(target);
    if (indexo < indext) {
      target.addValue(target.parent.removeValue(indexo));
      origin.parent.addValue(origin.removeValue(origin.n-1));
      if (!origin.leaf) {
        target.addChild(origin.deleteChild(origin.children.length-1), 0);
      }
    } else {
      target.addValue(target.parent.removeValue(indext));
      origin.parent.addValue(origin.removeValue(0));
      if (!origin.leaf) {
        target.addChild(origin.deleteChild(0), target.children.length);
      }
    }
  }

  /**
   * Mix 2 nodes into one. O(1)
   * @param {BTreeNode} origin 
   * @param {BTreeNode} target 
   */
  mergeNodes(origin, target) {
    const indexo = origin.parent.children.indexOf(origin);
    const indext = target.parent.children.indexOf(target);
    target.addValue(target.parent.removeValue(Math.min(indexo, indext)));
    for (let i = origin.n - 1; i >= 0; i--) {
      target.addValue(origin.removeValue(i));
    }
    // Remove reference to origin node
    target.parent.deleteChild(indexo);
    // Transfer all the children from origin node to target
    if (!origin.leaf) {
      while (origin.children.length) {
        if (indexo > indext) {
          target.addChild(origin.deleteChild(0), target.children.length);
        } else {
          target.addChild(origin.deleteChild(origin.children.length - 1), 0);
        }
      }
    }
  }
}

单链表的实现

更新于 2023.05.12 1分钟阅读 1 评论 15 推荐

    作者:

最近学习了RxJs的一些知识,发现RxJs中的Observable其实是一种链表结构,因此想试着温习一下链表的基础知识。

require("@fatso83/mini-mocha").install();
const sinon = require("sinon");
const { expect } = require('chai');

class LinkNode {
  constructor(data){
    this.data = data;
    this.next = null
  }
}

class SingleLinkList {
  constructor(head = null){
    this.head = head;
    this.tail = null;
  }

  size() {
    let count = 0;
    let node = this.head;
    while(node) {
      count++;
      node = node.next;
    }
    return count;
  }
  print() {
    let result = [];
    let cur = this.head;
    while(cur) {
      result.push(cur.data);
      cur = cur.next;    
    }
    return result;
  }

  delete(data) {
    if ( this.head == null) {
      //链表为空什么都不错
      return;
    }
    let pre = this.head;
    let cur = this.head;
    if (cur.data == data) {
      //第一个节点就是要删除的节点
      this.head = cur.next;
      //删除数据
      cur = null;
      return;
    }
    while(cur) {
      pre = cur;
      cur = cur.next;
      if (cur.data == data) {
        pre.next = cur.next;
        cur = null;
        return;
      }
    }
    
  }

  add(data) {
    const node = new LinkNode(data);
    if (this.head == null) {
      this.head = node;
    }
    
    const temp = this.tail;
    if (temp!=null) {
      temp.next = node;
    } 
    this.tail = node;
  }

  rotateRight(head, k) {
    let numberOfNodes = 0;
    let ptr1 = head;
    while(ptr1!=null) {
      ptr1 = ptr1.next;
      numberOfNodes++;
    }
    if(numberOfNodes!=0){
        k = k%numberOfNodes;
    }else{
        return head;
    }
    let count = 0;
    ptr1 = head;
    while(count < numberOfNodes-k-1) {
      ptr1 = ptr1.next;
      count++;
    }
    let tempNode = ptr1;
    while(ptr1.next!=null){
        ptr1 = ptr1.next;
    }
    
    ptr1.next = head;
    head = tempNode.next;
    tempNode.next = null;     
    return head;
  }
}

const singleLinkList = new SingleLinkList();
for (let i = 0 ; i < 10 ; i++) {
  singleLinkList.add(i);
}

describe('SingleLinkList test',()=>{
  
  it('SingleLinkList.add test',()=>{
    //测试新增节点
    const singleLinkList = new SingleLinkList();
    for (let i = 0 ; i < 10 ; i++) {
      singleLinkList.add(i);
    }
    const arrays = [0,1,2,3,4,5,6,7,8,9];
    const spy = sinon.spy(arrays);
    const result = singleLinkList.print();
    expect(result.length).to.equal(10);
    sinon.assert.match(result,spy);
  });
  
  it('SingleLinkList.delete middle node test',()=>{
    //测试删除链表中间节点
    const singleLinkList = new SingleLinkList();
    for (let i = 0 ; i < 10 ; i++) {
      singleLinkList.add(i);
    }
    singleLinkList.delete(3);
    const arrays = [0,1,2,4,5,6,7,8,9];
    const spy = sinon.spy(arrays);
    const result = singleLinkList.print();
    expect(result.length).to.equal(9);
    sinon.assert.match(result,spy);
  });
  it('SingleLinkList.delete head node test',()=>{
    //测试删除链表头节点
    const singleLinkList = new SingleLinkList();
    for (let i = 0 ; i < 10 ; i++) {
      singleLinkList.add(i);
    }
    singleLinkList.delete(0);
    const arrays = [1,2,3,4,5,6,7,8,9];
    const spy = sinon.spy(arrays);
    const result = singleLinkList.print();
    expect(result.length).to.equal(9);
    sinon.assert.match(result,spy);
  });
  it('SingleLinkList.delete tail node test',()=>{
    //测试删除链表尾节点
    const singleLinkList = new SingleLinkList();
    for (let i = 0 ; i < 10 ; i++) {
      singleLinkList.add(i);
    }
    singleLinkList.delete(9);
    const arrays = [0,1,2,3,4,5,6,7,8];
    const spy = sinon.spy(arrays);
    const result = singleLinkList.print();
    expect(result.length).to.equal(9);
    sinon.assert.match(result,spy);
  });

  it('SingleLinkList rotateRight test',()=>{
    const singleLinkList = new SingleLinkList();
    for (let i=1; i <6 ;i++) {
      singleLinkList.add(i);
    }
    const head = singleLinkList.rotateRight(singleLinkList.head,7);
    expect(head.data).to.equal(4);
  });
});

在上一个版本中,主要通过控制台打印来验证链表是否正确,但是在实际的产品中,代码没有通过正式的单元测试始终不那么靠谱。因此这部分增加了单元测试。

如果代码在运行过程中出现出现了错误,实际上不是代码的问题,而是运行测试框架的时候,需要选择更高版本的nodejs运行环境,这里的测试框架代码在node10版本运行有问题,因此需要切换到node14或者更高。

在这里,提出一个疑问?首先,这里的链表是单链表,因此,如果找到链表中间的那个元素呢?

欢迎在评论区写下您的思路即可。

Graphs 简单学习

更新于 2023.05.12 22分钟阅读 1 评论 15 推荐

    作者:

最近学习数据结构的时候,发现一个有意思的规律,那就是复杂的数据结构一定是对简单数据结构的封装和抽象。因此,今天学习一个稍微复杂的数据结构,那就是Graphs。

require("@fatso83/mini-mocha").install();
const { expect } = require('chai');
class Node {
  constructor(data) {
    this.data = data;
    this.nextElement = null;
  }
}

class LinkedList {
  constructor() {
    this.head = null;
  }

  //Insertion At Head  
  insertAtHead(newData) {
    let tempNode = new Node(newData);
    tempNode.nextElement = this.head;
    this.head = tempNode;
    return this; //returning the updated list
  }

  isEmpty() {
    return (this.head == null);
  }

  //function to print the linked list
  printList() {
    const arrStr = [];
    if (this.isEmpty()) {
      arrStr.push('Empty List');
      console.log(arrStr.join(''));
      return false;
    } else {
      let temp = this.head;
      while (temp != null) {
        arrStr.push(temp.data);
        arrStr.push('->');
        temp = temp.nextElement;
      }
      arrStr.push('null');
      console.log(arrStr.join(''));
      return true;
    }
  }

  getHead() {
    return this.head;
  }
  setHead(newHead) {
    this.head = newHead;
    return this;
  }
  getListStr() {
    if (this.isEmpty()) {
      console.log("Empty List");
      return "null";
    } else {
      let st = "";
      let temp = this.head
      while (temp != null) {
        st += (temp.data);
        st += " -> ";
        temp = temp.nextElement;
      }
      st += "null";
      return st;
    }
  }
  insertAtTail(newData) {
    //Creating a new Node with data as newData
    let node = new Node(newData);

    //check for case when list is empty
    if (this.isEmpty()) {
      //Needs to Insert the new node at Head
      this.head = node;
      return this;
    }

    //Start from head
    let currentNode = this.head;

    //Iterate to the last element
    while (currentNode.nextElement != null) {
      currentNode = currentNode.nextElement;
    }

    //Make new node the nextElement of last node of list
    currentNode.nextElement = node;
    return this;
  }
  search(value) {
    //Start from the first element
    let currentNode = this.head;

    //Traverse the list until you find the value or reach the end
    while (currentNode != null) {
      if (currentNode.data == value) {
        return true; //value found
      }
      currentNode = currentNode.nextElement
    }
    return false; //value not found
  }
  deleteAtHead() {
    //if list is empty, do nothing
    if (this.isEmpty()) {
      return this;
    }
    //Get the head and first element of the list
    let firstElement = this.head;

    //If list is not empty, link head to the nextElement of firstElement
    this.head = firstElement.nextElement;

    return this;
  }
  deleteVal(value) {
    let deleted = null; //True or False
    //Write code here

    //if list is empty return false
    if (this.isEmpty()) {
      return false;
    }

    //else get pointer to head
    let currentNode = this.head;
    // if first node's is the node to be deleted, delete it and return true
    if (currentNode.data == value) {
      this.head = currentNode.nextElement;
      return true;
    }

    // else traverse the list
    while (currentNode.nextElement != null) {
      // if a node whose next node has the value as data, is found, delete it from the list and return true
      if (currentNode.nextElement.data == value) {
        currentNode.nextElement = currentNode.nextElement.nextElement;
        return true;
      }
      currentNode = currentNode.nextElement;
    }
    //else node was not found, return false
    deleted = false;
    return deleted;
  }
  deleteAtTail() {
    // check for the case when linked list is empty
    if (this.isEmpty()) {
      return this;
    }
    //if linked list is not empty, get the pointer to first node
    let firstNode = this.head;
    //check for the corner case when linked list has only one element
    if (firstNode.nextElement == null) {
      this.deleteAtHead();
      return this;
    }
    //otherwise traverse to reach second last node
    while (firstNode.nextElement.nextElement != null) {
      firstNode = firstNode.nextElement;
    }
    //since you have reached second last node, just update its nextElement pointer to point at null, skipping the last node
    firstNode.nextElement = null;
    return this;
  }
}
//定义一个Graphs
class Graph {
  constructor(vertices){
    //存储顶点数目
    this.vertices = vertices;
    //Defining an array which can 
    //hold LinkedLists equal to the number of vertices in the graph
    //存储顶点对应的边的邻接表
    this.list = [];
    for (let i = 0 ; i < this.vertices ;i++) {
      const temp = new LinkedList();
      this.list.push(temp);
    }
  }

  printGraph() {
    console.log(">>Adjacency List of Directed Graph<<");
    var i;
    for (i = 0; i < this.list.length; i++) {
      console.log("|" + (i) + "| => ");
      let temp = this.list[i].getHead();
      while (temp != null) {
        console.log("[" + (temp.data) + "] -> ");
        temp = temp.nextElement;
      }
      console.log("end ");
    }
  }
  addEdge(source,destination) {
    if (source < this.vertices && destination < this.vertices)
      this.list[source].insertAtHead(destination);
  }
}

describe('LinkedList test',()=>{
  it('LinkedList insertAtHead test',()=>{
    const list = new LinkedList();
    expect(list.head).to.equal(null);
    list.insertAtHead(10);
    expect(list.head.data).to.equal(10);
    list.insertAtHead(20);
    expect(list.head.data).to.equal(20);
  });

  it('LinkedList isEmpty test',()=>{
    const list = new LinkedList();
    expect(list.isEmpty()).to.equal(true);
    list.insertAtHead(10);
    expect(list.isEmpty()).to.equal(false);
  });
  it('LinkedList printList test',()=>{
    const list = new LinkedList();
    expect(list.printList()).to.equal(false);
    list.insertAtHead(10).insertAtHead(20).insertAtHead(30);
    expect(list.printList()).to.equal(true);
  });
});

let g = new Graph(4);
g.addEdge(0, 1);
g.addEdge(0, 2);
g.addEdge(1, 3);
g.addEdge(2, 3);
g.printGraph();

从以上代码可以看出,我们在Graph中使用了LinkedList数据结构,因此,如果熟练掌握了基础的数据结构,使用起来还是比较容易理解的;

我们为这个Graph添加了两个重要方法,

addEdgeprintGraph一个用于构建整个Graph,另一个用于查看我们构建的情况是否正确。

我们以后再在这个类的基础上学习Graph的Breadth-First Search(BFS)Depth-First Search (DFS)。

本文水平有限,如果发现有啥问题,希望能提出来一起讨论。

Union Find 数据结构和算法理解

更新于 2023.05.11 2分钟阅读 2 评论 15 推荐

    作者:

最近开始学习一些不那么简单的算法,比如上一篇中的Graph就是一种不那么容易理解和学习的数据结构,今天来学习另一种数据结构和算法。

Union-Find这种算法的有哪些特点呢?

1、用最少的代码解决相对复杂的问题,而且时间和空间复杂度还相当优异。

2、这种算法能解决类似问题,比如:给定一组顶点和边,找出哪些组件属于同一个组,及存在多少个这样的组?

3、这种算法的另一个名称:"Disjoint Set"。

Union-Find算法的核心操作有哪些?

1、基本思想是使用数组(或类数组)数据结构模拟森林(一组树)。数组的索引代表节点。数组的值代表每个节点的父节点。

2、例如,如果 array[i] == i,则表示节点 i 是该组的父节点。如果 array[i] == j 且 i != j,则表示节点 i 指向节点 j,即节点 j 是节点 i 的父节点。

3、所以要找到节点 i 的父节点,我们可以从索引 i 向上遍历,直到遇到 array[j] == j,这意味着我们找到了 i 所在组的父节点。

4、总结下来,此算法有两个核心操作,那就是find以及union。

require("@fatso83/mini-mocha").install();
const { expect } = require('chai');
class UF {
  constructor(N){
    //存储父节点
    this.parent = Array.from({ length: N }, (_, i) => i);
    //代表所属的组有多少个元素
    this.count = new Array(N).fill(1);
  }

  /**
   * 寻找一个节点的最终父节点
   */
  find(x) {
    if (this.parent[x]!==x) {
      this.parent[x] = this.find(this.parent[x]);
    }
    return this.parent[x]
  }

  union(x, y){
    const xp = this.find(x), yp = this.find(y);
    if (xp == yp) return;
    if (this.count[xp] < this.count[yp]) {
      //说明yp节点是父节点,需要合并到yp节点所属的组中
      this.parent[xp] = yp;
      this.count[yp] += this.count[xp];
    } else {
      //合并到xp节点所属的组中
      this.parent[yp] = xp;
      this.count[xp] += this.count[yp];
    }
  }

  group() {
    return this.parent.filter((item,index)=>item==index).length;
  }
}

/**
 * 增加了一个单元测试
 */
describe('test UF', function(){
  
  it('test union of uf',()=>{
    const uf = new UF(10);
    uf.union(1,3);
    uf.union(2,3);
    uf.union(3,4);
    uf.union(7,8);
    expect(uf.group()).to.equal(6);
  });
  it('test find of uf',()=>{
    const uf = new UF(10);
    uf.union(1,3);
    uf.union(2,3);
    uf.union(3,4);
    uf.union(7,8);
    expect(uf.find(4)).to.equal(1);
    expect(uf.find(8)).to.equal(7);
  });
});

下面再来看看另一个例子:

const countComponents = function(n, edges) {
  const UF = Array.from({ length: n }, (_, i) => i);
  edges.forEach(([e1, e2]) => union(e1, e2));
  return UF.filter((c, i) => c == i).length;

  function union(c1, c2) {
    const p1 = find(c1), p2 = find(c2);
    if (p1 != p2) UF[p1] = p2;
  }

  function find(c) {
    if (c != UF[c]) UF[c] = find(UF[c]);
    return UF[c];
  };
};

这里例子的作用其实就是在一个无向图中,如果节点连接在一起算一个组件的话,那么这个方法就是计算出有多少个组件?大家认真想想,是不是和webpack中打包的时候,计算有多少个Chunck很像,也许webpack就是这样计算的。哈哈,猜的,如果有知道的,欢迎讨论。

这里再用另外一个例子来解释这个算法的作用:

require("@fatso83/mini-mocha").install();
const { expect } = require('chai');
class UF {
  constructor(N){
    //存储父节点
    this.parent = Array.from({ length: N }, (_, i) => i);
    //代表所属的组有多少个元素
    this.count = new Array(N).fill(1);
  }

  /**
   * 寻找一个节点的最终父节点
   */
  find(x) {
    if (this.parent[x]!==x) {
      this.parent[x] = this.find(this.parent[x]);
    }
    return this.parent[x]
  }

  union(x, y){
    const xp = this.find(x), yp = this.find(y);
    if (xp == yp) return;
    if (this.count[xp] < this.count[yp]) {
      //说明yp节点是父节点,需要合并到yp节点所属的组中
      this.parent[xp] = yp;
      this.count[yp] += this.count[xp];
    } else {
      //合并到xp节点所属的组中
      this.parent[yp] = xp;
      this.count[xp] += this.count[yp];
    }
  }

  group() {
    return this.parent.filter((item,index)=>item==index).length;
  }
}

const arrs = ["tars","rats","arts","star"];
//比较两个字符串是否相似
const isSimilar = (str1, str2)=>{
  const obj = {};
  let counter = 0;
  for (let i = 0 ; i < str1.length ; i++) {
    if (str1[i] !== str2[i]) {
      counter++;
      obj[str1[i]] = str2[i];
    }
  }
  return (counter === 2 && oppositeObj(obj))? true : false
}

const oppositeObj = (obj)=>{
  const keys = Object.keys(obj);
  const length = keys.length;
  if (length!=2){
    return false;
  }
  if (obj[keys[0]] !== keys[1]) {
    return false;
  }
  return true;
}

describe('pars-keys test',()=>{
  it('pars-keys obj={} test',()=>{
    const obj = {};
    const result = oppositeObj(obj);
    expect(result).to.equal(false);
  });
  it('pars-keys obj not two keys test',()=>{
    let obj = {
      'a': 'c'
    };
    const result = oppositeObj(obj);
    expect(result).to.equal(false);
    let obj1 = {
       'a': 'c',
       'b': 'b',
       'c':'a'
    };
    const result1 = oppositeObj(obj1);
    expect(result1).to.equal(false);
  });
  it('pars-keys obj has two key test',()=>{
    let obj = {
      'a':'c',
      'b':'c'
    }
    const result = oppositeObj(obj);
    expect(result).to.equal(false);
    let obj1 = {
      'a':'c',
      'c': 'a'
    }
    const result1 = oppositeObj(obj1);
    expect(result1).to.equal(true);
  });
});
describe('similar string group test',()=>{
    it('UF group test',()=>{
        const uf = new UF(4)
        for (let i = 0 ; i < arrs.length; i++) {
          for (j = i+1; j < arrs.length; j++) {
            if (isSimilar(arrs[i], arrs[j])) {
              uf.union(i,j);
            }
          }
        }
        expect(uf.group()).to.equal(2);  
    });
});
     

BST的简单实现

更新于 2023.04.12 20分钟阅读 3 评论 15 推荐

    作者:

最基础的数据结构往往会有最关键的作用,理解这些基础的数据结构能做什么,以及它们不能做什么对于我们理解数据结构的本质有关键的作用。

const Random = require("random-js").Random;
const MersenneTwister19937 = require("random-js").MersenneTwister19937;
const random = new Random(MersenneTwister19937.autoSeed());
class Node {
    constructor(data){
		this.data = data;
 		this.left = null;
		this.right = null;
	}
}

class BinarySearchTree {
	constructor(){
		this.root = null;
	}
	// function to be implemented
    // insert(data)
	insert(data) {
		let newNode = new Node(data);
		if (this.root === null) {
			this.root = newNode;
 		} else {
 			this.insertNode(this.root,newNode);
 		}
	}
	insertNode(node, newNode) {
		if (newNode.data < node.data) {//insert left tree
			if (node.left === null){
				node.left = newNode;
 			} else {
 				this.insertNode(node.left, newNode);
 			}
 		} else {//greater than value, insert right tree
 			if (node.right === null) {
				node.right = newNode;
 			} else {
 				this.insertNode(node.right, newNode);
 			}
 		}
	}
    // remove(data)
    remove(data){
		this.root = this.removeNode(this.root,data);
	}   
	removeNode(node,data){
		if (node === null){
			return null;
 		}else if (data < node.data) {
			node.left = this.removeNode(node.left, data);
			return node;
 		}else if (data > node.data){
			node.right = this.removeNode(node.right, data);
			return node;
 		} else {
 			//没有左右节点
			if (node.left === null && node.right === null){
				node = null;
				return null;
 			}
			//has right no left
			if (node.left === null){
				node = node.right;
				return node;
 			}
 			// has left no right
 			if (node.right === null){
				node = node.left;
				return node;
 			}
 			// both left and right
 			let aux = findMinNode(node.right);
			node.data = aux.data;
			node.right = this.removeNode(node.right,aux.data);
			return node;
 		}
	}
 	//recursive to find minimum node
    findMinNode(node){
        // if left of a node is null
        // then it must be minimum node
        if(node.left === null) {
            return node;
        } else {
            return this.findMinNode(node.left);
	    }  
	}  


	inorder(node){
		if (node!== null){
			this.inorder(node.left);
			console.log(node.data);
			this.inorder(node.right);
 		}
	}
    inorderiterator(node) {
      const stack = [];
      const reversed = [];
      let curr = node;
      while(stack.length || curr) {
        while(curr) {
          stack.push(curr);
          curr = curr.left;
        }
        curr = stack.pop();
        reversed.push(curr.data);
        curr = curr.right;
      }
      return reversed;
    }

	preorder(node){
    	if(node !== null){
        	console.log(node.data);
        	this.preorder(node.left);
        	this.preorder(node.right);
    	}
	}
    preorderiterator(node) {
      const stack = [node];
      const traversed = [];
      let curr;
      while(stack.length) {
        curr = stack.pop();
        traversed.push(curr.data);
        if (curr.right) {
          stack.push(curr.right);
        }
        if (curr.left) {
          stack.push(curr.left);
        }
      }
      return traversed;
    }

    preorderiterator1(node) {
      const stack = [];
      const traversed = [];
      let curr = node;
      while(stack.length || curr) {
        while(curr){
          traversed.push(curr.data);
          stack.push(curr);
          curr = curr.left;
        }
        curr = stack.pop();
        curr = curr.right;
      }
      return traversed;
    }
  
	postorder(node){
      if(node !== null){
        this.postorder(node.left);
        this.postorder(node.right);
        console.log(node.data);
      }
	}

    postorderiterator(node) {
      const s1 = [node];
      const s2 = [];
      const reversed = [];
      let curr;
      while(s1.length) {
        curr = s1.pop();
        if (curr.left) {
          s1.push(curr.left);
        }
        if (curr.right) {
          s1.push(curr.right);
        }
        s2.push(curr);
      }
      while(s2.length) {
        curr = s2.pop();
        reversed.push(curr.data);
      }
      return reversed;
    }
  
	getRootNode(){
    	return this.root;
	} 
	search(node, data){
   		// if trees is empty return null
    	if(node === null)
        	return null;
 
    	// if data is less than node's data
    	// move left
    	else if(data < node.data)
        	return this.search(node.left, data);
 
    	// if data is more than node's data
    	// move right
    	else if(data > node.data)
        	return this.search(node.right, data);
 
    	// if data is equal to the node data
    	// return node
    	else
        	return node;
	}    
 
    // Helper function
    // findMinNode()
    // getRootNode()
    // inorder(node)
    // preorder(node)              
    // postorder(node)
    // search(node, data)
}

const bst = new BinarySearchTree();
const newBet = new BinarySearchTree();
for (let i = 0 ; i< 20; i++){
	const value = random.integer(1, 100);
	newBet.insert(value);
}
console.log('inorder newBet');
//console.log(newBet.inorder(newBet.root));
bst.insert(10);
bst.insert(12);
bst.insert(8);
bst.insert(15);
bst.insert(9);
bst.insert(14);
console.log('inorder');
console.log(bst.inorder(bst.root));
//console.log('inorderiterator');
//console.log(bst.inorderiterator(bst.root));
console.log('preorder');
console.log(bst.preorder(bst.root));
//console.log('preorderiterator1');
//console.log(bst.preorderiterator1(bst.root));
console.log('postorder');
console.log(bst.postorder(bst.root));
console.log('postorderiterator');
console.log(bst.postorderiterator(bst.root));
bst.remove(12);

 

 

Queue BFS 初步学习

发布于 2023.04.11 1分钟阅读 0 评论 5 推荐

    作者:

最近在学习数据结构的时候,学到了树的广度优先搜索可以使用队列来实现,今天尝试着写一下:

定义一棵树:

let tree = {
	"10": {
		value: "10",
		left: "4",
		right: "17",
	},
	"4": {
		value: "4",
		left: "1",
		right: "9",
	},
	"17": {
		value: "17",
		left: "12",
		right: "18",
	},
	"1": {
		value: "1",
		left: null,
		right: null,
	},
	"9": {
		value: "9",
		left: null,
		right: null,
	},
	"12": {
		value: "12",
		left: null,
		right: null,
	},
	"18": {
		value: "18",
		left: null,
		right: null,
	},
};
//定义搜索方法,关键有两点:
// 1、进入队列的时候一定是层序,
// 2、同时访问结束的节点,一定要出队列
const BreadthFirstSearch = (tree, rootNode, searchValue)=>{
  //定义一个队列
  let queue= [];
  queue.push(rootNode);
  while(queue.length>0) {
    //拿出队列的第一个元素赋值给currentNode
    let currentNode = queue[0];
    console.log('currentValue is:',currentNode.value);
    if (currentNode.value === searchValue ) {
      console.log('Found it');
      return;
    }
    //进入队列的顺序一定是层序
    if (currentNode.left !== null) {
      queue.push(tree[currentNode.left]);
    }
    if (currentNode.right !== null) {
      queue.push(tree[currentNode.right]);
    }
    //访问过的元素直接出队列
    queue.shift();
  }
  console.log('sorry not such node');
}
BreadthFirstSearch(tree, tree[10], "42");
BreadthFirstSearch(tree, tree[17], "18");

以前觉得数据结构很难,实际上学习起来的话,抓住关键的点,还是比较容易理解的额

RxJs基础

Rxjs基础

我们知道在Rxjs中有几类基础组件,它们分别是:

1、Observables

2、Observers

3、Operators

4、Subjects

5、Schedulers

以上这些组件是Rxjs的核心构建块。我们需要理解它们如何协同工作以便提供强大的功能。一旦对它们有了更高层次的理解,使用起来将会得心应手。那么,让我们一一来看看它们。先来看一个例子:

//import { from } from "rxjs";
let from = require("rxjs").from;
let nums = [1, 2, 4, 34, 56, 789];
let numsObservable$ = from(nums); //Observable
let observer = {
  next: num => console.log(num),
  error: err => console.error(err),
  complete: () => console.log("Execution completed")
};
numsObservable$.subscribe(observer); //Observer subscribed

说明

1、从rxjs中导入必要的函数from

2、定义nums 是一个数字数组,然后将其转换为 Observable。

3、创建了一个名为numObservable$ 的 rxjs Observable,借助它可以将数组转换为Observable。注意在 Observables 末尾附加 $ 是一种命名约定。

4、当我们只创建一个 observable 时,什么都不会被执行或调用。要执行任何操作,您需要订阅 Observable。

5、numsObservable$ 订阅了一个名为observer的对象。注意,这个observer有 3 个属性,next、error 和 complete。一旦订阅,马上执行,因此订阅是执行的触发器

6、执行完成后,您可以在控制台中看到“执行完成”消息。

 

什么是Observable

Observable 是可以随时间到达的事件流或数据流。使用RxJS函数,您可以从几乎任何来源(事件、Web 服务、套接字数据等)创建 Observable。换句话说,Observable是未来数据的集合。

在上面的示例中,我使用 from 运算符从数组创建 Observable。同样,也有 fromEvent 操作符来创建 JavaScript 事件。

Observables 可以是Lazy或Cold。除非调用 subscribe 方法,否则它们不会激活任何producer。

下面的代码展示了如何使用 fromEvent 从点击事件创建一个 Observable。

import { fromEvent } from "rxjs";
let clicksObservable$ = fromEvent(document, "click");

 

什么是Observer

Observable 表示可被观察的数据源,任何对数据源感兴趣可以订阅 Observables。Observer是通过订阅,注册对数据流感兴趣的代码。(因此Observer应该是可执行的,是有行为的,比如成功会怎么样,失败会怎么样,什么时候算结束,结束后还需要执行啥?)

Observer提供了迭代Observable上的数据流(序列)。

import { Observer } from './types';
import { config } from './config';
import { hostReportError } from './util/hostReportError';
export const empty: Observer = {
  closed: true,
  next(value: any): void { /* noop */},
  error(err: any): void {
    if (config.useDeprecatedSynchronousErrorHandling) {
      throw err;
    } else {
      hostReportError(err);
    }
  },
  complete(): void { /*noop*/ }
};

以上是一个empty Observer,基本上,Observer有 3 种方法,next、error 和 complete。理解它们中的每一个很重要。

let observer = {
  next: num => console.log(num),
  error: err => console.error(err),
  complete: () => console.log("Execution completed")
};

next – 接受一个参数(值)。这是 Observable 正在推送/生成的数据。所以每次 observable 推送数据时 next(value) 都会收到该值。

error – 接受一个参数作为错误。如果Observer抛出错误,则执行此方法。

complete - 不接受任何参数。一旦 Observable 完成其操作/执行,此方法就会被执行。

 

什么是Operator

运算符是允许操纵 Observables 产生的值的函数。它们是每个人都喜欢使用 RxJS 的另一个原因。我将使用下面的代码示例向您解释运算符的概念。

import { from } from "rxjs";
import { map } from "rxjs/operators";
const nObservable$ = from([1, 2, 3, 5, 56, 567, 89]);
nObservable$.pipe(
  map(val => val * 2)
).subscribe(
  value => console.log(value)
);

上面的代码将存储在数组中的数字加倍。重要的是要注意 map 函数的使用,您可以将其与 Array.prototype.map() 相关联。正如您所注意到的,map 运算符放在管道方法中。

让我们看看另一个捕获鼠标点击并记录 x 坐标的示例。我将使用 pluck 运算符来执行此操作。

import { fromEvent } from "rxjs";
import { pluck } from "rxjs/operators";
const clicksObservable$ = fromEvent(document, "click");
clicksObservable$
  .pipe(pluck("clientX"))
  .subscribe({
    next: clientX => console.log(clientX),
    error: err => console.error(err),
    complete: () => console.log("Execution completed")
  });

rxjs/operators 中有很多 Operator。可以说,学习Rxjs的使用就是学习这些Operator,但是想要深入理解Rxjs还是需要深入学习基本的概念。

 

什么是Rxjs中的Subject

Subject 将数据推送到多个Observer,Observable一次将数据推送给单个Observer。 Subject 允许您发出新值,这与订阅 Observables 的情况下被动等待数据不同。

Subject用于多播,Observable用于单播。

如果您是 RxJS 的新手,请稍事休息,喝杯茶、咖啡或任何您喜欢的东西,然后查看下面的代码示例以从高层次上理解Subject。

如果您在 GitHub 上查看 Subject 的源代码,可以看到允许我们在subject调用.next,.error,.complete等。

让我们看看下面的例子,我只是向你展示了调用 next(),你也可以在需要时调用 error(err) complete()

import { Subject } from "rxjs";
const mySubject$ = new Subject();
mySubject$.subscribe({
  next : (num) => console.log("ObserverA: " + num)
});
mySubject$.subscribe({
  next : (num) => console.log("ObserverB: " + num)
});
mySubject$.next(5);
mySubject$.next(101);

输出如下:

ObserverA: 5
ObserverB: 5
ObserverA: 101
ObserverB: 101

如上所示,您可以使用 next(value) 向多个Observer发出下一个值。当值的发出完成时,您可以调用 complete() 如下。

mySubject$.complete();

 

什么是Rxjs中的Schedulers

Schedulers是 rxjs 构建块的另一个重要部分。Schedulers控制订阅何时开始以及何时传递通知。有几个开箱即用的Schedulers,如 queueScheduler、asyncScheduler、asapScheduler 等。

在一个简单的项目中,您甚至可能不需要触摸这部分,但是,当您需要对上下文进行精确控制时,它很有用。看下面的例子:

import { Observable, asyncScheduler } from 'rxjs';
import { observeOn } from 'rxjs/operators';
const observable = new Observable((subscriber) => {
  subscriber.next(1);
  subscriber.next(2);
  subscriber.next(3);
  subscriber.complete();
}).pipe(
  observeOn(asyncScheduler)
);
console.log('just before subscribe');
observable.subscribe({
  next(x) {
    console.log('got value ' + x)
  },
  error(err) {
    console.error('something wrong occurred: ' + err);
  },
  complete() {
     console.log('done');
  }
});
console.log('just after subscribe');

输出如下:

just before subscribe
just after subscribe
got value 1
got value 2
got value 3
done

总结:

以上就是对Rxjs中的基本构建块有了大致的理解和分析,记录一下方便以后查阅。

 

AVLTree简单实现

发布于 2023.04.06 17分钟阅读 0 评论 5 推荐

    作者:

今天突然感觉AVL树在Tree这种数据结构中有比较重要的作用,特别是理解如何将不平衡的树转化为平衡树的左旋和右旋操作是理解和学习AVL树的基础。

先上代码:

//定义树的基本节点
const Node = function(item){
	//基本元素
	this.item = item;
 	//节点的高度
 	this.height = 1;
	this.left = null;
	this.right = null;
}
const AVLTree = function(){
	//定义树的根节点
	let root = null;
	this.height = (Node)=>{
		if (Node == null){
			return 0;
 		}
		return Node.height;
	}
	this.rightRotate = (y)=>{
		
		let x = y.left;
		let T2 = x.right;
		x.right = y;
 		y.left = T2;
		y.height = Math.max(this.height(y.left), this.height(y.right))+1;
		x.height = Math.max(this.height(x.left), this.height(x.right))+1;
		return x;
	}
	this.leftRotate = (x)=> {
        let y = x.right;
        let T2 = y.left;
        y.left = x;
        x.right = T2;
        y.height = Math.max(this.height(y.left), this.height(y.right)) + 1;
        x.height = Math.max(this.height(x.left), this.height(x.right)) + 1;
        return y;
    }
 	this.getBalanceFactor = (Node)=>{
    	if (Node == null) {
            return 0;
        }
        return this.height(Node.left) - this.height(Node.right);
    }
	const insertNodeHelper = (node, item)=>{
    	//three case 
    	//根节点为空
    	if (node == null) {
      		return new Node(item);
    	}
    	//搜索需要插入的位置
    	if (item < node.item) {
      	//插入左子树
      		node.left = insertNodeHelper(node.left, item);
    	} else if (item > node.item) {
      		node.right = insertNodeHelper(node.right, item);
    	} else {
      	//这里返回是处理递归的结束
      		return node;
    	}
    	//处理插入的情况
    	node.height = 1 + Math.max(this.height(node.left), this.height(node.right));
    	let balanceFactor  = this.getBalanceFactor(node);
    	if (balanceFactor > 1) {
      		if (item < node.left.item) {//说明是插入左子树的左子树导致不平衡,因此需要右旋
        		return this.rightRotate(node);
      		} else if (item > node.left.item) {//说明是插入左子树的右子树导致不平衡,需要先左旋,再右旋
        		node.left = this.leftRotate(node.left);
        		return this.rightRotate(node);
      		}
    	} 
    	if (balanceFactor < -1) {
      		if (item > node.right.item) {
        		return this.leftRotate(node);
      		} else if (item < node.right.item) {
        		node.right = this.rightRotate(node.right);
       			return this.leftRotate(node);
     		}
    	}

    	return node;
  	}

  	this.insertNode = (item) => {
    	// console.log(root);
    	root = insertNodeHelper(root, item);
  	}

  	const deleteNodeHelper = (root, item)=>{
    	if (root == null){
      		return root;
    	}
    	if (item < root.item) {
      		root.left =  deleteNodeHelper(root.left, item);
    	} else if (item > root.item) {
      		root.right = deleteNodeHelper(root.right, item);
    	} else {
      		//只有一个节点
      		if(root.left == null || root.right == null) {
        		let temp = null;
        		if (temp == root.left) {
          			temp = root.right;
        		} else {
          			temp = root.left;
        		}
        		if (temp == null) {
          		//全部左右子节点为空
          			temp = root;
          			root = null;
          		//根节点为空
        		} else{
          		//根节点赋值为temp子节点
          			root = temp;
        		}
      		} else {
        		// 获取右子树最小的节点
        		let temp  = this.nodeWithMimumValue(root.right);
        		root.item = temp.item;
        		root.right = deleteNodeHelper(root.right, temp.item);
      		}
    	}
    	if (root == null) {
      		return root;
    	}

    	root.height = Math.max(this.height(root.left), this.height(root.right)) + 1;
    	let balanceFactor = this.getBalanceFactor(root);
    	if (balanceFactor > 1) {
     	 	if (this.getBalanceFactor(root.left) >=0) {
        		//右旋
        		return this.rightRotate(root);
      		} else {
        		root.left = this.leftRotate(root.left);
        		return this.rightRotate(root);
      		}
    	} 
    	if (balanceFactor < -1) {
      		if (this.getBalanceFactor(root.right) <= 0) {
        		//左旋
        		return this.leftRotate(root);
      		} else {
        		root.right = this.rightRotate(root.right);
        		return this.leftRotate(root);
      		}
    	}
    	return root;
  	}
 	this.deleteNode = (item) => {
    	// console.log(root);
    	root = deleteNodeHelper(root, item);
  	}

  	this.nodeWithMimumValue = (node) => {
    	let current = node;
    	while (current.left !== null){
      		current = current.left;
    	}
    	return current;
  	}

  	this.preOrder = () => {
    	preOrderHelper(root);
  	}
  
  	const preOrderHelper = (node) => {
    	if (node) {
      		console.log(node.item);
      		preOrderHelper(node.left);
      		preOrderHelper(node.right);
    	}
  	}
}

let tree = new AVLTree();
tree.insertNode(33);
tree.insertNode(13);
tree.insertNode(53);
tree.insertNode(9);
tree.insertNode(21);
tree.insertNode(61);
tree.insertNode(8);
tree.insertNode(11);
tree.preOrder();
tree.deleteNode(13);
console.log("After Deletion: ");
tree.preOrder();

平衡二叉树的代码主要包括插入和删除两个基本操作,而这两个基本操作最关键的是理解左旋和右旋。关于左旋和右旋的知识,我们以后再慢慢分享。

理解CKEditor5的schema

更新于 2023.03.13 0分钟阅读 0 评论 5 推荐

    作者:

我们知道,CKEditor5是一个用MVC架构设计的富文本编辑器。

如上图所示,三层分别是:Model,Controller, View

首先,第一个问题是schema属于那一层?

经过官方文档的初步学习,我们可以看到:

editor.model.schema;                // -> The model's schema.

因此,我们可以得出结论:schema属于模型层:

其次我们需要理解的是schema的作用是什么?

  • Where a node is allowed or disallowed (e.g. paragraph is allowed in $root, but not in heading1).
  • What attributes are allowed for a certain node (e.g. image can have the src and alt attributes).
  • Additional semantics of model nodes (e.g. image is of the “object” type and paragraph of the “block” type).

官网上是这样介绍的,我自己的理解就是:

  • 模型节点可放位置或不可放置位置。比如模型元素paragraph可放置$root下,却不可放置heading1下。
  • 指定模型节点允许属性和禁止属性。比如模型元素image允许属性src和alt,禁止其他属性。
  • 模型节点附加语意。比如模型节点image是一个对象类型(整体对待)而p是一个块类型(可包含元素,且元素可以分割)。

再次我们思考一下schema的信息有啥作用:

  • What happens with the pasted content and what is filtered out (note: in case of pasting the other important mechanism is the conversion. HTML elements and attributes which are not upcasted by any of the registered converters are filtered out before they even become model nodes, so the schema is not applied to them; the conversion will be covered later in this guide).
  • To which elements the heading feature can be applied (which blocks can be turned to headings and which elements are blocks in the first place).
  • Which elements can be wrapped with a block quote.
  • Whether the bold button is enabled when the selection is in a heading (and whether the text in this heading can be bolded).
  • Where the selection can be placed (which is — only in text nodes and on object elements).
  • etc.

最后,我们需要理解的是schema的定义包括哪些具体内容:

这里可以参考:SchemaItemDefinition,这里说说常用的属性

  1. allowIn : String | Array<String> 定义的模型节点可以作为哪些模型节点的子节点。白话来说就是可以放在哪些节点下面。
     
  2. allowWhere :  String | Array<String> 定义的模型节点从目标模型节点继承allowIn属性。通俗解释就是目标模型节点可以放置的位置,我们定义的模型节点就可以放置。
     
  3. allowAttributes : String | Array<String> 定义的模型节点允许包含哪些属性。通俗理解就是模型节点允许哪些属性。
     
  4. allowAttributesOf: String | Array<String> 定义的模型节点从目标模型节点继承allowAttributes属性。 通俗解释就是目标模型节点允许哪些属性,我们定义的模型节点就允许哪些属性。
     
  5. allowChildren: String | Array<String> 定义的模型节点可以包含哪些模型子节点。通俗理解就是模节点内部可以放置哪些模型子节点。
     
  6. allowContentOf : String | Array<String> 定义的模型节点从目标模型节点继承allowChildren属性。通俗理解就是目标模型节点可以包含哪些子模型节点,我们定义的模型节点就可以包含那些子模型节点。
     
  7. isLimit :  设置为 true 时,元素内的所有操作只会修改内容,不会影响元素本身。也就是说该元素无法使用回车键拆分,无法在元素内部使用删除键删除该元素(如果把整个 Molde 理解为一张网页,Limit Element 就相当于 iframe);
     
  8. isObject :  是否为一个完整对象(完整对象会被整体选中,无法使用回车拆分,无法直接编辑文本);
     
  9. isBlock : 是否为块元素,类似 HTML 中的块元素;
     
  10. isInline : 是否为行内元素。但对于 <a> <strong> 这些需要即时编辑的行内标签,在编辑器中以文本属性来区分,所以 isInline 只用于独立的元素,即 isObject 应设置为 true;
schema.register( 'newCodeBlock', {
         allowWhere: '$block',
         allowChildren: '$text',
         isBlock: true,
         allowAttributes: [ 'language' ]
 } );

用上面的代码作为例子,我们可以得出,schema:newCodeBlock可以放置在任何$block可以放置的地方,因为它继承了$block,newCodeBlock里面只能放置文本,而且是一个块元素,允许有属性language

 

总结

model的schema的作用就是:

定义允许的模型结构:——模型元素如何嵌套

定义允许的属性:——元素和文本节点可能允许和不允许的属性

其他特征:——内联还是块,对外部行为 的原子性反应等。

Injector框架理解(二)

今天我们继续学习didi这个依赖注入框架,上一节我们知道了怎么定义组件,怎么定义模块,怎么通过Injector这个类来载入模块和获取组件。

今天我们来学习如何重用模块:

我们首先定义好组件和模块:

const {Injector} = require('didi');
class FooType {
    constructor() {
      this.name = 'foo';
    }
}

function barFactory(foo) {
    return foo;
}

const module1 = ({
    foo: [ 'type', [ FooType ] ],
    bar: [ 'factory', [ 'foo', barFactory ] ],
});
const injector1 = new Injector([module1]);
console.log('injector1:',injector1);
console.log(injector1.get('foo')===injector1.get('bar'))

const injector2 = new Injector([module1]);
console.log('injector2:',injector2);
console.log(injector2.get('foo')===injector2.get('bar'))

console.log(injector1.get('foo')===injector2.get('foo'))

说明一下,这里的代码,首先我们定义了两个组件,一个是FooType,另一个是工厂方法barFactory返回的foo组件,其实看模块的定义实际上就是FooType的具体实例。然后我们将这两个组件定义为模块,这个模块的定义是可以重用的,用不同的依赖注入加载器加载后,它们的实例存在于不同的加载器中,虽然实例不同,但是它们的作用是相同的。

我们先看看运行的效果:

可以看出,这两个加载器中的实例是一模一样的。因此,定义好了模块以后是可以重用的。这里也存在一个问题,那就是组件的定义:

模块中组件的定义是这样的:

 const preModule =({
    foo: [
      'factory',
      function() {
        return 'foo-value';
      }
    ],
    bar: ['value', 'bar-value'],
    bub: ['type', BubType],
    buz: ['type', BuzType]
});
const curModule = ({
    foo: [ 'type', [ FooType ] ],
    bar: [ 'factory', [ 'foo', barFactory ] ],
});
//注意对于factory类型的组件,存在两种不同的定义方法,它们的第二个参数可以是一个函数
//也可以是一个数组,从后面可以看出,这是一个配置有依赖组件的数组
//同时,对于type类型的组件,第二个参数可以是一个类,也可以是一个数组,而数组中的值是一个类
//以上这样设计的原因,我们会在后文进行分析

依赖注入的方式,数组定义: 

const {Injector} = require('didi');
class FooType {
    constructor() {
      this.name = 'foo';
    }
}

function barFactory(foo) {
    return foo;
}

const module1 = ({
    foo: [ 'type', [ FooType ] ],
    bar: [ 'factory', [ 'foo', barFactory ] ],
});
const injector3 = new Injector([ module1 ]);
function fn(foo, bar) {
    console.log('foo0:',foo);
    console.log('foo1:',injector3.get('foo'));
    console.log('bar0:',bar);
    console.log('bar1:',injector3.get('bar'));
}
const annotatedFn = [ 'foo', 'bar', fn ];

injector3.invoke(annotatedFn);

我们看运行结果:

可以知道,依赖注入的配置可以是一个数组,而数组的最后一个就是组件函数,前面的所有参数就是这个函数组件依赖的组件,比如这里的fn组件依赖于foo和bar组件可以这样配置。

const annotatedFn = [ 'foo', 'bar', fn ];

好了,今天我们主要学习并理解了didi框架的模块重用以及数组形式的组件依赖注入配置

 

Injector框架理解(一)

在可测试js的框架学习过程中,我们不断提到了依赖注入,依赖注入使用最多的就是在java中的spring,为了在js中使用依赖注入,我们今天学习另一个不那么常用的框架:didi

首先看看安装命令

npm install didi

这个时候,我看到项目下的package.json

{
  "name": "babelwebpack",
  "version": "1.0.0",
  "description": "babel webpack ingegraty",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "pack": "webpack",
    "publish": "webpack-dev-server --output-public-path=/dev/"
  },
  "keywords": [
    "babel",
    "webpack"
  ],
  "author": "hk",
  "license": "ISC",
  "devDependencies": {
    "@babel/cli": "^7.18.10",
    "@babel/core": "^7.18.13",
    "@babel/preset-env": "^7.18.10",
    "webpack": "^5.74.0",
    "webpack-cli": "^4.10.0",
    "webpack-dev-server": "^4.10.1"
  },
  "dependencies": {
    "babel-loader": "^8.2.5",
    "didi": "^9.0.0"
  }
}

我再贴出我的webpack.config.js

var path = require('path');

module.exports = {
   entry: {
      /** 源代码根路径 */
      app: './src/main.js'
   },
   output: {
      /** 这里配置的是webpack的打包输出路径 */
      path: path.resolve(__dirname, 'dev'),
      filename: 'main_bundle.js'
   },
   mode:'development',
   module: {
      rules: [
         {
            test: /\.js$/,
            include: path.resolve(__dirname, 'src'),
            loader: 'babel-loader',
            options: {
               presets: ['@babel/preset-env']
            }
         }
      ]
   },
   devServer: {
      static: {
         /**这里配置的开发服务器的根路径 */
         directory: path.join(__dirname, './public'),
      },
      hot: true,
      compress: true,
      port: 9999,
   },
};

好了,我要开始使用didi

首先我定义三个组件:

const {Injector} =require('didi');
/*
 * 定义一个Car组件,这个Car组件依赖于engine组件
 */
function Car(engine) {
    this.start = function() {
      engine.start();
    };
}
 
/**
 * 定义一个Engine组件,同时这个Engine组件依赖于power组件
 */ 
function createPetrolEngine(power) {
    return {
      start: function() {
        console.log('Starting engine with ' + power + 'hp');
      }
    };
}
  
// define a (didi) Car module,定义一个CarModule,这个模块由三个组件构建
// it declares available components by name and specifies how these are provided
const carModule = {
    //请求Car组件的时候,依赖注入器会调用Car的构造函数来创建组件
    // asked for 'car', the injector will call new Car(...) to produce it
    'car': ['type', Car],
	//请求Engine组件的时候,依赖注入器会调用工厂方法来创建组件
    // asked for 'engine', the injector will call createPetrolEngine(...) to produce it
    'engine': ['factory', createPetrolEngine],
	//请求power组件,依赖注入器会通过复制一个value值来创建。
    // asked for 'power', the injector will give it number 1184
    'power': ['value', 1184] // probably Bugatti Veyron
};
  
// instantiate an injector with a set of (didi) modules
const injector = new Injector([
    carModule
]);

//使用依赖注入器来检索组件并调用组件的API 
// use the injector API to retrieve components
injector.get('car').start();

//使用依赖注入器的invoke函数来注入相应的组件 
// ...or invoke a function, injecting the arguments
injector.invoke(function(car) {
    console.log('started', car);
    car.start();
});

大家注意以上代码,我们需要指出几个概念,首先理解什么是组件,什么是模块,什么是依赖注入加载器

组件可以是一个类,一个函数,甚至是一个值,在以上的代码中,有一个叫做Car的组件,一个叫做Engine的组件,以及一个叫做power的组件,而这三个组件又构成了一个模块,叫做carModule,这个模块通过依赖注入加载器加载以后,我就可以使用这些组件啦。

使用组件的方式有两种,第一种通过调用get()方法拿到组件以后,再使用。

另一种是通过调用invoke方法,通过一个函数来获取依赖注入参数,然后再使用。大家可以运行代码试试效果,不过呢,这里有个小问题就是,只有在编辑状态的时候才能运行代码,而且只支持noderequire引入,不知道这个部分以后是否可以优化。

 

我们再举一个例子

const {Injector} =require('didi');
class BubType{
    constructor() {
      this.name = 'bub-value';
    }
}

function BuzType() {
    this.name = 'buz-value';
}

/**
  * 在一个模块定义了四个组件
  */
const newInjector = new Injector([
    {
      foo: [
        'factory',
        function() {
          return 'foo-value';
        }
      ],
      bar: ['value', 'bar-value'],
      bub: ['type', BubType],
      buz: ['type', BuzType]
    }
]);

console.log('foo:',newInjector.get('foo'));
console.log('bar:',newInjector.get('bar'));
console.log('bub:',newInjector.get('bub'));
console.log('buz:',newInjector.get('buz'));

在这里,我们可以得出一个小小的结论就是:Injector的构造函数是一个数组对象,数组中的每个对象实际上是会生成一个模块,同时产生组件的方式有三类:分别对应三种不同的key值,它们是factory,type,value

这里我们大概理解了基本的用法了吧,其实这个简单的框架为我们组织前端的代码提供了很好的组织方式,能够让我们的代码更加模块化。欢迎大家一起讨论学习。

Hibernate映射计算的属性

发布于 2023.01.04 43分钟阅读 0 评论 5 推荐

    作者:

在一般的实体映射中,一般都是一个属性对应数据库的某一列。今天我们来看看如何映射通过其他属性计算出来的属性。好了先看看具体的两个领域类:

新建一个Account.java

@Entity(name = "Account")
@Table(name = "account")
public class Account {

    @Id
    private Long id;

    @ManyToOne(fetch = FetchType.LAZY)
    private User owner;

    private String iban;

    private long cents;

    private double interestRate;

    private Timestamp createdOn;

    @Transient
    private double dollars;

    @Transient
    private long interestCents;

    @Transient
    private double interestDollars;

    public Account() {
    }

    public Account(
            Long id, User owner, String iban,
            long cents, double interestRate, Timestamp createdOn) {
        this.id = id;
        this.owner = owner;
        this.iban = iban;
        this.cents = cents;
        this.interestRate = interestRate;
        this.createdOn = createdOn;
    }

    @PostLoad
    private void postLoad() {
        this.dollars = cents / 100D;

        long months = createdOn.toLocalDateTime().until(
                LocalDateTime.now(),
                ChronoUnit.MONTHS)
                ;

        double interestUnrounded = (
                (interestRate / 100D) * cents * months
        ) / 12;

        this.interestCents = BigDecimal.valueOf(interestUnrounded)
                .setScale(0, BigDecimal.ROUND_HALF_EVEN)
                .longValue();

        this.interestDollars = interestCents / 100D;
    }

    public double getDollars() {
        return this.dollars;
//        return cents / 100D;
    }

    public long getInterestCents() {
        return this.interestCents;
//        long months = createdOn.toLocalDateTime().until(
//                LocalDateTime.now(),
//                ChronoUnit.MONTHS
//        );
//
//        double interestUnrounded = (
//                (interestRate / 100D) * cents * months
//        ) / 12;
//
//        return BigDecimal.valueOf(interestUnrounded)
//                .setScale(0, BigDecimal.ROUND_HALF_EVEN)
//                .longValue();
    }

    public double getInterestDollars() {
        return this.interestDollars;
//        return getInterestCents() / 100D;
    }
}

新建一个User.java

@Entity(name = "User")
@Table(name = "user")
public class User {
    @Id
    private Long id;

    private String firstName;

    private String lastName;

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getFirstName() {
        return firstName;
    }

    public void setFirstName(String firstName) {
        this.firstName = firstName;
    }

    public String getLastName() {
        return lastName;
    }

    public void setLastName(String lastName) {
        this.lastName = lastName;
    }

    @Override
    public String toString() {
        return "User{" +
                "id=" + id +
                ", firstName='" + firstName + '\'' +
                ", lastName='" + lastName + '\'' +
                '}';
    }
}

注意,这里我们在Account类中新增了三个属性,分别是:dollars,interestCents,interestDollars,而这些属性通过@Preload这个监听器方法来初始化,并且通过相应的get方法将属性暴露出来。

下面看看测试方法:

public class FormulaPreloadTest extends AbstractTest {
    @Override
    protected Class[] entities() {
        return new Class[]{
                Account.class,
                User.class
        };
    }

    @Test
    public void insertAccount() {
        doInJPA(entityManager -> {
            User user = new User();
            user.setId(1L);
            user.setFirstName("John");
            user.setFirstName("Doe");

            entityManager.persist(user);
            Account account = new Account(
                    1L,
                    user,
                    "ABC123",
                    12345L,
                    6.7,
                    Timestamp.valueOf(
                            LocalDateTime.now().minusMonths(3)
                    )
            );
            entityManager.persist(account);
        });

        doInJPA(entityManager -> {
            Account account = entityManager.find(Account.class, 1L);

            assertEquals(123.45D, account.getDollars(), 0.001);
            assertEquals(207L, account.getInterestCents());
            assertEquals(2.07D, account.getInterestDollars(), 0.001);
        });
    }
}

运行之后可以看看,我们可以直接拿到这些属性的值,验证通过:

日志信息如下:

[INFO] 2023-01-04 13:34:16 method: net.ttddyy.dsproxy.support.CommonsLogUtils.writeLog(CommonsLogUtils.java:23)----Name:DATA_SOURCE_PROXY, Connection:2, Time:0, Success:True, Type:Statement, Batch:False, QuerySize:1, BatchSize:0, Query:["drop table if exists account CASCADE "], Params:[]
[INFO] 2023-01-04 13:34:16 method: net.ttddyy.dsproxy.support.CommonsLogUtils.writeLog(CommonsLogUtils.java:23)----Name:DATA_SOURCE_PROXY, Connection:2, Time:0, Success:True, Type:Statement, Batch:False, QuerySize:1, BatchSize:0, Query:["drop table if exists user CASCADE "], Params:[]
[INFO] 2023-01-04 13:34:16 method: net.ttddyy.dsproxy.support.CommonsLogUtils.writeLog(CommonsLogUtils.java:23)----Name:DATA_SOURCE_PROXY, Connection:3, Time:0, Success:True, Type:Statement, Batch:False, QuerySize:1, BatchSize:0, Query:["create table account (id bigint not null, cents bigint not null, createdOn timestamp, iban varchar(255), interestRate double not null, owner_id bigint, primary key (id))"], Params:[]
[INFO] 2023-01-04 13:34:16 method: net.ttddyy.dsproxy.support.CommonsLogUtils.writeLog(CommonsLogUtils.java:23)----Name:DATA_SOURCE_PROXY, Connection:3, Time:1, Success:True, Type:Statement, Batch:False, QuerySize:1, BatchSize:0, Query:["create table user (id bigint not null, firstName varchar(255), lastName varchar(255), primary key (id))"], Params:[]
[INFO] 2023-01-04 13:34:16 method: net.ttddyy.dsproxy.support.CommonsLogUtils.writeLog(CommonsLogUtils.java:23)----Name:DATA_SOURCE_PROXY, Connection:3, Time:1, Success:True, Type:Statement, Batch:False, QuerySize:1, BatchSize:0, Query:["alter table account add constraint FKlijilgu3y8bx1rb3oirmqlw5k foreign key (owner_id) references user"], Params:[]
[INFO] 2023-01-04 13:34:16 method: org.hibernate.engine.transaction.jta.platform.internal.JtaPlatformInitiator.initiateService(JtaPlatformInitiator.java:52)----HHH000490: Using JtaPlatform implementation: [org.hibernate.engine.transaction.jta.platform.internal.NoJtaPlatform]
[INFO] 2023-01-04 13:34:16 method: net.ttddyy.dsproxy.support.CommonsLogUtils.writeLog(CommonsLogUtils.java:23)----Name:DATA_SOURCE_PROXY, Connection:4, Time:0, Success:True, Type:Prepared, Batch:False, QuerySize:1, BatchSize:0, Query:["insert into user (firstName, lastName, id) values (?, ?, ?)"], Params:[(Doe,NULL(VARCHAR),1)]
[INFO] 2023-01-04 13:34:16 method: net.ttddyy.dsproxy.support.CommonsLogUtils.writeLog(CommonsLogUtils.java:23)----Name:DATA_SOURCE_PROXY, Connection:4, Time:0, Success:True, Type:Prepared, Batch:False, QuerySize:1, BatchSize:0, Query:["insert into account (cents, createdOn, iban, interestRate, owner_id, id) values (?, ?, ?, ?, ?, ?)"], Params:[(12345,2022-10-04 13:34:16.4853734,ABC123,6.7,1,1)]
[INFO] 2023-01-04 13:34:16 method: net.ttddyy.dsproxy.support.CommonsLogUtils.writeLog(CommonsLogUtils.java:23)----Name:DATA_SOURCE_PROXY, Connection:5, Time:0, Success:True, Type:Prepared, Batch:False, QuerySize:1, BatchSize:0, Query:["select account0_.id as id1_0_0_, account0_.cents as cents2_0_0_, account0_.createdOn as createdo3_0_0_, account0_.iban as iban4_0_0_, account0_.interestRate as interest5_0_0_, account0_.owner_id as owner_id6_0_0_ from account account0_ where account0_.id=?"], Params:[(1)]
[INFO] 2023-01-04 13:34:16 method: org.hibernate.tool.schema.internal.SchemaDropperImpl$DelayedDropActionImpl.perform(SchemaDropperImpl.java:538)----HHH000477: Starting delayed evictData of schema as part of SessionFactory shut-down'
[INFO] 2023-01-04 13:34:16 method: net.ttddyy.dsproxy.support.CommonsLogUtils.writeLog(CommonsLogUtils.java:23)----Name:DATA_SOURCE_PROXY, Connection:6, Time:1, Success:True, Type:Statement, Batch:False, QuerySize:1, BatchSize:0, Query:["drop table if exists account CASCADE "], Params:[]
[INFO] 2023-01-04 13:34:16 method: net.ttddyy.dsproxy.support.CommonsLogUtils.writeLog(CommonsLogUtils.java:23)----Name:DATA_SOURCE_PROXY, Connection:6, Time:0, Success:True, Type:Statement, Batch:False, QuerySize:1, BatchSize:0, Query:["drop table if exists user CASCADE "], Params:[]

此外还有另一种方法,那就是hibernate提供的@Formula注解:

代码如下:

public class FormulaTest extends AbstractTest {
    @Override
    protected Class[] entities() {
        return new Class[]{
                Account.class,
                User.class
        };
    }

    @Test
    public void insertAccount() {
        doInJPA(entityManager -> {
            User user = new User();
            user.setId(1L);
            user.setFirstName("John");
            user.setFirstName("Doe");

            entityManager.persist(user);
            Account account = new Account(
                    1L,
                    user,
                    "ABC123",
                    12345L,
                    6.7,
                    Timestamp.valueOf(
                            LocalDateTime.now().minusMonths(3)
                    )
            );
            entityManager.persist(account);
        });

        doInJPA(entityManager -> {
            Account account = entityManager.find(Account.class, 1L);

            assertEquals(123.45D, account.getDollars(), 0.001);
            assertEquals(207L, account.getInterestCents());
            assertEquals(2.07D, account.getInterestDollars(), 0.001);
        });
    }

    @Entity(name = "Account")
    @Table(name = "account")
    public static class Account {
        @Id
        private Long id;

        @ManyToOne(fetch = FetchType.LAZY)
        private User owner;

        private String iban;

        private long cents;

        private double interestRate;

        private Timestamp createdOn;

        @Formula("cents::numeric / 100")
        private double dollars;

        @Formula("round((interestRate::numeric / 100) * cents * date_part('month', age(now(), createdOn))/ 12 ")
        private long interestCents;

        @Formula("round((interestRate::numeric / 100) * cents * date_part('month', age(now(), createdOn))/ 12)/ 100::numeric ")
        private double interestDollars;

        public Account() {
        }

        public Account(
                Long id, User owner, String iban,
                long cents, double interestRate, Timestamp createdOn) {
            this.id = id;
            this.owner = owner;
            this.iban = iban;
            this.cents = cents;
            this.interestRate = interestRate;
            this.createdOn = createdOn;
        }

        @Transient
        public double getDollars() {
            return this.dollars;
        }

        @Transient
        public long getInterestCents() {
            return this.interestCents;
        }
        @Transient
        public double getInterestDollars() {
            return this.interestDollars;
        }
    }

    @Entity(name = "User")
    @Table(name = "user")
    public static class User {
        @Id
        private Long id;

        private String firstName;

        private String lastName;

        public Long getId() {
            return id;
        }

        public void setId(Long id) {
            this.id = id;
        }

        public String getFirstName() {
            return firstName;
        }

        public void setFirstName(String firstName) {
            this.firstName = firstName;
        }

        public String getLastName() {
            return lastName;
        }

        public void setLastName(String lastName) {
            this.lastName = lastName;
        }

        @Override
        public String toString() {
            return "User{" +
                    "id=" + id +
                    ", firstName='" + firstName + '\'' +
                    ", lastName='" + lastName + '\'' +
                    '}';
        }
    }
}

注意,这里我们给属性添加的@Formula注解,并且将对应属性的get方法标注为@Transient,这里有一个小小的问题是@Formula注解的内容依赖于具体的数据库SQL的语法,因此兼容性不是很好;因此这个方法需要酌情考虑。

 

JPA——persistence.xml深入理解

更新于 2023.01.03 24分钟阅读 0 评论 5 推荐

    作者:

在本文中,我们会解释 JPA persistence.xml 配置文件的用途,以及如何使用可用的 XML 标记或属性设置 Java Persistence 应用程序。

虽然 Spring 应用程序可以在不需要 XML JPA 配置文件的情况下进行引导,但理解每个配置选项的含义仍然很重要,因为 Spring 在构建 Java Persistence LocalContainerEntityManagerFactoryBeanHibernate-specific LocalSessionFactoryBean 时还提供了另一种方法。

Persistence Unit

persistence.xml 配置文件用于配置给定的 JPA 持久化单元。持久化单元定义了引导 EntityManagerFactory 所需的所有元数据,例如实体映射、数据源和事务设置,以及 JPA proviser配置属性。

EntityManagerFactory 的目标是创建我们可以用于实体状态转换的 EntityManager 对象。

因此,persistence.xml 配置文件定义了引导 JPA EntityManagerFactory 所需的所有元数据。

JPA 持久化 XML 文件位置

传统上,persistence.xml 位于一个 META-INF 文件夹中,该文件夹需要驻留在 Java 类路径的根目录中。如果您使用的是 Maven,则可以将其存储在资源文件夹中,如下所示:

src/main/resources/META-INF/persistence.xml

JPA 持久性 XML 文件结构

persistence.xml 配置文件结构如下:

<?xml version="1.0" encoding="UTF-8"?>
<persistence version="2.2"
     xmlns="http://xmlns.jcp.org/xml/ns/persistence"
     xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
     xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/persistence
     http://xmlns.jcp.org/xml/ns/persistence/persistence_2_2.xsd">
 
    <persistence-unit
        name="HypersistenceOptimizer"
        transaction-type="JTA">
 
        <description>
            Hypersistence Optimizer is a dynamic analyzing tool that can scan
            your JPA and Hibernate application and provide you tips about the
            changes you need to make to entity mappings, configurations, queries,
            and Persistence Context actions to speed up your data access layer.
        </description>
 
        <provider>org.hibernate.jpa.HibernatePersistenceProvider</provider>
 
        <jta-data-source>java:global/jdbc/default</jta-data-source>
 
        <properties>          
            <property
                name="hibernate.transaction.jta.platform"
                value="SunOne"
            />
        </properties>
    </persistence-unit>
</persistence>

持久化标记<persistence>是根 XML 元素,它定义了 JPA 版本2.2和用于验证 persistence.xml 配置文件的 XML 模式。

持久化单元

<persistence-unit> 元素定义了关联的 JPA Persistence Unit 的名称,稍后您可以在使用 @PersistenceUnit JPA 注解注入关联的 EntityManagerFactory 实例时使用它来引用它:

@PersistenceUnit(name = "HypersistenceOptimizer")
private EntityManagerFactory entityManagerFactory;

transaction-type 属性定义了 JPA 事务策略,它可以取以下两个值之一:

  • JTA
  • RESOURCE_LOCAL

传统上,Java EE 应用程序默认使用 JTA,这需要有一个使用 2PC(两阶段提交)协议的 JTA 事务管理器以原子方式将更改应用于多个数据源(例如,数据库系统、JMS 队列、缓存)。

如果要将数据更改传播到单个数据源,则不需要 JTA,因此 RESOURCE_LOCAL 事务类型是一种更有效的替代方案。例如,默认情况下,Spring 应用程序使用 RESOURCE_LOCAL 事务,而要使用 JTA,您需要显式选择 JtaTransactionManager Spring bean。

简单总结一:如果涉及到多个数据源,则使用JTA,否则使用RESOURCE_LOCAL 。

描述

<description>元素允许您提供有关当前持久化单元目标的更多详细信息。

provider

<provider> XML 元素定义了实现 JPA PersistenceProvider 接口的完全限定类名。

如果您使用的是 Hibernate 4.3 或更新版本,那么您需要使用 org.hibernate.jpa.HibernatePersistenceProvider 类名。

如果您使用的是 Hibernate 4.2 或更早版本,则需要使用 org.hibernate.ejb.HibernatePersistence 类名。

jta 数据源和非 jta 数据源

JPA 规范定义两个不同的 XML 标记来提供 JNDI 数据源名称是非常不寻常的。应该有一个单独的数据源属性,因为事务类型已经指定是否使用 JTA。

不,如果您使用 JTA,您可以使用 jta-data-source 为关联的 JTA DataSource 指定 JNDI 名称,而对于 RESOURCE_LOCAL,您需要使用 non-jta-data-source。

如果您使用的是 Hibernate,还可以使用 hibernate.connection.datasource 配置属性来指定要使用的 JDBC 数据源。

properties

properties 元素允许您定义要配置的 JPA 或 JPA 提供程序特定的属性:

  • the Hibernate Dialect
  • the JTA transaction platform (e.g., GlassFish, JBoss, Bitronix, Atomikos)
  • 是否应该自动生成数据库模式
  • Hibernate 是否应该跳过对 RESOURCE_LOCAL 事务的自动提交检查
  • 激活慢SQL查询日志
  • 您可以在 org.hibernate.cfg.AvailableSettings 界面中找到更多属性。

举个例子:

<properties>
    <property name="hibernate.dialect" value="org.hibernate.dialect.H2Dialect"/>
    <property name="hibernate.connection.driver_class" value="org.h2.Driver"/>
    <!-- H2 is running in pure in Memory db mode, data1.txt will be lost as soon as connection is closed -->
    <property name="hibernate.connection.url" value="jdbc:h2:mem:db1;DB_CLOSE_DELAY=-1;MVCC=TRUE"/>
    <property name="hibernate.connection.username" value="sa"/>
    <property name="hibernate.connection.pool_size" value="5"/>
    <property name="hibernate.show_sql" value="true"/>
    <property name="hibernate.format_sql" value="true"/>
    <property name="hibernate.hbm2ddl.auto" value="update"/>
    <property name="org.hibernate.type.descriptor.sql.BasicBinder" value="true"/>
    <property name="org.hibernate.type.descriptor.sql.BasicExtractor" value="true"/>
</properties>

实体映射设置

默认情况下,Hibernate 能够根据 @Entity 注解的存在来查找 JPA 实体类,因此您无需声明实体类。

exclude-unlisted-classes

但是,如果要显式设置要使用的实体类,并排除在当前 Java 类路径中找到的任何其他实体类,则需要将 exclude-unlisted-classes 元素设置为 true 值:

<exclude-unlisted-classes>true</exclude-unlisted-classes>

class

在上面设置了 exclude-unlisted-classes XML 元素后,需要通过 class XML 元素指定当前 Persistence Unit 注册的实体类的列表:

<class>io.hypersistence.optimizer.forum.domain.Post</class>
<class>io.hypersistence.optimizer.forum.domain.PostComment</class>
<class>io.hypersistence.optimizer.forum.domain.PostDetails</class>
<class>io.hypersistence.optimizer.forum.domain.Tag</class>

绝大多数 JPA 和 Hibernate 应用程序使用注释来构建对象关系映射元数据。 但是,即使您正在使用注解,您仍然可以使用 XML 映射来覆盖静态注解元数据,并使用通过 orm.xml 配置文件提供的元数据。

mapping-file

默认情况下,orm.xml 配置文件位于 META-INF 文件夹中。如果要使用不同的文件位置,可以使用 persistence.xml 文件中的 mapping-file XML 元素,如下所示:

<mapping-file>file:///D:/Vlad/Work/Examples/mappings/orm.xml</mapping-file>

jar-file

但默认情况下,JPA 提供程序将扫描当前 Java 类路径以加载实体类或 XML 映射。如果要提供一个或多个要扫描的 JAR 文件,可以使用 jar-file 元素,如下所示:

<jar-file>lib/hypersistence-optimizer-glassfish-hibernate-example.jar</jar-file>

shared-cache-mode

shared-cache-mode 元素允许您定义将实体存储在二级缓存中的 SharedCacheMode 策略,它可以采用以下值之一:

  • ALL – 将所有实体存储在二级缓存中,
  • NONE – 实体不存储在二级缓存中,
  • ENABLE_SELECTIVE – 默认情况下不缓存任何实体,除了标记有 @Cacheable(true) 注释的实体,它们将被缓存
  • DISABLE_SELECTIVE - 默认情况下所有实体都被缓存,除了标有@Cacheable(false) 注释的实体
  • 未指定 - 使用 JPA 提供程序默认缓存策略。这也是未设置 shared-cache-mode 元素时使用的默认值。

您还可以使用 javax.persistence.cache.storeMode 属性以编程方式覆盖共享缓存模式策略,如下所示:

EntityManagerFactory entityManagerFactory = Persistence
.createEntityManagerFactory(
    "HypersistenceOptimizer",
    Collections.singletonMap(
        "javax.persistence.cache.storeMode",
        SharedCacheMode.ENABLE_SELECTIVE
    )
);

validation-mode

验证模式 XML 元素指定 ValidationMode 策略,它指示 JPA 提供者是否应该在运行时检查实体 Bean Validation。

验证模式元素可以采用以下值:

  • AUTO – 如果在当前 Java 类路径中找到 Bean Validation 提供程序,它将自动注册,并且所有实体都将被验证。 如果未找到 Bean Validation 提供程序,则不会验证实体。 这是默认值。
  • CALLBACK – 实体必须始终由 Bean 验证提供程序进行验证。如果 JPA 提供者没有在类路径上找到 Bean Validation 实现,则引导过程将失败。
  • NONE – 即使在类路径中找到 Bean Validation 提供程序,也不会验证实体。

您还可以使用 javax.persistence.validation.mode 属性以编程方式覆盖验证模式策略,如下所示:

EntityManagerFactory entityManagerFactory = Persistence
.createEntityManagerFactory(
    "HypersistenceOptimizer",
    Collections.singletonMap(
        "javax.persistence.validation.mode",
        ValidationMode.CALLBACK
    )
);

以上属性基本上总结了持久化单元的一些基本属性,如果还有其他的属性,欢迎大家一起讨论学习。

JPA实体状态学习-(瞬时态:Transient)

更新于 2022.12.14 14分钟阅读 0 评论 5 推荐

    作者:

为了学习实体的状态,我们还是贴出这张实体状态转换迁移图:

Transient(瞬时态)

按照上图的描述,java对象在内存中被赋值后,没有调用entityManager.persist()方法之前实体对象所处的状态就是瞬時態

举个例子:

Teacher teacher = new Teacher("email@dot.com");

此时,实例teacher就处于new/transient态(备注:这里的new和transient是同一个意思

 

Persistent(持久态)

掌握这个状态,需要记住一句关键的话:

An Object that is associated with persistence context (hibernate session) are in Persistent state. Any changes made to objects in this state are automatically propagated to databases without manually invoking persist/merge/remove

与持久上下文关联的对象处于持久态。任何对这个对象属性的更改都会自动传播到数据库而不需手动调用persist/merge/remove方法。(备注:这里的关联的意思就是说实体对象被持久上下文管理,也有的翻译将这个状态称作托管态)

代码举例:

这里我们还是用代码来说明:

**
 * 测试保存,验证持久态的数据更新
 */
public void saveTeacher() {
    Teacher teacher = new Teacher("new_email@gmail.com");
    logger.info("teacher: {} is transient state",teacher);
    EntityManager entityManager = entityManagerFactory.createEntityManager();
    EntityTransaction tx = entityManager.getTransaction();
    tx.begin();
    entityManager.persist(teacher);
    logger.info("teacher: {} is persistent state",teacher);
    teacher.setEmail("updated_email@gmail.com");
    Long persistedId = teacher.getId();
    logger.info("teacher: {} has changed email",teacher);
    tx.commit();
    entityManager.close();
    logger.info("teacher: {} is detached state",teacher);
    entityManager = entityManagerFactory.createEntityManager();
    tx = entityManager.getTransaction();
    tx.begin();
    teacher = entityManager.find(Teacher.class, persistedId);
    tx.commit();
    entityManager.close();
    logger.info("Persisted teacher: {}", teacher);
}

下面截取出相应的日志信息来分析问题:

teacher: Student{id=null, email='new_email@gmail.com'} is transient state
Hibernate: 
   call next value for hibernate_sequence
teacher: Student{id=1, email='new_email@gmail.com'} is persistent state
teacher: Student{id=1, email='updated_email@gmail.com'} has changed email
Hibernate: 
   insert 
   into
       teachers
       (email, id) 
   values
       (?, ?)
Hibernate: 
   update
       teachers 
   set
       email=? 
   where
       id=?
teacher: Student{id=1, email='updated_email@gmail.com'} is detached state
Hibernate: 
   select
       teacher0_.id as id1_0_0_,
       teacher0_.email as email2_0_0_ 
   from
       teachers teacher0_ 
   where
       teacher0_.id=?
Persisted teacher: Student{id=1, email='updated_email@gmail.com'}

日志分析:

以上的日志说明了一个teacher对象从transient----->persistent----->detached 过程中,SQL语句的执行情况:

执行persist()到持久态后执行一条insertSQL语句;

当改变持久态对象的属性时会执行一条update语句,这两条语句显然都是tx.commit()后才发出;

最后发出一个select语句来查询数据库中最终的teacher对象,用于验证之前的结论。

管理这一过程的就是持久化上下文。

我们测试在detached态时修改对象的状态有啥不一样:

/**
 * 测试保存,验证持久态的数据更新
 */
public void saveTeacher() {
    Teacher teacher = new Teacher("new_email@gmail.com");
    logger.info("teacher: {} is transient state",teacher);
    EntityManager entityManager = entityManagerFactory.createEntityManager();
    EntityTransaction tx = entityManager.getTransaction();
    tx.begin();
    entityManager.persist(teacher);
    logger.info("teacher: {} is persistent state",teacher);
    teacher.setEmail("updated_email@gmail.com");
    Long persistedId = teacher.getId();
    logger.info("teacher: {} has changed email",teacher);
    tx.commit();
    //增加了这一行代码后
    teacher.setEmail("updated_email1@gmail.com");
    entityManager.close();
    logger.info("teacher: {} is detached state",teacher);
    entityManager = entityManagerFactory.createEntityManager();
    tx = entityManager.getTransaction();
    tx.begin();
    teacher = entityManager.find(Teacher.class, persistedId);
    tx.commit();
    entityManager.close();
    logger.info("Persisted teacher: {}", teacher);
}

这里我只截取部分输出,能够看到结果表明原理就好

teacher: Student{id=1, email='updated_email1@gmail.com'} is detached state

Persisted teacher: Student{id=1, email='updated_email@gmail.com'}

可以知道,detached状态的对象属性email已经改变成updated_email1@gmail.com,但是从数据库查询数来的值没有改变,还是updated_email@gmail.com

结论说明 

  • Detached状态的实体属性改变不会同步更新数据库
  • Persistent状态的实体属性改变会同步更新数据库

下一篇,我会尝试用代码来说明其他实体状态的迁移,欢迎大家多多分享和交流。

 

JPA实体状态深入理解

更新于 2022.12.14 2分钟阅读 0 评论 5 推荐

    作者:

我们在学习JPA实体状态的时候,常常会问,JPA的实体有多少状态呢?相信这个问题不难回答:

  • 瞬时态(transient)
  • 托管态(persistent)
  • 游离态(detached)
  • 移除态(removed)

注意:这里最后一个移除态,有的时候也叫删除态(deleted),至于它和移除态有啥区别,暂时没有想到,如果您对此有更加深刻的理解,请留言回复。

为什么会有这四种状态呢?

啥,这个也有为啥,网上不是都这么说的,你怎么会提出这么个奇怪的问题?其实不然,我们对一个事物的理解,不仅要理解表象,更要深入理解本质:

这里我说说自己的一些浅见:

 

首先,一个实体对象从创建到持久化的数据库,必然会有自己的生命周期,而生命周期是由一些状态构成的,因此,我理解的一个结论是:实体状态是对实体对象生命周期的一个抽象。

其次,想到了状态的变化,您想到了啥?对了,就是状态机。想到深入理解实体状态的变化,其实就是掌握实体生命周期状态的迁移。

以上图片就是实体生命周期中状态的迁移变化图。

我这里有一个小小的心得就是,如果在使用JPA的时候遇到问题,我的第一反应就是,当前我操作的实体属于生命周期的哪个状态?然后再排查其他情况。

平时我们一般使用JPA比较多,而JPA的实现默认是hibernate实现的,因此为了理解实体的状态转化图,在下面贴出了另外两张图来提供给大家参考:

JPA状态转化图:

Hibernate状态转化图:'

最后,为啥一定需要四种状态来管理实体对象的生命周期呢?removed和detached有啥区别呢?我们留待下一篇通过代码来学习讨论。我们会使用Hibernate来作为例子进行讨论。

如何集成多个限界上下文?

发布于 2023.02.28 0分钟阅读 0 评论 5 推荐

    作者:

在进行领域驱动设计时,发现有两个情境很容易产生误解?他们是:多个限界上下文集成聚合设计。在本文中,主要介绍多个限界上下文集成。

通用语言

任何研究过 DDD 的人都已经了解到与其一起使用的许多概念:限界上下文、实体、聚合、值对象等。其中一些概念(实体,聚合,值对象)与代码直接关联,很容易理解和使用,而另一些(限界上下文)则由于其抽象性而难以吸收应用。通用语言就是这样一种情况,尽管它简单而强大,但很容易被忽视或遗忘。

通用语言的目标是建立一种明确的交流方式或语言,用于专家和开发人员之间的交流。这意味着无论是否以代码表示的所有概念,例如类和进程,都不应随意命名或从以开发人员为中心的角度来看,而是集中讨论您的软件解决方案如何尝试解决给定的问题。

也许一个典型的例子是我们倾向于将使用系统的个人称为用户,而领域专家将其称为客户。或者倾向于将我们操纵的概念称为 SomethingData 或 SomethingInformation。

这种看似无害的差异是可能导致混乱或成为“破电话游戏”的一个例子,在这种情况下,您的意图从业务预期到实际实施的时候,意图就会丢失。

达成正确的通用语言上是很强大的,因为它可以帮助您将隐含的定义明确化,这总是一件好事。想象一下,您发现它确实是一个目的地地址,而不是调用某些运输信息。

现在让我们谈谈作用域。在编程语言中,作用域很容易理解为某个项目(例如变量)的程序作用域。在该作用域之外,无法保证存在相同的概念,甚至无法保证具有相同的价值/意义。当我们将此讨论带到我们的非编程语言时,我们承认我们精心设计的语言存在局限性。这意味着可以甚至鼓励不追求在其所有应用程序中使用的概念的通用企业作用域定义。要定义此类限制并探索为什么需要这样做,有必要讨论限界上下文。

让我们设定一些边界

在讨论限界上下文 (BC) 的概念时,Eric Evans 表示我们应该“明确定义模型应用的上下文。 在团队组织、应用程序特定部分内的使用以及代码库和数据库模式等物理表现方面明确设置界限。 在这些范围内保持模型严格一致,但不要被外部问题分散注意力或混淆”。

该声明揭示了一个事实,即我们的模型以及最终定义它们的语言的有效性是有限的。它还强调,有些事情“超出”我们不应该“分心”的界限。虽然讨论如何定义这些边界超出了本文的范围,但我想重点关注限制的存在,如图 1 所示。

Figure 1. Different Bounded Contexts and their relationships.

在此图中,我们注意到以下特征:

1、限界上下文之间可能有也可能没有关系

2、从一个限界上下文到另一个模型可以具有相同的名称但不同的定义

3、模型在一个限界上下文和另一个限界上下文之间可以有不同的名称,但具有一些或所有共同特征

对于没有任何关系的限界上下文,除了重申一下,如果你在两者中找到相同的模型名称就可以了,其他没什么好说的。假设这是仔细决定/迭代的结果。

另一方面,有共享关系的限界上下文,让我们看一下上下文映射,它提供了关于它们的关系如何影响它们的模型和语言的额外信息。

Upstream/Downstream

Figure 2. An upstream/downstream relationship.

在这里,上游是决定关系的人,使下游采用该语言作为自己的一部分。在我们的示例中,不仅在代码中使用了 Customer Profile,而且还被识别为 Checkout 限界上下文的概念部分。

Anti-Corruption Layer

Figure 3. An upstream/downstream with an anti-corruption layer.

在此示例中,Warehouse 不需要从产品信息管理 (PIM) 导入产品定义,而是定义一个本地概念,该概念具有所需信息的子集。

让我们从实现的角度仔细看看这些例子。

Connecting the Dots

我们的第一个实现将说明如何表示上游/下游关系。代码和讨论将集中在集成方面,而不是其他与 DDD 相关的方面。

我们虚构的 Customer Profile 定义了一个名为 Ordering Preferences 的模型,供 Checkout 使用。想象一下,作为其运营的一部分,它需要了解客户的预定义决定,以加快交付所有购买的商品。

class PrepareCheckoutCommandHandler {
    // other dependencies would also be injected here
    constructor(private readonly orderingPreferenceService: OrderingPreferenceService) {
    }

    // omitting error handling aspects
    async handle(command: PrepareCheckoutCommand): Promise<CheckoutDTO> {
        // obtain the ordering preferences for a given customer
        const orderingPreferences = await this.orderingPreferenceService.getOrderingPreferences(command.customerId);
        // ... potentially call other dependencies
        const checkout = new Checkout(command.customerId, command.cart, orderingPreferences);
        return toDTO(checkout);
    }       
}

我们的应用程序服务——命令处理程序——需要为试图启动结账流程的客户获取订购偏好。有了这些信息,就可以创建一个 Checkout 来封装预期要遵循的业务规则。

所以 OrderingPreferences 是一个本地概念,从 Customer Profile 导入并通过 OrderingPreferencesService 获得。一种常见的方法是将其表示为一个接口。

interface OrderingPreferencesService {
    getOrderingPreference(customerId: string): OrderingPreference;
}

在我们的基础设施层中,我们有这样一个接口的具体实现,它实际上与客户资料交互并产生预期值对象。

class CustomerProfileOrderingPreferencesService implements OrderingPreferencesService {
    constructor(private readonly httpClient: HttpClient) {
    }

    async getOrderingPreferences(customerId: string): Promise<OrderingPreferences> {
        const response = await this.httpClient.get(`/customers/${customerId}/ordering-preferences`);
        return OrderingPreferences.create(response.data);
    }
}

在这里我们看到我正在使用某种 HTTP 客户端发出此请求以检索数据,此时它只是一个数据集合。然后我使用该数据创建 OrderingPreferences,这是公认的本地概念。

现在让我们看看我们的第二个实现,它旨在说明您何时不导入概念,而是必须根据外部定义构建一个本地概念。

我们有一个仓库操作,为了处理和运送客户订单,要求我们提供一些用于海关目的的监管信息。与下订单时收到的上下文信息相反,这些额外的信息位于产品信息管理 (PIM) 的另一个限界上下文中。在 PIM 中,此信息是产品模型的一部分。

Figure 4. Free Trade Agreement made up of portions of the Product definition.

经过讨论,决定在当地没有必要有相同的产品概念,所需的信息被称为自由贸易协定。

像以前一样,我们有一个应用程序服务,它注入了依赖项。

class CreateOutboundHandler {
    constructor(private readonly freeTradeAgreementService: FreeTradeAgreementService) {
    }

    async handle(command: CreateOutboundCommand): Promise<OutboundDTO> {
        const freeTradeAgreement = await this.freeTradeAgreementService.getFromProduct(command.productId);
        const outbound = new Outbound(command.productId, command.quantity, freeTradeAgreement);
        return toDTO(outbound);
    }
}

首先要提到的是,您没有将 PIM 的产品定义公开给您的应用程序。它期望一个自由贸易协定值对象作为输出。和以前一样,一种方法是定义一个具有具体基础设施实现的接口。

// In your application
interface FreeTradeAgreementService {
    getFromProduct(productId: string): FreeTradeAgreement;
}

// In your infrastructure
class TranslatingFreeTradeAgreementService implements FreeTradeAgreementService {
    constructor(private readonly adapter: FreeTradeAgreementAdapter) {
    }

    async getFromProduct(productId: string): Promise<FreeTradeAgreement> {
        // More complex situations could require multiple calls to separate services in order to
        // build the local concept
        return await this.adapter.getFreeTradeAgreement(productId);
    }
}

TranslatingFreeTradeAgreementService 的前缀旨在明确此服务的作用,即采用外部概念并生成遵循本地语言的内容。

至于信息的实际检索及其操作,一种推荐的方法是在适配器和翻译器之间拆分职责,前者提出实际请求,后者负责验证并将必要的信息传递给您 值对象。

class FreeTradeAgreementAdapter {
    constructor(private readonly httpClient: HttpClient, private readonly translator: FreeTradeAgreementTranslator) {
    }

    getFreeTradeAgreement(productId: string): FreeTradeAgreement {
        const response = this.httpClient.get(`/products/${productId}`);
        return this.translator.translate(response.data);
    }
}

class FreeTradeAgreementTranslator {
    
    async translate(product: any): FreeTradeAgreement {
        // do some validation, conversion from string to number, etc.
        return FreeTradeAgreement.create(product.limitAmount, product.percentage);
    }
}

很好,但我需要所有这些吗?

使用的结构肯定有很多部分:接口、具体的翻译实现、适配器和转化器。您的第一反应可能是认为它在现实生活中没有用处太多或太复杂。最后,决定将取决于您和您采用的开发生态系统、编程语言和工具。

这里简要总结了每个组件的用途和一些实际注意事项。

The Service Interface

如果您知道并相信 SOLID 原则可以促进良好实践,那么该界面可以帮助您专注于意图,而不是被迫预先对太多实施细节做出决定。

一些开发语言允许您动态替换现有的实现以方便测试,这可以说可以让您跳过接口定义。

The Translating Service

这里的目标是确保您的翻译服务知道它需要联系哪些服务才能返回您的应用程序期望的本地概念。

它依靠一个或多个适配器来执行其命令,更像是一个编排器。

The Adapter

它是实际使用基础设施、http、gRPC 等从外部限界上下文获取数据并将其传递给转化器的一种。

如果您只有一个 BC 要联系,一个潜在的简化方法是直接从翻译服务进行远程呼叫和转化。

The Translator

获取从外部限界上下文接收的数据,验证它是否包含我们需要的结构,并使用它来创建所需的值对象。

转化中不应该存在任何实际的业务规则。在这里,您要验证预期存在的数据是否确实存在,丢弃我们不需要的数据,必要时在数据中执行最小转换,并将其传递给值对象以进行实际的域验证。

由于您将不得不编写代码来执行此操作,如果您想避免创建单独的类,您可以将此功能分组为适配器的内部部分。

从视觉上看,这是所有部分之间的关​​系:

我的建议是遵循垂直开发方法并不断重构您的代码。随着您的进步和获得更多关于所需集成的知识,这种功能分解将变得更加容易。

总结

集成多个限界上下文更像是一种规范而不是例外,因此花时间了解可以维护这些关系的多种形式是必不可少的。正如我们所看到的,这不是“魔法”,但确实需要一些时间来了解您如何进行这种集成,而不仅仅是技术方面。

如果您将依赖同步或异步通信,REST 或 gRPC 是重要且必要的方面,通常应推迟到您实际需要做出决定时再考虑。而且,这是在了解关系类型和将采用的(领域)语言之后发生的。

JPA实现自定义类型(一)

更新于 2022.12.01 6分钟阅读 0 评论 5 推荐

    作者:

我们知道,Hibernate是一个JPA的默认实现,因此,在本文中,我们用hibernate来实现一个自定义类型。

自定义类型有多种情况,比如:

基本类型——基本 Java 类型的映射 

可嵌入——复合 java 类型/POJO 的映射 

集合——映射基本和复合 java 类型的集合

先来实现一种自定义的基本类型,具体的用例就是有一个基本类型LocalDate,我们希望将这个本地日期字段对应到一个数据库的varchar。看看具体的代码:

首先创建一个LocalDateStringType

public class LocalDateStringType extends AbstractSingleColumnStandardBasicType<LocalDate> {

    public static final LocalDateStringType INSTANCE = new LocalDateStringType();

    public LocalDateStringType() {
        super(VarcharTypeDescriptor.INSTANCE, LocalDateStringJavaDescriptor.INSTANCE);
    }

    @Override
    public String getName() {
        return "LocalDateString";
    }

    @Override
    public Object resolve(Object value, SharedSessionContractImplementor session, Object owner, Boolean overridingEager) throws HibernateException {
        return super.resolve(value, session, owner, overridingEager);
    }

}

这里需要注意的是,我们继承的AbstractSingleColumnStandardBasicType类需要在构造函数中调用一下,而调用的参数就是LocalDate类型和Varchar类型的转化描述类。

其次我们需要自己实现转换类:LocalDateStringJavaDescriptor

public class LocalDateStringJavaDescriptor extends AbstractTypeDescriptor<LocalDate> {

    public static final LocalDateStringJavaDescriptor INSTANCE =
            new LocalDateStringJavaDescriptor();

    public LocalDateStringJavaDescriptor() {
        super(LocalDate.class, ImmutableMutabilityPlan.INSTANCE);
    }

    @Override
    public LocalDate fromString(String s) {
        return null;
    }

    @Override
    public <X> X unwrap(LocalDate localDate, Class<X> aClass, WrapperOptions wrapperOptions) {
        if (localDate == null)
            return null;

        if (String.class.isAssignableFrom(aClass))
            return (X) LocalDateType.FORMATTER.format(localDate);

        throw unknownUnwrap(aClass);
    }

    @Override
    public <X> LocalDate wrap(X value, WrapperOptions wrapperOptions) {
        if (value == null)
            return null;

        if(String.class.isInstance(value))
            return LocalDate.from(LocalDateType.FORMATTER.parse((CharSequence) value));

        throw unknownWrap(value.getClass());
    }
}

这里需要实现两个关键的方法unwrapwrap实际上就是将LocalDate转化为String,以及将String转化为LocalDate。同时注意构造函数需要将处理的类型(LocalDate)转入进去。

最后,我们用一个例子来测试一下:

public class CustomTypeTest extends AbstractTest {
    @Override
    protected Class[] entities() {
        return new Class[]{OfficeEmployee.class};
    }

    @Test
    public void testCustomType() {
        doInJPA(entityManager -> {
            LocalDate date = LocalDate.now();
            OfficeEmployee officeEmployee = new OfficeEmployee(date);
            entityManager.persist(officeEmployee);
        });
    }
}

@Entity
class OfficeEmployee  {
    @Id
    @GeneratedValue( strategy = GenerationType.AUTO)
    private Long id;

    @Column
    @Type(type = "com.jpa.demo.custom.type.LocalDateStringType")
    private LocalDate dateOfJoining;

    public OfficeEmployee(){

    }

    public OfficeEmployee(LocalDate dateOfJoining){
        this.dateOfJoining = dateOfJoining;
    }

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public LocalDate getDateOfJoining() {
        return dateOfJoining;
    }

    public void setDateOfJoining(LocalDate dateOfJoining) {
        this.dateOfJoining = dateOfJoining;
    }

    @Override
    public String toString() {
        return "OfficeEmployee{" +
                "id=" + id +
                ", dateOfJoining=" + dateOfJoining +
                '}';
    }
}

执行后可以看到LocalDate被转化为String存储到数据库啦。

欢迎大家多多交流。

JPA实现自定义类型(二)

发布于 2022.12.01 4分钟阅读 0 评论 5 推荐

    作者:

今天继续昨天的内容,JPA实现一个自定义类型,昨天我们说了怎么实现一个基本类型,今天学习一个复杂一点的。

我们的用例场景是将一个领域对象映射到一个类型,比如有一个PhoneNumber的对象,它包含三个字段分别是countryCode,cityCode,number,现在需要将这个对象映射到数据库,我们看怎么来实现:

第一步,创建一个PhoneNumber

public class PhoneNumber {
    private Integer countryCode;

    private Integer cityCode;

    private Integer number;

    public PhoneNumber(){}
    public PhoneNumber(Integer countryCode,
                       Integer cityCode,
                       Integer number){
        this.countryCode = countryCode;
        this.cityCode = cityCode;
        this.number = number;
    }


    public Integer getCountryCode() {
        return countryCode;
    }

    public void setCountryCode(Integer countryCode) {
        this.countryCode = countryCode;
    }

    public Integer getCityCode() {
        return cityCode;
    }

    public void setCityCode(Integer cityCode) {
        this.cityCode = cityCode;
    }

    public Integer getNumber() {
        return number;
    }

    public void setNumber(Integer number) {
        this.number = number;
    }
}

第二步,创建一个PhoneNumberType

public class PhoneNumberType implements UserType {
    @Override
    public int[] sqlTypes() {
        return new int[]{Types.INTEGER, Types.INTEGER, Types.INTEGER};
    }

    @Override
    public boolean equals(Object o, Object o1) throws HibernateException {
        return false;
    }

    @Override
    public int hashCode(Object o) throws HibernateException {
        return 0;
    }

    @Override
    public Object nullSafeGet(ResultSet resultSet, String[] names,
                              SharedSessionContractImplementor sharedSessionContractImplementor,
                              Object o) throws HibernateException, SQLException {
        if (resultSet.wasNull())
            return null;
        int countryCode = resultSet.getInt(names[0]);
        int cityCode = resultSet.getInt(names[1]);
        int number = resultSet.getInt(names[2]);
        PhoneNumber employeeNumber = new PhoneNumber(countryCode, cityCode, number);

        return employeeNumber;
    }

    @Override
    public void nullSafeSet(PreparedStatement st, Object value, int index, SharedSessionContractImplementor sharedSessionContractImplementor) throws HibernateException, SQLException {
        if (Objects.isNull(value)) {
            st.setNull(index, Types.INTEGER);
            st.setNull(index + 1, Types.INTEGER);
            st.setNull(index + 2, Types.INTEGER);
        } else {
            PhoneNumber employeeNumber = (PhoneNumber) value;
            st.setInt(index,employeeNumber.getCountryCode());
            st.setInt(index+1,employeeNumber.getCityCode());
            st.setInt(index+2,employeeNumber.getNumber());
        }
    }

    @Override
    public Object deepCopy(Object o) throws HibernateException {
        return null;
    }

    @Override
    public boolean isMutable() {
        return false;
    }

    @Override
    public Serializable disassemble(Object o) throws HibernateException {
        return null;
    }

    @Override
    public Object assemble(Serializable serializable, Object o) throws HibernateException {
        return null;
    }

    @Override
    public Object replace(Object o, Object o1, Object o2) throws HibernateException {
        return null;
    }

    @Override
    public Class returnedClass() {
        return PhoneNumber.class;
    }
}

在这里,重写的 sqlTypes 方法返回字段的 SQL 类型,顺序与它们在我们的 PhoneNumber 类中声明的顺序相同。同样,returnedClass 方法返回我们的 PhoneNumber Java 类型。

唯一剩下要做的就是实现在 Java 类型和 SQL 类型之间转换的方法,就像我们为 BasicType 所做的那样。

关键就是实现nullSafeSetnullSafeGet两个方法,它的基本原理就是设置PreparedStatement的值,以及从ResultSet中取出值来创建Java对象;

注意,在这里我们实现的是UserType接口,也许您有的疑问是为什么不用@Embeddable 其实都是可以使用的,但是这里用UserType的原因是如果有需要自定义的情况,还是建议您使用UserType,否则还是使用@Embeddable ;

在具体使用的时候,需要这样配置:

@Type(type = "com.jpa.demo.custom.type.PhoneNumberType")
@Columns(columns = { @Column(name = "country_code"),
        @Column(name = "city_code"), @Column(name = "number") })
private PhoneNumber employeeNumber;

好啦,欢迎讨论。

 

Spring hibernate JPA日志记录问题

发布于 2022.11.25 6分钟阅读 0 评论 5 推荐

    作者:

今天我们来学习一下如何使用JPA的日志信息来记录执行的SQL语句,当然这里的SQL语句包括查询和修改。

我们先了解一般情况下的SQL是如何记录的,看看下面的配置

spring:
  jpa:
    database-platform: org.hibernate.dialect.H2Dialect
    hibernate:
      ddl-auto: update
    properties:
      hibernate:
        format_sql: true
    show_sql: true

这种类型的配置算是最普遍的配置的,当然,这个看着还行,会打印出一些SQL语句,但是不会把SQL的执行中的参数打印出来;

为了解决打印参数的问题,我们需要在log4j中配置,具体的操作就是,在log4j.properties

log4j.logger.org.hibernate.SQL = trace
log4j.logger.org.hibernate.type = trace

这样参数会连同sql语句一起打印出来;万事大吉,真的万事大吉吗?如果你按照上面的思路做,你会发现很多hibernate的日志都会打印出来,满屏幕都是日志。

我说的第二种方法是使用数据源代理,具体的操作很简单:

第一步,在pom.xml中增加一个配置依赖:

<dependency>
    <groupId>com.github.gavlyukovskiy</groupId>
    <artifactId>datasource-proxy-spring-boot-starter</artifactId>
    <version>1.8.1</version>
</dependency>

第二步,在application.yml中增加配置:

logging:
  level:
    net:
      ttddyy:
        dsproxy:
          listener: debug

好了,我们看看输出:

Name:dataSource, Connection:35, Time:0, Success:True
Type:Prepared, Batch:False, QuerySize:1, BatchSize:0
Query:["delete from us_follower where id=?"]
Params:[(10113)]

Name:dataSource, Connection:34, Time:0, Success:True
Type:Prepared, Batch:False, QuerySize:1, BatchSize:0
Query:["select follower0_.id as id1_7_, follower0_.avatar_url as avatar_u2_7_,
 follower0_.followable_id as followab5_7_, follower0_.name as name3_7_, 
 follower0_.user_id as user_id4_7_ from us_follower follower0_
  where (follower0_.name like ?) and follower0_.followable_id=1000 limit ?"]
Params:[(%yun900%,10)]

怎么样,看着还挺舒服吧,最主要是这个日志为我们提供了很多关键的信息:

比如,查询对应的数据源名称,连接编号,花费时间等等。

具体的字段说明请参考下面的表格:

KeyValue

Name

Name of ProxyDataSource

Connection

Connection ID

Time

How long query took to execute in ms.

Success

Query execution was successful or not.

Type

Type of statement (Statement/Prepared/Callable).

Batch

Batch execution.

QuerySize

Number of queries.

BatchSize

Number of batch.

Query

Query

Params

Query parameters

好了,要想优化自己的JPA,这是一个强烈推荐的工具。欢迎大家讨论学习。

如何使用 Hibernate 5 访问数据库表元数据

发布于 2022.11.23 13分钟阅读 0 评论 5 推荐

    作者:

在本文中,我们简单介绍如何在hibernate中访问数据库表的元数据。

Integrator

Hibernate 非常灵活,因此它定义了许多 SPI(服务提供者接口),您可以注册这些 SPI 以自定义 Hibernate 内部结构。其中一个接口是 org.hibernate.integrator.spi.Integrator,它被许多与 Hibernate ORM 集成的技术使用,比如 Bean Validation、Envers 或 JACC Security Provider。

使用 Hibernate Integrator API,我们可以编写自己的组件来捕获 SessionFactory 构建时元数据,否则该元数据仅在引导期间可用。

public class MetadataExtractorIntegrator
    implements org.hibernate.integrator.spi.Integrator {
 
    public static final MetadataExtractorIntegrator INSTANCE =
        new MetadataExtractorIntegrator();
 
    private Database database;
 
    public Database getDatabase() {
        return database;
    }
 
    @Override
    public void integrate(
        Metadata metadata,
        SessionFactoryImplementor sessionFactory,
        SessionFactoryServiceRegistry serviceRegistry) {
 
        database = metadata.getDatabase();
    }
 
    @Override
    public void disintegrate(
        SessionFactoryImplementor sessionFactory,
        SessionFactoryServiceRegistry serviceRegistry) {
    }
}

我们感兴趣的是 org.hibernate.boot.model.relational.Database,因为它包含所有与数据库相关的元数据。

要向 Hibernate 注册 MetadataExtractorIntegrator,我们有两种基于引导方法的可能性。

Hibernate-native boostrap

如果您使用的是 Hibernate-native bootstrap,那么您可以使用 BootstrapServiceRegistryBuilder 注册 Integrator,如下所示:

final BootstrapServiceRegistry bootstrapServiceRegistry =
    new BootstrapServiceRegistryBuilder()
    .enableAutoClose()
    .applyIntegrator(MetadataExtractorIntegrator.INSTANCE)
    .build();
 
final StandardServiceRegistry serviceRegistry =
    new StandardServiceRegistryBuilder(bootstrapServiceRegistry)
    .applySettings(properties())
    .build();

JPA boostrap

如果您使用的是 JPA 引导程序,则可以使用 BootstrapServiceRegistryBuilder 注册 Integrator,如下所示:

Map<String, Object> configuration = new HashMap<>();
 
Integrator integrator = integrator();
if (integrator != null) {
    configuration.put("hibernate.integrator_provider",
        (IntegratorProvider) () -> Collections.singletonList(
            MetadataExtractorIntegrator.INSTANCE
        )
    );
}
 
EntityManagerFactory entityManagerFactory = new EntityManagerFactoryBuilderImpl(
    new PersistenceUnitInfoDescriptor(persistenceUnitInfo),
    configuration
)
.build();

用一个例子来测试下:

for(Namespace namespace : MetadataExtractorIntegrator.INSTANCE
    .getDatabase()
    .getNamespaces()) {
     
    for( Table table : namespace.getTables()) {
        LOGGER.info( "Table {} has the following columns: {}",
             table,
             StreamSupport.stream(
                Spliterators.spliteratorUnknownSize(
                    table.getColumnIterator(),
                    Spliterator.ORDERED
                ),
                false
            )
            .collect( Collectors.toList())
        );
    }
}

以上代码会将持久化单元中的实体对应的表结构都打印出来,感兴趣的可以试试。

另一个例子如下:

Metadata metadata = MetadataExtractorIntegrator.INSTANCE.getMetadata();
 
for ( PersistentClass persistentClass : metadata.getEntityBindings()) {
 
    Table table = persistentClass.getTable();
     
    LOGGER.info( "Entity: {} is mapped to table: {}",
                 persistentClass.getClassName(),
                 table.getName()
    );
 
    for(Iterator propertyIterator = persistentClass.getPropertyIterator();
            propertyIterator.hasNext(); ) {
        Property property = (Property) propertyIterator.next();
         
        for(Iterator columnIterator = property.getColumnIterator();
                columnIterator.hasNext(); ) {
            Column column = (Column) columnIterator.next();
             
            LOGGER.info( "Property: {} is mapped on table column: {} of type: {}",
                         property.getName(),
                         column.getName(),
                         column.getSqlType()
            );
        }
    }
}

以上代码会将持久化单元中的实体到表结构的映射打印出来。

如何在没有 persistence.xml 配置文件的情况下以编程方式引导 JPA

发布于 2022.11.21 28分钟阅读 0 评论 5 推荐

    作者:

上一篇我们知道了如何用编程的方式创建一个EntityManagerFactory;今天我们用另外一种更加通用方式来创建EntityManagerFactory。这种方式是基于标准的java持久化API。

在上一节,我们用到了一个接口,这个类就是PersistenceUnitInfo,JPA 规范 PersistenceUnitInfo 接口封装了引导 EntityManagerFactory 所需的一切。通常,此接口由 JPA 提供程序实现,以存储在解析 persistence.xml 配置文件后检索到的信息。

因为我们将不再使用persistence.xml 配置文件,所以我们必须自己实现这个接口。出于此测试的目的,请考虑以下实现:

public class PersistenceUnitInfoImpl
        implements PersistenceUnitInfo {
 
    public static final String JPA_VERSION = "2.1";
 
    private final String persistenceUnitName;
 
    private PersistenceUnitTransactionType transactionType =
        PersistenceUnitTransactionType.RESOURCE_LOCAL;
 
    private final List<String> managedClassNames;
 
    private final List<String> mappingFileNames = new ArrayList<>();
 
    private final Properties properties;
 
    private DataSource jtaDataSource;
 
    private DataSource nonJtaDataSource;
 
    public PersistenceUnitInfoImpl(
            String persistenceUnitName,
            List<String> managedClassNames,
            Properties properties) {
        this.persistenceUnitName = persistenceUnitName;
        this.managedClassNames = managedClassNames;
        this.properties = properties;
    }
 
    @Override
    public String getPersistenceUnitName() {
        return persistenceUnitName;
    }
 
    @Override
    public String getPersistenceProviderClassName() {
        return HibernatePersistenceProvider.class.getName();
    }
 
    @Override
    public PersistenceUnitTransactionType getTransactionType() {
        return transactionType;
    }
 
    @Override
    public DataSource getJtaDataSource() {
        return jtaDataSource;
    }
 
    public PersistenceUnitInfoImpl setJtaDataSource(
            DataSource jtaDataSource) {
        this.jtaDataSource = jtaDataSource;
        this.nonJtaDataSource = null;
        transactionType = PersistenceUnitTransactionType.JTA;
        return this;
    }
 
    @Override
    public DataSource getNonJtaDataSource() {
        return nonJtaDataSource;
    }
 
    public PersistenceUnitInfoImpl setNonJtaDataSource(
            DataSource nonJtaDataSource) {
        this.nonJtaDataSource = nonJtaDataSource;
        this.jtaDataSource = null;
        transactionType = PersistenceUnitTransactionType.RESOURCE_LOCAL;
        return this;
    }
 
    @Override
    public List<String> getMappingFileNames() {
        return mappingFileNames;
    }
 
    @Override
    public List<URL> getJarFileUrls() {
        return Collections.emptyList();
    }
 
    @Override
    public URL getPersistenceUnitRootUrl() {
        return null;
    }
 
    @Override
    public List<String> getManagedClassNames() {
        return managedClassNames;
    }
 
    @Override
    public boolean excludeUnlistedClasses() {
        return false;
    }
 
    @Override
    public SharedCacheMode getSharedCacheMode() {
        return SharedCacheMode.UNSPECIFIED;
    }
 
    @Override
    public ValidationMode getValidationMode() {
        return ValidationMode.AUTO;
    }
 
    public Properties getProperties() {
        return properties;
    }
 
    @Override
    public String getPersistenceXMLSchemaVersion() {
        return JPA_VERSION;
    }
 
    @Override
    public ClassLoader getClassLoader() {
        return Thread.currentThread().getContextClassLoader();
    }
 
    @Override
    public void addTransformer(ClassTransformer transformer) {
 
    }
 
    @Override
    public ClassLoader getNewTempClassLoader() {
        return null;
    }
}

为了更容易地重用引导逻辑,我们可以定义一个 AbstractJPAProgrammaticBootstrapTest 基类,它将由我们所有想要在没有外部 persistence.xml 配置文件的情况下引导的单元测试扩展。

AbstractJPAProgrammaticBootstrapTest 在开始新测试时创建一个 EntityManagerFactory,并在执行单元测试后将其关闭。这样,所有测试都是独立运行的,每个测试类也可以是独立的。

private EntityManagerFactory emf;
 
public EntityManagerFactory entityManagerFactory() {
    return emf;
}
 
@Before
public void init() {
    PersistenceUnitInfo persistenceUnitInfo = persistenceUnitInfo(
        getClass().getSimpleName()
    );
 
    Map<String, Object> configuration = new HashMap<>();
 
    Integrator integrator = integrator();
    if (integrator != null) {
        configuration.put(
            "hibernate.integrator_provider",
            (IntegratorProvider) () ->
                Collections.singletonList(integrator)
        );
    }
 
    emf = new HibernatePersistenceProvider()
    .createContainerEntityManagerFactory(
        persistenceUnitInfo,
        configuration
    );
}
 
@After
public void destroy() {
    emf.close();
}

JPA 标准定义了 PersistenceProvider 接口,定义了用于实例化新 EntityManagerFactory 的契约。我们将使用 HibernatePersistenceProvider,它是此接口的特定于 Hibernate 的实现。如果您想使用不同的 JPA 提供程序,则必须检查 PersistenceProvider 实现的提供程序 API 并改用它。

现在,让我们看看 persistenceUnitInfo 长什么样:

protected PersistenceUnitInfoImpl persistenceUnitInfo(String name) {
    PersistenceUnitInfoImpl persistenceUnitInfo = new PersistenceUnitInfoImpl(
        name, entityClassNames(), properties()
    );
 
    String[] resources = resources();
    if (resources != null) {
        persistenceUnitInfo.getMappingFileNames().addAll(
            Arrays.asList(resources)
        );
    }
 
    return persistenceUnitInfo;
}

实体类和关联的 XML 配置由以下方法定义:

protected abstract Class<?>[] entities();
 
protected String[] resources() {
    return null;
}
 
protected List<String> entityClassNames() {
    return Arrays.asList(entities())
    .stream()
    .map(Class::getName)
    .collect(Collectors.toList());
}

因此,我们可以简单地实现实体或扩展资源方法以编程方式提供 JPA 映射信息。

properties 方法定义了所有测试所需的一些基本属性,例如自动生成模式或提供 JDBC 数据源以连接到底层数据库。

protected Properties properties() {
    Properties properties = new Properties();
     
    properties.put(
        "hibernate.dialect",
        dataSourceProvider().hibernateDialect()
    );
     
    properties.put(
        "hibernate.hbm2ddl.auto",
        "create-drop"
    );
     
    DataSource dataSource = newDataSource();
     
    if (dataSource != null) {
        properties.put(
            "hibernate.connection.datasource",
            dataSource
        );
    }
     
    properties.put(
        "hibernate.generate_statistics",
        Boolean.TRUE.toString()
    );
 
    return properties;
}

当然,我们可以扩展属性基类的方法来提供额外的属性。

newDataSource 方法定义如下:

protected DataSource newDataSource() {
   return proxyDataSource()
        ? dataSourceProxyType().dataSource(
            dataSourceProvider().dataSource())
        : dataSourceProvider().dataSource();
}
 
protected DataSourceProxyType dataSourceProxyType() {
    return DataSourceProxyType.DATA_SOURCE_PROXY;
}
 
protected boolean proxyDataSource() {
    return true;
}
 
protected DataSourceProvider dataSourceProvider() {
    return database().dataSourceProvider();
}
 
protected Database database() {
    return Database.HSQLDB;
}

dataSourceProxyType 允许我们代理底层的 JDBC 数据源,这样我们就可以使用 datasource-proxy 开源项目来记录 SQL 语句及其绑定参数值。

但是,您不仅限于标准 JPA 引导,因为 Hibernate 允许您通过 Integrator 机制集成您自己的引导逻辑。

默认情况下,我们不提供任何 Integrator:

protected Integrator integrator() {
    return null;
}

但是如果我们提供一个集成器,这个集成器将通过 hibernate.integrator_provider 配置属性传递给 Hibernate。

现在我们有了 AbstractJPAProgrammaticBootstrapTest,具体测试如下所示:

public class BootstrapTest
    extends AbstractJPAProgrammaticBootstrapTest {
 
    @Override
    protected Class<?>[] entities() {
        return new Class[] {
            Post.class
        };
    }
     
    @Test
    public void test() {
        doInJPA(entityManager -> {
            for (long id = 1; id <= 3; id++) {
                Post post = new Post();
                post.setId(id);
                post.setTitle(
                    String.format(
                        "High-Performance Java Persistence, Part %d", id
                    )
                );
                entityManager.persist(post);
            }
        });
    }
 
    @Entity(name = "Post")
    @Table(name = "post")
    public static class Post {
 
        @Id
        private Long id;
 
        private String title;
 
        //Getters and setters omitted for brevity
    }
}

我们只需扩展 AbstractJPAProgrammaticBootstrapTest 基类并定义我们要使用的实体。请注意,实体仅与此测试相关联,以便我们确保实体映射更改不会波及其他测试。

至于这个doInJPA工具方法的具体实现,这个以后再讨论吧。欢迎大家留言交流。

没有persistence.xml初始化一个EntityManagerFactory

更新于 2022.11.20 9分钟阅读 5 评论 5 推荐

    作者:

上一节我们介绍了通过persistence.xml初始化一个EntityManagerFactory;但是在很多种情况下是不需要使用xml配置文件来配置持久化单元的,因此,我们今天来学习如何通过编程来自动初始化一个EntityManagerFactory

Hibernate早就为我们考虑到了这一点,它允许您完全以编程方式构建一个 EntityManagerFactory,只需几行代码:

protected EntityManagerFactory newEntityManagerFactory() {
    PersistenceUnitInfo persistenceUnitInfo =
        persistenceUnitInfo(getClass().getSimpleName());
    Map<String, Object> configuration = new HashMap<>();
    configuration.put(AvailableSettings.INTERCEPTOR,
        interceptor()
    );
 
    return new EntityManagerFactoryBuilderImpl(
            new PersistenceUnitInfoDescriptor(
                persistenceUnitInfo), configuration
    ).build();
}
 
protected PersistenceUnitInfoImpl persistenceUnitInfo(
    String name) {
    return new PersistenceUnitInfoImpl(
        name, entityClassNames(), properties()
    );
}

每个测试都以一些合理的默认属性开始,并且必须在每个测试的基础上提供实体。

protected Properties properties() {
    Properties properties = new Properties();
    properties.put("hibernate.dialect",
        dataSourceProvider().hibernateDialect()
    );
    properties.put("hibernate.hbm2ddl.auto",
        "create-drop");
    DataSource dataSource = newDataSource();
    if (dataSource != null) {
        properties.put("hibernate.connection.datasource",
        dataSource);
    }
    return properties;
}
 
protected List entityClassNames() {
    return Arrays.asList(entities())
        .stream()
        .map(Class::getName)
        .collect(Collectors.toList());
}

每个测试可以定义自己的设置和实体,这样我们就可以封装整个环境。

@Override
protected Class<?>[] entities() {
    return new Class<?>[] {
        Patch.class
    };
}
 
@Entity(name = "Patch")
public class Patch {
 
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;
 
    @ElementCollection
    @CollectionTable(
            name="patch_change",
            joinColumns=@JoinColumn(name="patch_id")
    )
    @OrderColumn(name = "index_id")
    private List<Change> changes = new ArrayList<>();
 
    public List<Change> getChanges() {
        return changes;
    }
}
 
@Embeddable
public class Change {
 
    @Column(name = "path", nullable = false)
    private String path;
 
    @Column(name = "diff", nullable = false)
    private String diff;
 
    public Change() {
    }
 
    public Change(String path, String diff) {
        this.path = path;
        this.diff = diff;
    }
 
    public String getPath() {
        return path;
    }
 
    public String getDiff() {
        return diff;
    }
}

以上代码简单吧,主要就是使用了两个关键的类EntityManagerFactoryBuilderImplPersistenceUnitInfoImpl进行了封装,第一个类负责创建EntityManagerFactory,第二个类负责封装persistence.xml的相关配置信息,因此,主要还是用这个类来实现持久化单元的可编程配置。

大家如果对此感兴趣,可以留言一起讨论。嘿嘿

Mockito and Fluent APIs

发布于 2022.11.07 24分钟阅读 2 评论 5 推荐

    作者:

Fluent API 是一种基于方法链的软件工程设计技术,用于构建简洁、易读和健壮的接口。

它们通常用于建造者、工厂和其他创造性的设计模式。最近,随着 Java 的发展,它们变得越来越流行,并且可以在 Java Stream API 和 Mockito 测试框架等流行的 API 中找到。

然而,模拟 Fluent API 可能会很痛苦,因为我们经常需要设置一个复杂的模拟对象层次结构。

在本文中,我们将看看如何使用 Mockito 的强大功能来避免这种情况。

一个简单的 Fluent API

我们将使用构建器设计模式来说明用于构建披萨对象的简单流畅 API:

public class Pizza {
    public enum PizzaSize {
        LARGE, MEDIUM, SMALL;
    }

    private String name;
    private PizzaSize size;
    private List<String> toppings;
    private boolean stuffedCrust;
    private boolean collect;
    private Integer discount;

    private Pizza(PizzaBuilder builder) {
        this.name = builder.name;
        this.size = builder.size;
        this.toppings = builder.toppings;
        this.stuffedCrust = builder.stuffedCrust;
        this.collect = builder.collect;
        this.discount = builder.discount;
    }

    public String getName() {
        return name;
    }

    public PizzaSize getSize() {
        return size;
    }

    public List<String> getToppings() {
        return toppings;
    }

    public boolean isStuffedCrust() {
        return stuffedCrust;
    }

    public boolean isCollecting() {
        return collect;
    }

    public Integer getDiscount() {
        return discount;
    }

    public static class PizzaBuilder {
        private String name;
        private PizzaSize size;

        private List<String> toppings;
        private boolean stuffedCrust;
        private boolean collect;
        private Integer discount = null;

        public PizzaBuilder() {
        }

        public PizzaBuilder name(String name) {
            this.name = name;
            return this;
        }

        public PizzaBuilder size(PizzaSize size) {
            this.size = size;
            return this;
        }

        public PizzaBuilder withExtraTopping(String extraTopping) {
            if (this.toppings == null) {
                toppings = new ArrayList<>();
            }
            this.toppings.add(extraTopping);
            return this;
        }

        public PizzaBuilder withStuffedCrust(boolean stuffedCrust) {
            this.stuffedCrust = stuffedCrust;
            return this;
        }

        public PizzaBuilder willCollect(boolean collect) {
            this.collect = collect;
            return this;
        }

        public PizzaBuilder applyDiscount(Integer discount) {
            this.discount = discount;
            return this;
        }

        public Pizza build() {
            return new Pizza(this);
        }
    }
}

正如我们所见,我们创建了一个易于理解的 API,它读起来像 DSL,并允许我们创建具有各种特征的 Pizza 对象。

现在我们将定义一个使用我们的构建器的简单服务类。这将是我们稍后要测试的类:

public class PizzaService {

    private Pizza.PizzaBuilder builder;

    public PizzaService(Pizza.PizzaBuilder builder) {
        this.builder = builder;
    }

    public Pizza orderHouseSpecial() {
        return builder.name("Special")
          .size(PizzaSize.LARGE)
          .withExtraTopping("Mushrooms")
          .withStuffedCrust(true)
          .withExtraTopping("Chilli")
          .willCollect(true)
          .applyDiscount(20)
          .build();
    }
}

我们的服务非常简单,包含一个名为 orderHouseSpecial 的方法。顾名思义,我们可以使用这种方法来构建具有一些预定义属性的特殊披萨。

传统的Mock

以传统方式使用模拟存根将需要我们创建八个模拟 PizzaBuilder 对象。我们需要一个由 name 方法返回的 PizzaBuilder 的模拟,然后是一个由 size 方法返回的 PizzaBuilder 的模拟,等等。我们将继续这种方式,直到我们满足流式 API 链中的所有方法调用。

现在让我们看看我们如何编写一个单元测试来使用传统的 Mockito 模拟来测试我们的服务方法:

@RunWith(MockitoJUnitRunner.class)
public class PizzaServiceTest {

    @Mock
    private Pizza expectedPizza;

    @Mock(answer = Answers.RETURNS_DEEP_STUBS)
    private PizzaBuilder anotherbuilder;

    @Captor
    private ArgumentCaptor<String> stringCaptor;
    @Captor
    private ArgumentCaptor<Pizza.PizzaSize> sizeCaptor;

    @Test
    public void givenTraditonalMocking_whenServiceInvoked_thenPizzaIsBuilt() {
        PizzaBuilder nameBuilder = Mockito.mock(Pizza.PizzaBuilder.class);
        PizzaBuilder sizeBuilder = Mockito.mock(Pizza.PizzaBuilder.class);
        PizzaBuilder firstToppingBuilder = Mockito.mock(Pizza.PizzaBuilder.class);
        PizzaBuilder secondToppingBuilder = Mockito.mock(Pizza.PizzaBuilder.class);
        PizzaBuilder stuffedBuilder = Mockito.mock(Pizza.PizzaBuilder.class);
        PizzaBuilder willCollectBuilder = Mockito.mock(Pizza.PizzaBuilder.class);
        PizzaBuilder discountBuilder = Mockito.mock(Pizza.PizzaBuilder.class);

        PizzaBuilder builder = Mockito.mock(Pizza.PizzaBuilder.class);
        when(builder.name(anyString())).thenReturn(nameBuilder);
        when(nameBuilder.size(any(Pizza.PizzaSize.class))).thenReturn(sizeBuilder);
        when(sizeBuilder.withExtraTopping(anyString())).thenReturn(firstToppingBuilder);
        when(firstToppingBuilder.withStuffedCrust(anyBoolean())).thenReturn(stuffedBuilder);
        when(stuffedBuilder.withExtraTopping(anyString())).thenReturn(secondToppingBuilder);
        when(secondToppingBuilder.willCollect(anyBoolean())).thenReturn(willCollectBuilder);
        when(willCollectBuilder.applyDiscount(anyInt())).thenReturn(discountBuilder);
        when(discountBuilder.build()).thenReturn(expectedPizza);

        PizzaService service = new PizzaService(builder);
        Pizza pizza = service.orderHouseSpecial();
        assertEquals("Expected Pizza", expectedPizza, pizza);

        verify(builder).name(stringCaptor.capture());
        assertEquals("Pizza name: ", "Special", stringCaptor.getValue());

        // rest of test verification
    }

    @Test
    public void givenDeepStubs_whenServiceInvoked_thenPizzaIsBuilt() {
        Mockito.when(anotherbuilder.name(anyString())
                        .size(any(Pizza.PizzaSize.class))
                        .withExtraTopping(anyString())
                        .withStuffedCrust(anyBoolean())
                        .withExtraTopping(anyString())
                        .willCollect(anyBoolean())
                        .applyDiscount(anyInt())
                        .build())
                .thenReturn(expectedPizza);

        PizzaService service = new PizzaService(anotherbuilder);
        assertEquals("Expected Pizza", expectedPizza, service.orderHouseSpecial());
    }
}

在这个例子中,我们需要模拟我们提供给 PizzaService 的 PizzaBuilder。正如我们所看到的,这不是一项简单的任务,因为我们需要返回一个模拟,它将为我们流式 API 中的每个调用返回一个模拟。

这导致了复杂的模拟对象层次结构,难以理解并且难以维护。

要创建一个深度存根,我们只需在创建模拟时添加 Mockito.RETURNS_DEEP_STUBS 常量作为附加参数:

通过使用 Mockito.RETURNS_DEEP_STUBS 参数,我们告诉 Mockito 进行一种深度模拟。这使得可以一次性模拟完整方法链的结果,或者在我们的例子中是 fluent API。

值得庆幸的是,Mockito 提供了一个非常简洁的功能,称为深度存根,它允许我们在创建模拟时指定应答模式。

这导致了一个更优雅的解决方案和一个比我们在上一节中看到的更容易理解的测试。从本质上讲,我们避免了创建复杂的模拟对象层次结构的需要。

我们也可以直接通过@Mock 注解使用这种应答模式:

@Mock(answer = Answers.RETURNS_DEEP_STUBS)
private PizzaBuilder anotherBuilder;

需要注意的一点是,验证仅适用于链中的最后一个模拟。

总结

在本文中,我们已经看到了如何使用 Mockito 来模拟一个简单的流式 API。首先,我们研究了一种传统的模拟方法,并了解了与这种方法相关的困难。

然后我们查看了一个使用 Mockito 鲜为人知的特性(称为深度存根)的示例,它允许以更优雅的方式模拟我们的流式 API。

Mockito ArgumentCaptor @Captor

发布于 2022.11.07 9分钟阅读 6 评论 5 推荐

    作者:

Mockito ArgumentCaptor 用于捕获模拟方法的参数。ArgumentCaptor 与 Mockito verify() 方法一起使用,以在调用任何方法时获取传递的参数。这样,我们可以为我们的测试提供额外的 JUnit 断言。

Mockito ArgumentCaptor

我们可以为任何类创建 ArgumentCaptor 实例,然后它的 capture() 方法与 verify() 方法一起使用。最后,我们可以从 getValue() 和 getAllValues() 方法中获取捕获的参数。当我们捕获单个参数时,可以使用 getValue() 方法。如果多次调用验证的方法,则 getValue() 方法将返回最新捕获的值。如果捕获了多个参数,则调用 getAllValues() 以获取参数列表。

举个例子:

public class MathUtils {
    public int add(int x, int y) {
        return x + y;
    }

    public boolean isInteger(String s) {
        try {
            Integer.parseInt(s);
        } catch (NumberFormatException e) {
            return false;
        }
        return true;
    }

    public long squareLong(long l) {
        return l*l;
    }
}

测试代码如下:

@RunWith(MockitoJUnitRunner.class)
public class MathUtilsTest {
    @Test
    public void test() {
        MathUtils mockMathUtils = mock(MathUtils.class);
        when(mockMathUtils.add(1, 1)).thenReturn(2);
        when(mockMathUtils.isInteger(anyString())).thenReturn(true);

        ArgumentCaptor<Integer> acInteger = ArgumentCaptor.forClass(Integer.class);
        ArgumentCaptor<String> acString = ArgumentCaptor.forClass(String.class);

        assertEquals(2, mockMathUtils.add(1, 1));
        assertTrue(mockMathUtils.isInteger("1"));
        assertTrue(mockMathUtils.isInteger("999"));

        verify(mockMathUtils).add(acInteger.capture(), acInteger.capture());
        List allValues = acInteger.getAllValues();
        assertEquals(List.of(1, 1), allValues);

        verify(mockMathUtils, times(2)).isInteger(acString.capture());
        List allStringValues = acString.getAllValues();
        assertEquals(List.of("1", "999"), allStringValues);
    }
}

这里说明一下:在第一个加法当中,我们首先通过 verify(mockMathUtils).add(acInteger.capture(), acInteger.capture());来捕获参数,这里显然捕获到的是1和1,然后将所有的值取出来进行比较;同理,在第二个验证方法是不是整数中,分别用不同的参数调用了两次,然后将其值捕获后进行比较。

 

Mockito @Captor

我们可以使用@Captor 注释在字段级别创建参数捕获器。因此,不要将字段级别 ArgumentCaptor 初始化为:

ArgumentCaptor<Long> acLong = ArgumentCaptor.forClass(Long.class);

我们可以将@Captor 用作:

@Captor 
ArgumentCaptor<Long> acLong;

请注意,我们必须调用 MockitoAnnotations.initMocks(this);在测试方法之前让它被 Mockito 框架初始化。

@Captor
ArgumentCaptor<Long> acLong;

@Test
public void testAnnotation() {
    MathUtils mockMathUtils = mock(MathUtils.class);
    when(mockMathUtils.squareLong(2L)).thenReturn(4L);
    assertEquals(4L, mockMathUtils.squareLong(2L));
    verify(mockMathUtils).squareLong(acLong.capture());
    assertTrue(2 == acLong.getValue());
}

在这种情况下,我只捕获了单个的值,所以直接使用了getValue()方法来获取值;

这些基础知识挺简答的,但是组合起来就会发挥巨大的作用。

 

Mockito——Resetting Mock

发布于 2022.11.07 12分钟阅读 2 评论 5 推荐

    作者:

Mockito 提供了重置Mock的功能,以便以后可以重用。看看下面的代码片段。

//reset mock
reset(calcService);

在这里,我们重置了Mock对象。 MathApplication 使用 calcService 并在重置模拟后,使用模拟方法将失败测试。

举一个例子:

// @RunWith attaches a runner with the test class to initialize the test data
@RunWith(MockitoJUnitRunner.class)
public class MathApplicationTester {
	
   private MathApplication mathApplication;
   private CalculatorService calcService;

   @Before
   public void setUp(){
      mathApplication = new MathApplication();
      calcService = mock(CalculatorService.class);
      mathApplication.setCalculatorService(calcService);
   }

   @Test
   public void testAddAndSubtract(){

      //add the behavior to add numbers
      when(calcService.add(20.0,10.0)).thenReturn(30.0);
  
      //test the add functionality
      Assert.assertEquals(mathApplication.add(20.0, 10.0),30.0,0);

      //reset the mock	  
      reset(calcService);

      //test the add functionality after resetting the mock
      Assert.assertEquals(mathApplication.add(20.0, 10.0),30.0,0);   
   }
}

大家注意在这里,testAddAndSubtract测试中,第一个测试能够通过,但是第二个测试开始前,我们重置了Mock,这样第二个测试显然就不能通过啦。这里问一个问题,重置以后,这个新的计算返回的值应该是哪里的呢?

输出结果如下:

testAddAndSubtract(MathApplicationTester): expected:<0.0> but was:<30.0>
false

 

行为驱动开发是一种编写测试的风格,使用给定、何时以及格式化为测试方法。 Mockito 提供了特殊的方法来做到这一点。看看下面的代码片段。

/Given
given(calcService.add(20.0,10.0)).willReturn(30.0);

//when
double result = calcService.add(20.0,10.0);

//then
Assert.assertEquals(result,30.0,0);

在这里,我们使用 BDDMockito 类的given方法而不是when .

举个例子:

// @RunWith attaches a runner with the test class to initialize the test data
@RunWith(MockitoJUnitRunner.class)
public class MathApplicationTester {
	
   private MathApplication mathApplication;
   private CalculatorService calcService;

   @Before
   public void setUp(){
      mathApplication = new MathApplication();
      calcService = mock(CalculatorService.class);
      mathApplication.setCalculatorService(calcService);
   }

   @Test
   public void testAdd(){

      //Given
      given(calcService.add(20.0,10.0)).willReturn(30.0);

      //when
      double result = calcService.add(20.0,10.0);

      //then
      Assert.assertEquals(result,30.0,0);   
   }
}

使用以上写法的好处是代码给人一种见文知意的感觉。

 

Mockito 提供了一个特殊的 Timeout 选项来测试一个方法是否在规定的时间范围内被调用。

语法如下:

//passes when add() is called within 100 ms.
verify(calcService,timeout(100)).add(20.0,10.0);

还是用一个例子来说明:

// @RunWith attaches a runner with the test class to initialize the test data
@RunWith(MockitoJUnitRunner.class)
public class MathApplicationTester {
	
   private MathApplication mathApplication;
   private CalculatorService calcService;

   @Before
   public void setUp(){
      mathApplication = new MathApplication();
      calcService = mock(CalculatorService.class);
      mathApplication.setCalculatorService(calcService);
   }

   @Test
   public void testAddAndSubtract(){

      //add the behavior to add numbers
      when(calcService.add(20.0,10.0)).thenReturn(30.0);

      //subtract the behavior to subtract numbers
      when(calcService.subtract(20.0,10.0)).thenReturn(10.0);

      //test the subtract functionality
      Assert.assertEquals(mathApplication.subtract(20.0, 10.0),10.0,0);

      //test the add functionality
      Assert.assertEquals(mathApplication.add(20.0, 10.0),30.0,0);

      //verify call to add method to be completed within 100 ms
      verify(calcService, timeout(100)).add(20.0,10.0);
	  
      //invocation count can be added to ensure multiplication invocations
      //can be checked within given timeframe
      verify(calcService, timeout(100).times(1)).subtract(20.0,10.0);
   }
}

这里的例子意思是加法和减法只要在100ms以内执行都算测试通过。

Mockito——回调

发布于 2022.11.06 11分钟阅读 2 评论 5 推荐

    作者:

Callback

Mockito 提供了一个 Answer 接口,它允许使用通用接口进行stubbing

语法

//add the behavior to add numbers
when(calcService.add(20.0,10.0)).thenAnswer(new Answer<Double>() {
   @Override
   public Double answer(InvocationOnMock invocation) throws Throwable {
      //get the arguments passed to mock
      Object[] args = invocation.getArguments();
      //get the mock 
      Object mock = invocation.getMock();	
      //return the result
      return 30.0;
   }
});

举一个使用的例子:

// @RunWith attaches a runner with the test class to initialize the test data
@RunWith(MockitoJUnitRunner.class)
public class MathApplicationTester {
	
   private MathApplication mathApplication;
   private CalculatorService calcService;

   @Before
   public void setUp(){
      mathApplication = new MathApplication();
      calcService = mock(CalculatorService.class);
      mathApplication.setCalculatorService(calcService);
   }

   @Test
   public void testAdd(){

      //add the behavior to add numbers
      when(calcService.add(20.0,10.0)).thenAnswer(new Answer<Double>() {

         @Override
         public Double answer(InvocationOnMock invocation) throws Throwable {
            //get the arguments passed to mock
            Object[] args = invocation.getArguments();
			
            //get the mock 
            Object mock = invocation.getMock();	
			
            //return the result
            return 30.0;
         }
      });

      //test the add functionality
      Assert.assertEquals(mathApplication.add(20.0, 10.0),30.0,0);
   }
}

我们通过在其中注入一个calculatorService 的模拟来测试MathApplication 类。 Mock 将由 Mockito 创建。我们通过回调来实现一个接口来模拟方法的返回。

 

Spy

Mockito 提供了在真实对象上创建间谍的选项。当调用 spy 时,会调用真实对象的实际方法。

语法:

//create a spy on actual object
calcService = spy(calculator);

//perform operation on real object
//test the add functionality
Assert.assertEquals(mathApplication.add(20.0, 10.0),30.0,0);

还是用一个例子来说明:

// @RunWith attaches a runner with the test class to initialize the test data
@RunWith(MockitoJUnitRunner.class)
public class MathApplicationTester {
	
   private MathApplication mathApplication;
   private CalculatorService calcService;

   @Before
   public void setUp(){
      mathApplication = new MathApplication();
      Calculator calculator = new Calculator();
      calcService = spy(calculator);
      mathApplication.setCalculatorService(calcService);	     
   }

   @Test
   public void testAdd(){

      //perform operation on real object
      //test the add functionality
      Assert.assertEquals(mathApplication.add(20.0, 10.0),30.0,0);
   }

   class Calculator implements CalculatorService {
      @Override
      public double add(double input1, double input2) {
         return input1 + input2;
      }

      @Override
      public double subtract(double input1, double input2) {
         throw new UnsupportedOperationException("Method not implemented yet!");
      }

      @Override
      public double multiply(double input1, double input2) {
         throw new UnsupportedOperationException("Method not implemented yet!");
      }

      @Override
      public double divide(double input1, double input2) {
         throw new UnsupportedOperationException("Method not implemented yet!");
      }
   }
}

看过代码后很容易理解,mock一般是对一个接口,而spy是对一个对象实例;这里提出一个问题,它们之间有什么区别呢?

 

Mockito——创建Mock

发布于 2023.01.12 11分钟阅读 6 评论 5 推荐

    作者:

Mock创建

到目前为止,我们已经使用注解来创建Mock。 Mockito 提供了各种方法来创建模拟对象。 mock() 创建模拟,而不用担心Mock将在适当的时候进行的方法调用的顺序。

创建语法:

calcService = mock(CalculatorService.class);

我们再来看看一个具体的例子:

// @RunWith attaches a runner with the test class to initialize the test data
@RunWith(MockitoJUnitRunner.class)
public class MathApplicationTester {
	
   private MathApplication mathApplication;
   private CalculatorService calcService;

   @Before
   public void setUp(){
      mathApplication = new MathApplication();
      calcService = mock(CalculatorService.class);
      mathApplication.setCalculatorService(calcService);
   }

   @Test
   public void testAddAndSubtract(){

      //add the behavior to add numbers
      when(calcService.add(20.0,10.0)).thenReturn(30.0);

      //subtract the behavior to subtract numbers
      when(calcService.subtract(20.0,10.0)).thenReturn(10.0);

      //test the subtract functionality
      Assert.assertEquals(mathApplication.subtract(20.0, 10.0),10.0,0);

      //test the add functionality
      Assert.assertEquals(mathApplication.add(20.0, 10.0),30.0,0);

      //verify call to calcService is made or not
      verify(calcService).add(20.0,10.0);
      verify(calcService).subtract(20.0,10.0);
   }
}

以上代码通过在其中注入一个calculatorService 的模拟来测试MathApplication 类。 Mock 将由 Mockito 创建。 在这里,我们通过 when() 向模拟对象添加了两个模拟方法调用 add() 和减法()。然而,在测试期间,我们在调用 add() 之前调用了 subtract()。当我们使用 create() 创建模拟对象时,方法的执行顺序无关紧要。

下面我们再说说

有序验证

Mockito 提供了 Inorder 类,该类负责处理模拟将在适当的时候进行的方法调用的顺序。

语法:

//create an inOrder verifier for a single mock
InOrder inOrder = inOrder(calcService);

//following will make sure that add is first called then subtract is called.
inOrder.verify(calcService).add(20.0,10.0);
inOrder.verify(calcService).subtract(20.0,10.0);

我们还是用一个例子来说明

// @RunWith attaches a runner with the test class to initialize the test data
@RunWith(MockitoJUnitRunner.class)
public class MathApplicationTester {
	
   private MathApplication mathApplication;
   private CalculatorService calcService;

   @Before
   public void setUp(){
      mathApplication = new MathApplication();
      calcService = mock(CalculatorService.class);
      mathApplication.setCalculatorService(calcService);
   }

   @Test
   public void testAddAndSubtract(){

      //add the behavior to add numbers
      when(calcService.add(20.0,10.0)).thenReturn(30.0);

      //subtract the behavior to subtract numbers
      when(calcService.subtract(20.0,10.0)).thenReturn(10.0);

      //test the add functionality
      Assert.assertEquals(mathApplication.add(20.0, 10.0),30.0,0);

      //test the subtract functionality
      Assert.assertEquals(mathApplication.subtract(20.0, 10.0),10.0,0);

      //create an inOrder verifier for a single mock
      InOrder inOrder = inOrder(calcService);

      //following will make sure that add is first called then subtract is called.
      inOrder.verify(calcService).subtract(20.0,10.0);
      inOrder.verify(calcService).add(20.0,10.0);

   }
}

我们通过在其中注入一个calculatorService 的模拟来测试MathApplication 类。 Mock 将由 Mockito 创建。

在这里,我们通过 when() 向模拟对象添加了两个模拟方法调用 add() 和减法()。然而,在测试期间,我们在调用 add() 之前调用了 subtract()。当我们使用 Mockito 创建模拟对象时,方法的执行顺序无关紧要。使用 InOrder 类,我们可以确保调用顺序。

注意,在这里我特意打乱了顺序,让测试不通过,这样来验证InOrder inOrder = inOrder(calcService)这个功能的作用。

Mockito基本测试功能

发布于 2022.11.03 9分钟阅读 1 评论 5 推荐

    作者:

在上一篇中,我们知道了如何测试了如何验证mock的方法是否被调用,以及mock出来的方法的返回值等。今天我们继续讨论这些话题。

预期调用计数

Mockito 提供了以下附加方法来改变预期的调用计数

atLeast (int min) - 期望最小调用。 

atLeastOnce () - 至少需要一个调用。 

atMost (int max) - 期望最大调用次数。

我们看看代码的例子:

// @RunWith attaches a runner with the test class to initialize the test data
@RunWith(MockitoJUnitRunner.class)
public class MathApplicationTester {
	
   //@InjectMocks annotation is used to create and inject the mock object
   @InjectMocks 
   MathApplication mathApplication = new MathApplication();

   //@Mock annotation is used to create the mock object to be injected
   @Mock
   CalculatorService calcService;

   @Test
   public void testAdd(){
      //add the behavior of calc service to add two numbers
      when(calcService.add(10.0,20.0)).thenReturn(30.00);
		
      //add the behavior of calc service to subtract two numbers
      when(calcService.subtract(20.0,10.0)).thenReturn(10.00);
      
      //test the add functionality
      Assert.assertEquals(mathApplication.add(10.0, 20.0),30.0,0);
      Assert.assertEquals(mathApplication.add(10.0, 20.0),30.0,0);
      Assert.assertEquals(mathApplication.add(10.0, 20.0),30.0,0);
      
      //test the subtract functionality
      Assert.assertEquals(mathApplication.subtract(20.0, 10.0),10.0,0.0);
      
      //check a minimum 1 call count
      verify(calcService, atLeastOnce()).subtract(20.0, 10.0);
      
      //check if add function is called minimum 2 times
      verify(calcService, atLeast(2)).add(10.0, 20.0);
      
      //check if add function is called maximum 3 times
      verify(calcService, atMost(3)).add(10.0,20.0);     
   }
}

异常处理

Mockito 为模拟提供了抛出异常的能力,因此可以测试异常处理。看看下面的代码片段。

//add the behavior to throw exception
doThrow(new Runtime Exception("divide operation not implemented"))
   .when(calcService).add(10.0,20.0);

下面是一个例子:

// @RunWith attaches a runner with the test class to initialize the test data
@RunWith(MockitoRunner.class)
public class MathApplicationTester {
	
   // @TestSubject annotation is used to identify class 
      which is going to use the mock object
   @TestSubject
   MathApplication mathApplication = new MathApplication();

   //@Mock annotation is used to create the mock object to be injected
   @Mock
   CalculatorService calcService;

   @Test(expected = RuntimeException.class)
   public void testAdd(){
      //add the behavior to throw exception
      doThrow(new RuntimeException("Add operation not implemented"))
         .when(calcService).add(10.0,20.0);

      //test the add functionality
      Assert.assertEquals(mathApplication.add(10.0, 20.0),30.0,0); 
   }
}

好了,结合上文的Mockito基本使用功能以及这一节的预期调用计数和异常处理,我们可以使用Mockito测试我们平时工作中的很多情况。

下一篇中,我们来学习如何创建Mock以及其他一些基础知识。

 

Mockito整合JUnit

更新于 2022.11.02 18分钟阅读 5 评论 5 推荐

    作者:

在本文中,我们将学习如何将 JUnit 和 Mockito 集成在一起。在这里,我们将创建一个数学应用程序,它使用 CalculatorService 执行基本的数学运算,例如加法、减法、乘法和除法。

我们将使用 Mockito 来模拟 CalculatorService 的虚拟实现。此外,我们还广泛使用注解来展示它们与 JUnit 和 Mockito 的兼容性。

下面将逐步讨论该过程。

第 1 步 - 创建一个名为 CalculatorService 的接口以提供数学函数

CalculatorService.java

public interface CalculatorService {
   public double add(double input1, double input2);
   public double subtract(double input1, double input2);
   public double multiply(double input1, double input2);
   public double divide(double input1, double input2);
}

第 2 步 - 创建一个 JAVA 类来表示 MathApplication

MathApplication.java

public class MathApplication {
   private CalculatorService calcService;

   public void setCalculatorService(CalculatorService calcService){
      this.calcService = calcService;
   }
   
   public double add(double input1, double input2){
      return calcService.add(input1, input2);
   }
   
   public double subtract(double input1, double input2){
      return calcService.subtract(input1, input2);
   }
   
   public double multiply(double input1, double input2){
      return calcService.multiply(input1, input2);
   }
   
   public double divide(double input1, double input2){
      return calcService.divide(input1, input2);
   }
}

第 3 步 - 测试 MathApplication 类

让我们通过在其中注入一个calculatorService 的模拟来测试MathApplication 类。 Mock 将由 Mockito 创建。

MathApplicationTester.java

import static org.mockito.Mockito.when;

import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.runners.MockitoJUnitRunner;

// @RunWith attaches a runner with the test class to initialize the test data
@RunWith(MockitoJUnitRunner.class)
public class MathApplicationTester {
	
   //@InjectMocks annotation is used to create and inject the mock object
   //这里这个对象是可以通过set注入来创建,它注入的实际上就是下面的@Mock注解的对象
   @InjectMocks 
   MathApplication mathApplication = new MathApplication();

   //@Mock annotation is used to create the mock object to be injected
   @Mock
   CalculatorService calcService;

   @Test
   public void testAdd(){
      //add the behavior of calc service to add two numbers
      when(calcService.add(10.0,20.0)).thenReturn(30.00);
		
      //test the add functionality
      Assert.assertEquals(mathApplication.add(10.0, 20.0),30.0,0);
   }
}

第四步 - 创建一个类来执行测试用例

在 C> Mockito_WORKSPACE 中创建一个名为 TestRunner 的 java 类文件来执行测试用例。

TestRunner.java

import org.junit.runner.JUnitCore;
import org.junit.runner.Result;
import org.junit.runner.notification.Failure;

public class TestRunner {
   public static void main(String[] args) {
      Result result = JUnitCore.runClasses(MathApplicationTester.class);
      
      for (Failure failure : result.getFailures()) {
         System.out.println(failure.toString());
      }
      
      System.out.println(result.wasSuccessful());
   }
}  	

第 5 步 - 验证结果

使用 javac 编译器编译类如下

C:\Mockito_WORKSPACE>javac CalculatorService.java MathApplication.
   java MathApplicationTester.java TestRunner.java

现在运行 Test Runner 以查看结果

C:\Mockito_WORKSPACE>java TestRunner

验证输出

true

为mock添加行为

有了上面的例子,我们可以学习第一个基本的Mockito测试功能,那就是为被测试的接口添加行为,这里的具体代码就是:

//add the behavior of calc service to add two numbers
when(calcService.add(10.0,20.0)).thenReturn(30.00);

这里的意义就是为mock对象calcService的add方法添加了一个行为就是10.0+20.0返回30;在这里添加的是正确的行为?如果您感兴趣的话,可以试试这样:

//add the behavior of calc service to add two numbers
when(calcService.add(10.0,20.0)).thenReturn(50.00);
//很明显这是一个错误的行为

验证mock的行为

验证mock的行为如下:

//test the add functionality
Assert.assertEquals(calcService.add(10.0, 20.0),30.0,0);

//verify call to calcService is made or not with same arguments.
verify(calcService).add(10.0, 20.0);
//这里验证mock对象calcService的add方法是否被调用

同样的到来,您试试这样写会有什么效果?

//test the add functionality
Assert.assertEquals(calcService.add(10.0, 20.0),30.0,0);

//verify call to calcService is made or not with same arguments.
verify(calcService).add(10.0, 30.0);
//这里验证mock对象calcService的add方法是否被调用

验证mock的期望调用

期望调用的diamante如下:

when(calcService.add(10.0,20.0)).thenReturn(30.00);

//limit the method call to 1, no less and no more calls are allowed
verify(calcService, times(1)).add(10.0, 20.0);

下面是一个具体的期望调用的例子:

   @Test
   public void testAdd(){
      //add the behavior of calc service to add two numbers
      when(calcService.add(10.0,20.0)).thenReturn(30.00);
		
      //add the behavior of calc service to subtract two numbers
      when(calcService.subtract(20.0,10.0)).thenReturn(10.00);
      
      //test the add functionality
      Assert.assertEquals(mathApplication.add(10.0, 20.0),30.0,0);
      Assert.assertEquals(mathApplication.add(10.0, 20.0),30.0,0);
      Assert.assertEquals(mathApplication.add(10.0, 20.0),30.0,0);
      
      //test the subtract functionality
      Assert.assertEquals(mathApplication.subtract(20.0, 10.0),10.0,0.0);
      
      //default call count is 1 
      verify(calcService).subtract(20.0, 10.0);
      
      //check if add function is called three times
      verify(calcService, times(3)).add(10.0, 20.0);
      
      //verify that method was never called on a mock
      verify(calcService, never()).multiply(10.0,20.0);
   }

好了,今天我们简单理解一下以上的测试功能,后续的其他测试功能我们下一篇文章再继续。

Mockito第一个demo

发布于 2022.10.30 11分钟阅读 5 评论 5 推荐

    作者:

在深入了解 Mockito 框架的细节之前,让我们先看看一个正在运行的应用程序。在这个例子中,我们创建了一个股票服务的模拟来获取一些股票的虚拟价格,并对一个名为 Portfolio 的 java 类进行了单元测试。

下面将逐步讨论该过程。

第 1 步 - 创建一个 JAVA 类来表示股票

Stock.java

public class Stock {
   private String stockId;
   private String name;	
   private int quantity;

   public Stock(String stockId, String name, int quantity){
      this.stockId = stockId;
      this.name = name;		
      this.quantity = quantity;		
   }

   public String getStockId() {
      return stockId;
   }

   public void setStockId(String stockId) {
      this.stockId = stockId;
   }

   public int getQuantity() {
      return quantity;
   }

   public String getTicker() {
      return name;
   }
}

第 2 步 - 创建接口 StockService 以获取股票价格

StockService.java

public interface StockService {
   public double getPrice(Stock stock);
}

第 3 步 - 创建一个 Portfolio 类来代表任何客户的投资组合

Portfolio.java

import java.util.List;

public class Portfolio {
   private StockService stockService;
   private List<Stock> stocks;

   public StockService getStockService() {
      return stockService;
   }
   
   public void setStockService(StockService stockService) {
      this.stockService = stockService;
   }

   public List<Stock> getStocks() {
      return stocks;
   }

   public void setStocks(List<Stock> stocks) {
      this.stocks = stocks;
   }

   public double getMarketValue(){
      double marketValue = 0.0;
      
      for(Stock stock:stocks){
         marketValue += stockService.getPrice(stock) * stock.getQuantity();
      }
      return marketValue;
   }
}

第 4 步 - 测试 Portfolio 类

让我们通过在其中注入 stockservice 的模拟来测试 Portfolio 类。 Mock 将由 Mockito 创建。

PortfolioTester.java

package com.tutorialspoint.mock;

import java.util.ArrayList;
import java.util.List;

import static org.mockito.Mockito.*;

public class PortfolioTester {
	
   Portfolio portfolio;	
   StockService stockService;
	   
   
   public static void main(String[] args){
      PortfolioTester tester = new PortfolioTester();
      tester.setUp();
      System.out.println(tester.testMarketValue()?"pass":"fail");
   }
   
   public void setUp(){
      //Create a portfolio object which is to be tested		
      portfolio = new Portfolio();		
  
      //Create the mock object of stock service
      stockService = mock(StockService.class);		
  
      //set the stockService to the portfolio
      portfolio.setStockService(stockService);
   }
   
   public boolean testMarketValue(){
    	   
      //Creates a list of stocks to be added to the portfolio
      List<Stock> stocks = new ArrayList<Stock>();
      Stock googleStock = new Stock("1","Google", 10);
      Stock microsoftStock = new Stock("2","Microsoft",100);	
 
      stocks.add(googleStock);
      stocks.add(microsoftStock);

      //add stocks to the portfolio
      portfolio.setStocks(stocks);

      //mock the behavior of stock service to return the value of various stocks
      when(stockService.getPrice(googleStock)).thenReturn(50.00);
      when(stockService.getPrice(microsoftStock)).thenReturn(1000.00);		

      double marketValue = portfolio.getMarketValue();		
      return marketValue == 100500.0;
   }
}

第 5 步 - 验证结果

使用 javac 编译器编译类如下

C:\Mockito_WORKSPACE>javac Stock.java StockService.java Portfolio.java PortfolioTester.java

现在运行 PortfolioTester 来查看结果

C:\Mockito_WORKSPACE>java PortfolioTester

验证输出

pass

 

Mockito初步认识

发布于 2022.10.27 9分钟阅读 4 评论 5 推荐

    作者:

Mockito 是一个模拟框架,基于 JAVA 的库,用于对 JAVA 应用程序进行有效的单元测试。Mockito 用于模拟接口,以便可以将虚拟功能添加到可用于单元测试的模拟接口中。本笔记将帮助您了解如何使用 Mockito 创建单元测试以及如何以简单直观的方式使用其 API。

本系列文章适用于希望通过单元测试和测试驱动开发来提高软件质量的从新手到专家级别的 Java 开发人员。学习完成本系利文章后,您应该对 Mockito 有足够的了解,从那里您可以将自己带到下一个专业水平。

读者必须具备 JAVA 编程语言的工作知识才能充分利用本教程。 JUnit 的知识是一个额外的优势。

 

什么是Mocking?

Mocking是一种单独测试类功能的方法。Mocking不需要数据库连接或属性文件读取或文件服务器读取来测试功能。模拟对象模拟真实的服务。模拟对象返回对应于传递给它的一些虚拟输入的虚拟数据。

Mockito

Mockito 有助于无缝地创建模拟对象。它使用 Java 反射来为给定的接口创建模拟对象。模拟对象只不过是实际实现的代理。

考虑一个返回股票价格细节的股票服务案例。在开发过程中,无法使用实际的库存服务来获取实时数据。所以我们需要一个股票服务的虚拟实现。顾名思义,Mockito 可以很容易地做到这一点。

Mockito 的好处

No Handwriting - 无需自己编写模拟对象。

Refactoring Safe - 重命名接口方法名称或重新排序参数不会破坏测试代码,因为 Mocks 是在运行时创建的。

Return value support  - 支持返回值。

Exception support - 支持异常。

Order check support - 支持检查方法调用的顺序。

Annotation support  - 支持使用注释创建模拟。

考虑以下代码片段。

package com.tutorialspoint.mock;

import java.util.ArrayList;
import java.util.List;

import static org.mockito.Mockito.*;

public class PortfolioTester {
   public static void main(String[] args){

      //Create a portfolio object which is to be tested		
      Portfolio portfolio = new Portfolio();

      //Creates a list of stocks to be added to the portfolio
      List<Stock> stocks = new ArrayList<Stock>();
      Stock googleStock = new Stock("1","Google", 10);
      Stock microsoftStock = new Stock("2","Microsoft",100);

      stocks.add(googleStock);
      stocks.add(microsoftStock);		

      //Create the mock object of stock service
      StockService stockServiceMock = mock(StockService.class);

      // mock the behavior of stock service to return the value of various stocks
      when(stockServiceMock.getPrice(googleStock)).thenReturn(50.00);
      when(stockServiceMock.getPrice(microsoftStock)).thenReturn(1000.00);

      //add stocks to the portfolio
      portfolio.setStocks(stocks);

      //set the stockService to the portfolio
      portfolio.setStockService(stockServiceMock);

      double marketValue = portfolio.getMarketValue();

      //verify the market value to be 
      //10*50.00 + 100* 1000.00 = 500.00 + 100000.00 = 100500
      System.out.println("Market value of the portfolio: "+ marketValue);
   }
}

让我们了解上述程序的重要概念。完整的代码可在下一节中可以找到。

Portfolio  - 携带股票列表并获取使用股票价格和股票数量计算的市场价值的对象。

Stock - 携带股票详细信息的对象,例如其 id、名称、数量等。

StockService - 股票服务返回股票的当前价格。

mock(...) - Mockito 创建了一个模拟股票服务。

when(...).thenReturn(...) - stockService 接口的 getPrice 方法的模拟实现。对于 googleStock,返回 50.00 作为价格。

portfolio.setStocks(...) - 投资组合现在包含两只股票的列表。

portfolio.setStockService(...) - 将 stockService Mock 对象分配给投资组合。

portfolio.getMarketValue() - 投资组合使用模拟股票服务根据其股票返回市场价值。

 

 

 

 

函数的命令和查询分裂

上一节,我们知道了什么是命令,以及什么是查询。

今天我们继续前面的例子,来试着将一个功能太多的函数进行命令和查询的分离。

 

 

 

 

 

 

/**
 * 查询函数(用于设置值并返回)
 *
 */	
function configure(values) {
	var config = { docRoot: '/somewhere' };
 	var key;
 	for (key in values) {
 		config[key] = values[key];
 	}
 	return config;
}

/**
 * 命令函数(用与验证config的docRoot)
 *
 */
function validateDocRoot(config) {
	var fs = require('fs');
	var stat;
 	stat = fs.statSync(config.docRoot);
	if (!stat.isDirectory()) {
		throw new Error('Is not valid');
 	}
}

/**
 * 命令函数(用于验证config的其他值)
 *
 */
function validateSomethingElse(config){ ... }

好了,这样我们就将一个复杂的configure()函数分离称为了三个简单的函数,且每个函数或者是命令函数或者是查询函数,这样会方便对每个函数进行测试。

注意:前面我们说过命令函数属于setter和查询函数属于getter,而新抽象出来的configure()函数即包含了对config对象的设置值,又返回了一个config对象,那么它到底是命令函数查询呢?

我的一个简单判断就是,因为这个函数有返回值,所以把它当做查询来进行测试

测试代码

有了命令和查询的分离以后,我就可以对这些简单的命令和查询函数进行测试了.

require("@fatso83/mini-mocha").install();
const sinon = require("sinon");
const { assert } = require("@sinonjs/referee");

var fs = require('fs');
/**
 * 命令函数(用与验证config的docRoot)
 *
 */
function validateDocRoot(config) {
	var stat;
 	stat = fs.statSync(config.docRoot);
	if (!stat.isDirectory()) {
		throw new Error('Is not valid');
 	}
}
describe('validate value1', ()=>{
	
	it('accept the correct value',function(){
		const config = validateDocRoot({docRoot: '/tmp'});
		assert.isUndefined(config);
	});
	it('accept the incorrect value',function(){
        var statSync= sinon.stub(fs , 'statSync').callsFake(()=>{
    	   return {
        	   isDirectory: function(){
            	   return false;
        	   }
    	   };
	   });
       const fn = function() {
            validateDocRoot({docRoot: '/xxx'});
       }
	   assert.exception(fn);
	});
});
//在这里例子中,我们对validateDocRoot函数进行了测试,
//注意一下,这里提取出validateDocRoot函数以后,要么验证正确,要么抛出错误信息
//与上一节中,抛出了错误还返回一个undefined这种处理方式,更加提供了更好可理解性
//同时这里使用了一个stub来模拟fs模块的功能,不需要nodejs环境就可以直接测试啦。
//以往的情况是必须安装nodejs,现在我们直接在浏览器上就可以直接搞定
function validateSomethingElse(){
}

这里我们的configure()有点小问题就是缺少了验证,因此需要重构一下:

/**
 * 此函数接收一个散列对象,要么返回一个有效的配置对象要么抛出一个错误
 *
 */
function configure(values) {
	var config = { docRoot: '/somewhere' };
 	var key;
 	for (key in values) {
 		config[key] = values[key];
 	}
 	validateDocRoot(config);
	validateSomethingElse(config);
 	return config;
}

顺便提一点,在上一个版本的验证函数中,我们看到如果抛出错误,函数返回的是一个undefined而有了命令和查询分离以后,这个返回undefined的奇怪逻辑也不需要了。

 

以上函数算是初步进行了分离,但是存在的一个问题是,configure()的测试需要依赖验证函数,可以尝试做一些抽象,比如:

var fields = {
 	docRoot: { validator: validateDocRoot, default: '/somewhere'},
	somethingElse: { validator: validateSomethingElse}
}
function configure(values) {
	var config = {};
	for (var key in fields) {
 		if (typeof values[key] !== 'undefined') {
			fields[key].validator(values[key]);
			config[key] = values[key];
		}else {
 			config[key] = fields[key].default;
 		}
 	}
	return config;
}

这个函数有几个优点,验证函数可以方便的测试,对象赋值的时候可以进行验证,且数据都在一个中央位置。

当然这个函数也存在缺陷,至于到底存在什么问题呢?我们下一节进行分析。

EventStorming

更新于 2022.10.28 0分钟阅读 1 评论 5 推荐

    作者:

EventStorming 词汇表和备忘单

EventStorming 是最智能的协作方法,它能打破孤立的边界。EventStorming的能力来自多元化、多学科的人群,他们共同拥有丰富的智慧和知识。它最初是为领域驱动设计聚合建模的研讨会发明的,但它现在具有更广泛的适用范围。从获得整个领域的全局问题空间到深入了解整个软件交付流程并制定长期规划。这些研讨会中的每一个都有相同的基本要求和需求。

在这里,您将找到以一致且全面的词汇表记录的有关 EventStorming 核心概念的术语表组合。请务必尽量避免使用行话,因为它设置了不必要的内部和外部区别。还有一张备忘单,您可以使用它来促进您自己的 EventStorming。

词汇表

核心概念

领域事件(Domain Event)

领域事件是 EventStorming 的主要概念。这是一个与领域专家相关并且与正在探索的领域相关的事件。领域事件是过去时的动词。 EventStorming 的官方颜色是橙色。

热点(HotSpot)

热点用于可视化和捕捉热点冲突。由于(语言方面的)不一致、摩擦、问题、异议、反对、问题或拖延引起的冲突,但不限于此,这些冲突需要深入探讨以备后用。EventStorming 的官方颜色是霓虹粉色,我们在使用它时也会稍微转动一个热点。

时间线(Timeline)

当我们有一个故事要讲述,且我们有一个时间线时,EventStorming 是一个强大的工具。墙上的纸卷从左到右代表时间先后。我们可以在纸卷上从上到下表示并行的流程。

混沌探索(Chaotic Exploration)

可以在 EventStorming 开始时使用混沌探索。每个人都自己编写他们可以想到的领域事件。他们会将这些领域事件按照他们认为发生在纸卷上的顺序排列。

执行时间线(Enforce the Timeline)

在混沌探索之后发生的一个阶段,这意味着我们试图使时间线保持一致并删除重复的贴纸。

 

事件风暴大图景(Big Picture EventStorming)

Big Picture EventStorming 的目标是评估现有业务线的健康状况或探索创新商业模式的可行性。它有助于团队创建公司该领域愿景的共享状态。我们可以将输出用作 Conway 定律一致的输入,围绕团队和软件组织业务流程,这些流程具有新兴的限界上下文。您可以在一个纸卷上与 10-30 多人一起参加这些研讨会。

机遇(Opportunity)

因为热点可能会产生负面关联,所以我们也让人们有机会增加机遇。我们使用绿色是因为它与积极的事物相关联。在我们制定一致的时间表后开始使用机遇。

参与者/代理(Actor/Agent)

参与者或代理是围绕(一组)领域事件所涉及的一组人、一个部门、一个团队或一个特定的人。使用的官方颜色是小黄色便利贴。

系统(System)

系统是可部署的 IT 系统,用作领域中问题的解决方案。当我们完成时间线的一致性后,我们可以开始围绕领域事件映射系统。也可能有重复,它可以是任何东西,从使用 Excel 到某些微服务。官方颜色是宽粉色便利贴。

价值(Value)

在我们使时间线保持一致之后,我们可以像在价值流图中那样添加价值。们这样做是为了明确价值在我们领域中的定位。我们使用红色和绿色的小便签来显示正负值。

关键事件(Pivotal Events)

借助关键事件,我们开始寻找流程中少数几个最重要的事件。对于电子商务网站,它们可能看起来像“已添加到目录的商品”、“已下订单”、“已发货”、“已付款”和“已发货”。这些通常是感兴趣的人数最多的事件。

泳道(Swimlanes)

将整个流程分成水平泳道,分配给给定的参与者或部门,是另一个诱人的选择,因为它提高了可读性。对于具有流程建模背景的人来说,这似乎是最明显的选择。

新兴的限界上下文(Emerging Bounded Contexts)

从事件风暴大图景中,我们可以描绘出新兴的限界上下文。它们是从哪里开始深入研究围绕业务问题设计限界上下文的第一个指标。

流程建模EventStorming(Process modelling EventStorming)

流程建模 EventStorming 的目标是评估公司当前流程的健康状况。它帮助团队创建对当前流程现状的共享状态,发现并找出系统瓶颈以及解耦现有软件。

策略(Policy)

从本质上讲,策略是一种反应,即“只要 X 发生,我们就做 Y”。最终以领域事件和命令/动作之间的流程结束。我们用一个大的丁香花色便利贴来做这些。策略可以是自动过程或手动。策略也可以命名为反应器、最终的业务约束或规则或测谎仪,因为策略总是比您最初想象的要多。

命令或行为(Command/Action)

代表决定、行动或意图。它们可以由参与者或自动化过程启动。在 EventStorming 过程中,Action 这个词通常比命令更适合利益相关者。我们正式使用蓝色的便利贴。

查询模型/信息(Query Model/Information)

为了做出决策,参与者可能需要信息,我们在查询模型中捕获这些信息。对于流程事件风暴信息,利益相关者可能会更认可。我们正式使用绿色的便利贴来表示查询模型。

执行颜色编码(Enforce colour coding)

执行颜色编码是按规则播放 EventStorming。通常在执行时间线之后或期间使用,它会创建不同的动态。您可以在下面看到颜色编码以及如何在时间线流程中使用它们。

软件设计事件风暴(Software Design EventStorming)

设计级别 EventStorming 的结果是设计干净且可维护的事件驱动软件,以支持快速发展的业务。我们与业务利益相关者一起设计了一种共享语言,并在一个共享模型中表示它,从而为在有限的上下文中解决问题带来价值。

聚合/一致的业务规则(Aggregate/Consistent Business Rule)

聚合是一种域驱动设计模式,它表示域对象的集群以使无效状态无法表示。我们用大的黄色便利贴来表示它。因为我们想避免对我们的涉众使用 DDD 行话,所以我们也可以使用一致的业务规则或约束来代替。

 

sinon最佳实践

我们对可测试js代码的学习已经有了一些理解,也分析了一些关于测试替身的问题,比如什么时候使用spies?什么时候使用stubs,什么时候使用mocks。

今天我们用一个测试框架sinonjs来做具体的说明。

简介

测试使用了Ajax、网络、超时、数据库或其他依赖项的代码可能很困难。例如,如果您使用 Ajax 或网络,您需要有一个服务器来响应您的请求。使用数据库,您需要使用测试数据设置测试数据库。

所有这一切都意味着编写和运行测试更加困难,因为您需要做额外的工作来准备和设置一个测试可以成功运行的环境。

幸运的是,我们可以使用 Sinon.js 来避免所有的麻烦。我们可以利用它的特性将上述案例简化为几行代码。

然而,开始使用Sinon可能会很棘手。你会以所谓的spies、stubs和mocks的形式获得很多功能,但很难选择何时使用什么。他们也有一些陷阱,所以你需要知道你在做什么以避免问题。

在本文中,我们将向您展示spies、stubs和mocks之间的区别、何时以及如何使用它们,并为您提供一组最佳实践来帮助您避免常见的陷阱。

样例函数

为了更容易理解我们在说什么,下面是一个简单的函数来说明示例。

function setupNewUser(info, callback) {
  var user = {
    name: info.name,
    nameLowercase: info.name.toLowerCase()
  };

  try {
    Database.save(user, callback);
  }
  catch(err) {
    callback(err);
  }
}

该函数有两个参数——一个带有我们想要保存的数据的对象和一个回调函数。我们将 info 对象中的数据放入 user 变量中,并将其保存到数据库中。就本示例而言,save 的作用无关紧要——它可以发送 Ajax 请求,或者,如果这是 Node.js 代码,也许它会直接与数据库对话,但细节并不重要。想象一下它做了某种数据保存操作。

Spies, Stubs and Mocks

spies、stubs和mocks一起被称为测试替身。类似于特技替身如何在电影中完成危险的工作,我们使用测试替身来代替麻烦制造者并使测试更容易编写。

什么时候需要测试替身?

为了更好地理解何时使用测试替身,我们需要了解我们可以拥有的两种不同类型的函数。我们可以将函数分为两类:

  1. 无副作用的功能(纯函数)
  2. 以及有副作用的功能(非纯函数)

没有副作用的函数很简单:这样的函数的结果只取决于它的参数——在给定相同参数的情况下,函数总是返回相同的值。

具有副作用的函数可以定义为依赖于外部事物的函数,例如某个对象的状态、当前时间、对数据库的调用或其他一些保持某种状态的机制。除了参数之外,这种函数的结果还可能受到多种因素的影响。

如果回顾示例函数,我们会在其中调用两个函数——toLowerCase 和 Database.save。前者没有副作用——toLowerCase 的结果只取决于输入字符串的值。然而,后者有一个副作用——如前所述,它执行某种保存操作,因此 Database.save 的结果也会受到该操作的影响。

如果我们想测试 setupNewUser,我们可能需要在 Database.save 上使用 test-double,因为它有副作用。换句话说,我们可以说当函数有副作用时我们需要测试替身。

除了具有副作用的函数之外,我们有时可能需要测试替身与在我们的测试中引起问题的函数。一个常见的情况是当一个函数执行计算或其他一些非常慢的操作时,这会使我们的测试变慢。然而,我们主要需要测试替身来处理具有副作用的函数。

何时使用Spies

顾名思义,spies用于获取有关函数调用的信息。例如,spies可以告诉我们一个函数被调用了多少次,每次调用有哪些参数,返回了哪些值,抛出了哪些错误等等。

因此,只要测试的目标是验证发生了什么,spies就是一个不错的选择。结合 Sinon 的断言,我们可以使用一个简单的 spy 来检查许多不同的结果。

spies最常见的场景包括

  1. 检查函数被调用的次数
  2. 检查传递给函数的参数

我们可以使用 sinon.assert.callCountsinon.assert.callOncesinon.assert.notCalled 等来检查一个函数被调用了多少次。例如,以下是验证是否调用了 save 函数的方法:

require("@fatso83/mini-mocha").install();
const sinon = require("sinon");
describe('test spy example', function(){
	const Database = {
 		save: function(){
		}
 	}
    function setupNewUser(info, callback) {
      var user = {
        name: info.name,
        nameLowercase: info.name.toLowerCase()
      };

      try {
        Database.save(user, callback);
      }
      catch(err) {
        callback(err);
      }
    }
	//验证spy函数调用了多少次
	it('should call save once', function() {
  		var save = sinon.spy(Database, 'save');

  		setupNewUser({ name: 'test' }, function() { });

  		save.restore();
  		sinon.assert.calledOnce(save);
	});
});

我们可以使用 sinon.assert.calledWith 或通过使用 spy.lastCallspy.getCall() 直接访问调用来检查传递给函数的参数。例如,如果我们想验证上述保存函数接收到正确的参数,我们将使用以下规范:

require("@fatso83/mini-mocha").install();
const sinon = require("sinon");
describe('test spy example', function(){
	const Database = {
 		save: function(){
		}
 	}
    function setupNewUser(info, callback) {
      var user = {
        name: info.name,
        nameLowercase: info.name.toLowerCase()
      };

      try {
        Database.save(user, callback);
      }
      catch(err) {
        callback(err);
      }
    }
    it('should pass object with correct values to save', function() {
  		var save = sinon.spy(Database, 'save');
  		var info = { name: 'test' };
  		var expectedUser = {
   			 name: info.name,
    		 nameLowercase: info.name.toLowerCase()
 		};

 	    setupNewUser(info, function() { });

  		save.restore();
 		sinon.assert.calledWith(save, expectedUser);
	});
});

不过,这些并不是您可以通过spies检查的唯一内容——Sinon 提供了许多其他断言,您可以使用这些断言来检查各种不同的内容。相同的断言也可以用于存根。

如果您spy某个函数,则该函数的行为不会受到影响。如果你想改变一个函数的行为方式,你需要一个stub。

何时使用Stubs

stubs就像spies,只是它们替换了目标函数。它们还可以包含自定义行为,例如返回值或抛出异常。他们甚至可以自动调用作为参数提供的任何回调函数。

stubs有一些常见用途:

  1. 您可以使用它们来替换有问题的代码片段
  2. 您可以使用它们来触发不会触发的代码路径——例如错误处理
  3. 您可以使用它们来帮助更轻松地测试异步代码

Stubs可用于替换有问题的代码,即使编写测试变得困难的代码。这通常是由外部因素引起的——网络连接、数据库或其他一些非 JavaScript 系统。这些问题是它们通常需要手动设置。例如,我们需要在运行测试之前用测试数据填充数据库,这使得运行和编写它们变得更加复杂。

如果我们将有问题的代码片段取而代之,我们可以完全避免这些问题。我们之前的示例使用了 Database.save,如果我们在运行测试之前没有设置数据库,这可能会出现问题。因此,在其上使用stubs而不是spies可能是个好主意。

require("@fatso83/mini-mocha").install();
const sinon = require("sinon");
describe('test spy example', function(){
	const Database = {
 		save: function(){
		}
 	}
    function setupNewUser(info, callback) {
      var user = {
        name: info.name,
        nameLowercase: info.name.toLowerCase()
      };

      try {
        Database.save(user, callback);
      }
      catch(err) {
        callback(err);
      }
    }
  it('should pass object with correct values to save', function() {
  		var save = sinon.stub(Database, 'save');
  		var info = { name: 'test' };
  		var expectedUser = {
    		name: info.name,
    		nameLowercase: info.name.toLowerCase()
  		};

  		setupNewUser(info, function() { });

 		save.restore();
  		sinon.assert.calledWith(save, expectedUser);
	});
});

通过用stubs替换与数据库相关的函数,我们不再需要一个实际的数据库来进行测试。几乎任何涉及难以测试的代码的情况都可以使用类似的方法。

Stubs也可用于触发不同的代码路径。果我们正在测试的代码调用了另一个函数,我们有时需要测试它在异常情况下的行为——最常见的是如果出现错误。我们可以利用存根从代码中触发错误:

require("@fatso83/mini-mocha").install();
const sinon = require("sinon");
describe('test spy example', function(){
	const Database = {
 		save: function(){
		}
 	}
    function setupNewUser(info, callback) {
      var user = {
        name: info.name,
        nameLowercase: info.name.toLowerCase()
      };

      try {
        Database.save(user, callback);
      }
      catch(err) {
        callback(err);
      }
    }  
    it('should pass the error into the callback if save fails', function() {
  		var expectedError = new Error('oops');
  		var save = sinon.stub(Database, 'save');
  		save.throws(expectedError);
  		var callback = sinon.spy();

  		setupNewUser({ name: 'foo' }, callback);

  		save.restore();
  		sinon.assert.calledWith(callback, expectedError);
	});
});

第三,Stubs可用于简化测试异步代码。如果我们stub一个异步函数,我们可以强制它立即调用回调,使测试同步并消除异步测试处理的需要。

require("@fatso83/mini-mocha").install();
const sinon = require("sinon");
describe('test spy example', function(){
	const Database = {
 		save: function(){
		}
 	}
    function setupNewUser(info, callback) {
      var user = {
        name: info.name,
        nameLowercase: info.name.toLowerCase()
      };

      try {
        Database.save(user, callback);
      }
      catch(err) {
        callback(err);
      }
    }
    it('should pass the database result into the callback', function() {
  		var expectedResult = { success: true };
  		var save = sinon.stub(Database, 'save');
  		save.yields(null, expectedResult);
  		var callback = sinon.spy();

  		setupNewUser({ name: 'foo' }, callback);

 		save.restore();
  		sinon.assert.calledWith(callback, null, expectedResult);
	});
});

Stubs是高度可配置的,并且可以做的远不止这些,但大多数都遵循这些基本思想。

何时使用Mocks

使用 mock 时你应该小心——当 mock 可以做任何事情时很容易忽略 spies 和 stub,但是 mock 也很容易让你的测试过于具体,这导致脆弱的测试很容易崩溃。易碎测试是在更改代码时很容易意外中断的测试。

Mocks 应该主要在您使用stubs时使用,但需要在其上验证多个更具体的行为。

例如,下面是我们如何使用模拟来验证更具体的数据库保存场景:

require("@fatso83/mini-mocha").install();
const sinon = require("sinon");
describe('test spy example', function(){
	const Database = {
 		save: function(){
		}
 	}
    function setupNewUser(info, callback) {
      var user = {
        name: info.name,
        nameLowercase: info.name.toLowerCase()
      };

      try {
        Database.save(user, callback);
      }
      catch(err) {
        callback(err);
      }
    }
    it('should pass object with correct values to save only once', function() {
  		var info = { name: 'test' };
 		var expectedUser = {
    		name: info.name,
    		nameLowercase: info.name.toLowerCase()
  		};
  		var database = sinon.mock(Database);
  		database.expects('save').once().withArgs(expectedUser);

  		setupNewUser(info, function() { });

  		database.verify();
  		database.restore();
	});
});

请注意,通过mocks,我们预先定义了我们的期望。通常,期望以断言函数调用的形式出现在最后。有了mock,我们直接在mocked函数上定义,最后只调用verify。

在这个测试中,我们使用 once 和 withArgs 来定义一个 mock,它会检查调用的数量和给定的参数。如果我们使用存根,检查多个条件需要多个断言,这可能是代码异味。

由于为mock声明多个条件很方便,所以很容易过火。我们可以很容易地使模拟的条件比需要的更具体,这会使测试更难理解和容易破解。这也是避免多重断言的原因之一,所以在使用模拟时要记住这一点。

最佳实践和技巧

遵循这些最佳实践以避免spies、stubs和mocks的常见问题。

尽可能使用 sinon.test

当您使用spies、stubs或mocks时,请将您的测试函数包装在 sinon.test 中。这允许您使用 Sinon 的自动清理功能。没有它,如果你的测试在你的测试替身被清理之前失败,它可能会导致级联失败——更多的测试失败是由最初的失败引起的。级联故障很容易掩盖问题的真正根源,因此我们希望尽可能避免它们。

使用 sinon.test 消除了这种级联故障的情况。这是我们之前编写的测试之一:

it('should call save once', function() {
  var save = sinon.spy(Database, 'save');

  setupNewUser({ name: 'test' }, function() { });

  save.restore();
  sinon.assert.calledOnce(save);
});

如果 setupNewUser 在此测试中抛出异常,则意味着spies永远不会被清除,这将对任何后续测试造成严重破坏。

我们可以通过使用 sinon.test 来避免这种情况,如下所示:

it('should call save once', sinon.test(function() {
  var save = this.spy(Database, 'save');

  setupNewUser({ name: 'test' }, function() { });

  sinon.assert.calledOnce(save);
}));

注意三个不同之处:在第一行中,我们用 sinon.test 包装了测试函数。在第二行中,我们使用 this.spy 而不是 sinon.spy。最后,我们删除了 save.restore 调用,因为它现在正在自动清理。

您可以将此机制用于所有三个测试替身:

  • sinon.spy 变成 this.spy 
  • sinon.stub 变成 this.stub 
  • sinon.mock 变成 this.mock

使用 sinon.test 进行异步测试

使用 sinon.test 时,您可能需要禁用异步测试的虚假计时器。当将 Mocha 的异步测试与 sinon.test 一起使用时,这是一个潜在的混淆来源。

要使测试与 Mocha 异步,您可以在测试函数中添加一个额外的参数:

it('should do something async', function(done) {

当与 sinon.test 结合使用时,这可能会中断:

it('should do something async', sinon.test(function(done) {

结合这些可能会导致测试无缘无故地失败,并显示有关测试超时的消息。这是由 Sinon 的假计时器引起的,默认情况下,使用 sinon.test 包装的测试会启用这些计时器,因此您需要禁用它们。

这可以通过更改测试代码中的某处或随测试加载的配置文件中的 sinon.config 来解决:

sinon.config = {
  useFakeTimers: false
};

sinon.config 控制一些函数的默认行为,如 sinon.test。它还有一些其他可用的选项。

在 beforeEach 中创建共享Stubs

如果您需要在所有测试中用stubs替换某个函数,请考虑在 beforeEach 挂钩中将其stubs。例如,我们所有的测试都使用了 Database.save 的测试替身,因此我们可以执行以下操作:

describe('Something', function() {
  var save;
  beforeEach(function() {
    save = sinon.stub(Database, 'save');
  });

  afterEach(function() {
    save.restore();
  });

  it('should do something', function() {
    //you can use the stub in tests by accessing the variable
    save.yields('something');
  });
});

确保还添加一个 afterEach 并清理stubs。没有它,stubs可能会留在原处,并且可能会导致其他测试出现问题。

检查正在设置的函数调用或值的顺序

如果您需要检查某些函数是否按顺序调用,您可以将间谍或存根与 sinon.assert.callOrder 一起使用:

var a = sinon.spy();
var b = sinon.spy();

a();
b();

sinon.assert.callOrder(a, b);

如果需要在调用函数之前检查是否设置了某个值,可以使用 stub 的第三个参数将断言插入到 stub 中:

var object = { };
var expectedValue = 'something';
var func = sinon.stub(example, 'func', function() {
  assert.equal(object.value, expectedValue);
});

doSomethingWithObject(object);

sinon.assert.calledOnce(func);

stubs中的断言确保在调用存根函数之前正确设置值。请记住还包括一个 sinon.assert.calledOnce 检查以确保调用stubs。没有它,当不调用stubs时,您的测试将不会失败。

总结

Sinon 是一个功能强大的工具,通过遵循本文中列出的做法,您可以避免开发人员在使用它时遇到的最常见问题。要记住的最重要的事情是使用 sinon.test - 否则,级联故障可能是挫败感的一大来源。

js设计模式(抽象工厂和建造器)

更新于 2022.10.20 8分钟阅读 3 评论 5 推荐

    作者:

什么是设计模式

首先,需要了解设计模式的真正定义。作为软件开发人员,您可以“以任何方式”编写代码。但是,设计模式将是最佳实践,这将对您维护代码的方式产生重大影响。以最大的技巧编写的代码将比业余代码持续更长时间。这意味着当您选择正确的编码风格时,您无需担心可扩展性或维护。

  • 设计模式的作用是帮助您构建不会使整体问题复杂化的解决方案。
  • 模式将帮助您构建交互式对象和高度可重用的设计。
  • 设计模式是面向对象编程中不可或缺的概念。

 

创建型设计模式

  1. 抽象工厂
  2. 构建器
  3. 工厂
  4. 原型
  5. 单例

抽象工厂

什么是工厂?如果你问一个孩子,他会如何形容工厂?工厂不过是我们制造东西的地方。例如,在玩具厂,你会看到玩具的生产。就像这个定义一样,JavaScript 中的工厂是一个创建其他对象的地方。不是所有的玩具工厂都生产泰迪熊和变形金刚,对吧?独立的玩具工厂,生产不同类型的玩具。玩具厂总是有一个主题。类似地,抽象工厂与主题一起工作。来自抽象工厂的对象将共享一个共同的主题。

让我们通过一个例子来理解 JavaScript 中的抽象工厂。

我要为玩具工厂构建软件。

我有一个部门,生产以变形金刚电影为主题的玩具。而星球大战主题玩具则是另一个部门进行生产。

这两个部门都有一些共同的特点和独特的主题。

function StarWarsFactory(){
	this.create = function(name){
	return new StarWarsToy(name)
}}

function TransformersFactory(){
	this.create = function(name){
	return new TransformersToy(name)
}}

function StarWarsToy(name){
	this.nameOfToy = name;
	this.displayName = function(){
		console.log("My Name is "+this.nameOfToy);
	}
}

function TransformersToy(name){
	this.nameOfToy = name;
	this.displayName = function(){
		console.log("My Name is "+this.nameOfToy);
	}
}

function buildToys(){
	var toys = [];


	var factory_star_wars = new StarWarsFactory();
	var factory_transformers = new TransformersFactory();


	toys.push(factory_star_wars.create("Darth Vader"));
	toys.push(factory_transformers.create("Megatron"));

	for(let toy of toys)
	console.log(toy.displayName());
}
buildToys();

 

建造器

建造器的作用是建造复杂的对象。客户端将收到一个最终对象,而不必担心实际完成的工作。大多数时候,构建器模式是对复合对象的封装。主要是因为整个过程复杂且重复。

让我们看看如何使用构建器模式来实现我们的玩具工厂。

  1. 我有一家玩具厂
  2. 我有一个部门负责建造星球大战玩具。
  3. 在星球大战部门,我想制造许多达斯维达玩具。
  4. 制作达斯维达玩具分为两步。我需要制作玩具,然后提及颜色。
function StarWarsFactory(){
  this.build = function(builder){
  	builder.step1();
  	builder.step2();
  	return builder.getToy();
  }
}

function DarthVader_Builder(){
  this.darth = null;
  this.step1 = function () {
    this.darth = new DarthVader(); 
  }

  this.step2 = function () {
    this.darth.addColor();
  }

  this.getToy = function () {
    return this.darth;
  }
}

function DarthVader () {
  this.color = '';
  this.addColor = function(){
    this.color = 'black';
  }
  this.say = function () {
    console.log("I am Darth, and my color is "+this.color);
  }
}

function build(){
  let star_wars = new StarWarsFactory();
  let darthVader = new DarthVader_Builder();
  let darthVader_Toy = star_wars.build(darthVader);
  darthVader_Toy.say();
}
build();

 

js设计模式(工厂,原型,单例)

更新于 2022.10.10 7分钟阅读 0 评论 5 推荐

    作者:

我们知道,在设计模式中,创建型模式有五种,今天我们继续讨论剩下的模式。

 

工厂模式

工厂的作用是生产具有相同特性的相似物体。这有助于轻松管理、维护和操作对象。例如,在我们的玩具工厂,每个玩具都会有一定的信息。它将包含购买日期、原产地和类别。这里我首先定义了一个玩具叫做StarWars,然后定义了一个玩具工厂,然后根据工厂函数创建一个具体的玩具。

 

var StarWars = function(){
};
var ToyFactory = function(){
  this.createToy = function(type) 
  {
    var toy;
    if(type == "starwars") {
        toy = new StarWars();
    }
    
    toy.origin = "Origin";
    toy.dop = "2/22/2022";
    toy.category="fantasy";
    return toy;
  }
};
const toyFactory = new ToyFactory();
const starwarsToy = toyFactory.createToy('starwars');
console.log('starwarsToy:',starwarsToy.origin);

原型模式

很多时候,我们需要创建一个新对象,它具有来自另一个父对象的默认值。这可以防止创建具有未初始化值的对象。原型模式可用于创建此类对象。

原型模式也称为属性模式。

  1. 在我们的玩具厂里,我们有星球大战部门,生产许多角色。
  2. 每个角色都有一个流派、到期日期和状态字段。来自《星球大战》部门的所有玩具的这些字段将保持不变。
function Star_Wars_Prototype(parent){
   this.parent = parent;
   this.duplicate = function () 
   {
      let starWars = new StarWarsToy();
      starWars.genre = parent.genre;
      starWars.expiry = parent.expiry;
      starWars.status = parent.status;
      return starWars;
   };
}

function StarWarsToy(genre, expiry, status){
   this.genre = genre;
   this.expiry = expiry;
   this.status = status;
}

function build () {
   var star_wars_toy = new StarWarsToy('fantasy', 'NA', 'Jan');
   var new_star_wars_toy = new Star_Wars_Prototype(star_wars_toy);
   
   //When you are ready to create
   var darth = new_star_wars_toy.duplicate();
}

单例模式

单例模式对应一个“单例”。一个给定的对象只能有一个实例。当系统有数据需要从一个点进行协调时,您可以使用此模式。

var Singleton = (function () {
   var instance;

   function createInstance() {
       var object = new Object("I am the instance");
       return object;
   }

   return {
       getInstance: function () {
           if (!instance) {
               instance = createInstance();
           }
           return instance;
       }
   };
})();

function run() {

   var instance1 = Singleton.getInstance();
   var instance2 = Singleton.getInstance();

   console.log("Same instance? " + (instance1 === instance2));
}

 

耦合

耦合

上一节我们重点关注了扇出的内容,以及用例子说明了如何才能将一个函数或者模块的代码通过重构来达到一个合理的扇出值。今天我们来理解耦合:

耦合是关注模块如何组合在一起的,增加子模块或许可以减少扇出的值,但是不能减少原始模块对最初依赖之间的耦合度;本质是将显示依赖变成了间接依赖。

虽然今天我们不讲解内聚,但是我们也说说什么是内聚,以及什么是耦合?

 

内聚是从功能角度来度量模块内的联系,一个好的内聚模块应当恰好做一件事。它描述的是模块内的功能联系。

耦合是软件结构中各模块之间相互连接的一种度量,耦合强弱取决于模块间接口的复杂程度、进入或访问一个模块的点以及通过接口的数据。

耦合一般有六个级别,它们分别是:

1、内容耦合

内容耦合是最紧的耦合,包括在外部对象上调用方法或者函数,或通过修改外部对象的属性直接修改对象的状态。以下是例子:

let O = function() {
}
O.property = 'blah'; //直接改变外部对象的属性
O.methodName = function(){} //改变外部对象的方法
O.prototype.methodName = function(){} //直接改变了对象的原型

以上与外部对象O之间都是一种内容耦合。

 

2、公共耦合

比内容耦合略低一点的就是公共耦合。如果两个对象都共享另外一个全局变量,那么这两个对象就有公共耦合了。

var Global = 'global';
Function A() { GLobal = 'A'; };
Function B() { GLobal = 'B'; };

在这里,对象A和对象B就是公共耦合。

 

3、控制耦合

控制耦合比公共耦合的耦合度低一些。该耦合基于标记或者参数设置来控制外部对象。举个例子,创建一个单例抽象工厂,接着传入一个env标记告知该抽象工厂如何操作。这就是一种控制耦合。

var absFactory = new AbstractFactory({ env: 'Test'});

 

4、印记耦合

印记耦合是通过向外部对象传递一个记录,而只用该记录的一部分。举个例子

O.makeBread({ type: wheat, size:99, name: 'foo'});
O.prototype.makeBread = function(args) {
	return new Bread(args.type. args.size);
}

以上代码中,向makeBread函数传递一个记录,而函数只使用了该记录三个属性中的两个,这就属于印记耦合。

 

5、数据耦合

耦合类型最松散的就是数据耦合。这种耦合发生在一个对象传递给另一个对象消息数据,而没有传递控制外部对象的消息参数。简单理解就是方法调用的时候传递数据,这些数据不能让被调用的方法具有分支逻辑啥的。

 

6、无耦合

最后一种耦合形式就是无耦合,两个对象之间的绝对零耦合。

 

我们用一个例子来说明减少耦合对于代码测试的好处:

function setTable() {
	var cloth = new TableCloth();
	var dishes = new Dishes();
	this.placeTableCloth(cloth);
	this.placeDishes(dishes);
}

我们看这段代码,setTable函数耦合了两个对象TableClothDishes,而我在测试这个方法的时候必须为这两个对象进行模拟,因此代码测试将变得困难。修改成一下依赖注入以后

function setTable(cloth, dishes) {
	this.placeTableCloth(cloth);
	this.placeDishes(dishes);
}

这样修改以后,我通过向函数注入对象,起到了将方法和对象隔离的作用,测试起来也会更加简单。

 

另一种减少耦合的方法就是通过工厂和抽象工厂设计模式来重构代码,感兴趣的可以去网上看看相关的内容。

如果大家对耦合如何用代码来表现感觉有疑惑的话,可以看看这篇博文