节点抽象存储库
#网络开发人员 #node #tooling #mongodb

node-abstract-repository是一个轻巧且类型安全的库,可无缝为node.js应用程序创建自定义数据库存储库。它为开发人员提供了一些内置的CRUD操作,从而使他们能够集中精力添加自己的操作,并确保持久性和域逻辑之间的干净分离。尽管本质上是数据库技术不可知论,但目前包括对MongoDB的实施。

笔记

本文介绍了node-abstract-repository,但并未包含其所有细节。您可能会在this GitHub repository上找到完整的文档和一些用法示例,其中包括how to use this library in a NestJS-based application的示例。

为什么构建另一个存储库库

加入ClimatePartner使我换了齿轮,并开始开发基于JS/TS的Web应用程序。我的第一个任务是将一个小的后端Nestjs应用与MongoDB数据库连接起来。我们为应用程序制定了宏伟的计划,我们很快意识到,我的团队拥有的域模型将在复杂而迅速地增长。此外,我们看到这也是其他想要使用相同解决方案的团队中的现实。

全部考虑了,我们决定合并一个数据库访问实用程序,该数据库访问实用程序实现Repository pattern,以使持久性与域逻辑的持久性清除,同时将查询逻辑重复保留到裸露的最低限度。我们还希望它具有摘要的性质,以便我们可以重复使用所有自定义存储库(即,在任何持久域对象上的CRUD操作)共有的数据库访问逻辑。最后,我们需要它来实施Polymorphic pattern以支持基于亚型的域模型。

因此,我们开始研究最先进的艺术品,并发现了一些可以满足我们要求的图书馆。我们能找到的最好的候选人是MongooseTypegooseTypeORM。 Mongoose是一个众所周知的节点。但是,Mongoose与具体数据模型一起使用,该模型在复杂的域模型方案中会导致查询逻辑重复。 TypeGoose是一种类型的安全杂种包装器,可通过JS装饰器在域对象字段级别进行架构约束声明。不幸的是,那些非常装饰的人将持久性逻辑泄漏到域模型中。此外,TypeGoose还实现了数据映射器模式,从而共享了猫鼬的相同缺点。另一方面,Typeorm实现了存储库模式,并为MongoDB提供了一些基本支持。但是,typeormpresents several limitations compared to Mongoose

尽管现有的替代方案都不完全适合我们的需求,但我们看到了Mongoose坚实,有据可查且完整的一组功能的价值。最重要的是,我们希望可以使用最轻巧的解决方案。这就是为什么我们决定构建node-abstract-repository

切入正题

说我们有一个域模型,该模型将Book指定为Supertype域对象,而PaperBookAudioBook作为子类型。这是该域模型的一个可能定义:

export class Book implements Entity {
  readonly id?: string;
  readonly title: string;
  readonly description: string;
  readonly isbn: string;

  constructor(book: {
    id?: string;
    title: string;
    description: string;
    isbn: string;
  }) {
    this.id = book.id;
    this.title = book.title;
    this.description = book.description;
    this.isbn = book.isbn;
  }
}

export class PaperBook extends Book {
  readonly edition: number;

  constructor(paperBook: {
    id?: string;
    title: string;
    description: string;
    isbn: string;
    edition: number;
  }) {
    super(paperBook);
    this.edition = paperBook.edition;
  }
}

export class AudioBook extends Book {
  readonly hostingPlatforms: string[];
  readonly format?: string;

  constructor(audioBook: {
    id?: string;
    title: string;
    description: string;
    isbn: string;
    hostingPlatforms: string[];
  }) {
    super(audioBook);
    this.hostingPlatforms = audioBook.hostingPlatforms;
  }
}

现在,您希望能够从mongoDB数据库中持续并检索Book及其任何子类型的实例。 MongooseBookRepository是一个自定义存储库,指定与Book相关的数据库操作。这是其定义:

export class MongooseBookRepository
  extends MongooseRepository<Book>
  implements BookRepository
{
  constructor() {
    super({
      Default: { type: Book, schema: BookSchema },
      PaperBook: { type: PaperBook, schema: PaperBookSchema },
      AudioBook: { type: AudioBook, schema: AudioBookSchema },
    });
  }

  async findByIsbn<T extends Book>(isbn: string): Promise<Optional<T>> {
    if (!isbn)
      throw new IllegalArgumentException('The given ISBN must be valid');
    return this.entityModel
      .findOne({ isbn: isbn })
      .exec()
      .then((book) => Optional.ofNullable(this.instantiateFrom(book) as T));
  }
}

voilã! MongooseBookRepository继承了MongooseRepository的一系列CRUD数据库操作,并添加了自己的findByIsbn。现在,您可以简单地实例化MongooseBookRepository并执行任何数据库操作,如下所示:

const bookRepository = new MongooseBookRepository();
const books: Book[] = bookRepository.findAll();

不再将持久性逻辑泄漏到您的域/应用程序逻辑中!

一些重要的实施细节

您可能想知道上一个示例中暴露的某些元素。从MongooseBookRepository定义的顶部开始,其中的第一个是MongooseRepository<Book>。这是在node-abstract-repository:一个通用模板上暴露的两个主要元素之一,它实现了几个常见的CRUD数据库操作。此外,此类还处理了能够处理基于亚型的书籍数据对象的猫鼬数据模型的创建。所有这些复杂性都隐藏在您身上;您需要知道的是,您可以使用entityModel来实施自己的数据库操作,如findByIsbn所示。

您所需要的唯一额外的努力是,您定义了一张地图,该地图指定了构成域模型及其相关的杂种模式的类型,如MongooseBookRepository的构造函数所示。同样重要的是要注意,指定您的域对象supertype的条目的键必须命名为Default,并且彼此的键必须以其所指的子类型域对象的类型名称命名。这是MongooseRepository的内部实现详细信息要求的命名约定。如果您的域模型中没有任何域对象子类型,则只需使用唯一的条目实例化映射,该映射指定了Default键和域对象的类型 - 设施值。

可能引起您眼睛的另一个元素是BookRepository。这是一个可选的接口,该接口扩展了Repository,这是node-abstract-repository:数据库技术不可或缺的通用界面上公开的第二个主要元素,它指定了所有存储库将实现的所有支持的CRUD数据库操作。在面向对象的应用程序中,处理接口类型而不是类的实例是一个很好的做法。 depending on abstractions instead of implementations使您的代码可扩展,更适合更改。因此,我强烈建议您定义自己的存储库接口,或者在情况下,您只想对暴露CRUD操作,将自定义存储库实例化为类型Repository<T>的对象,其中T是您的域对象Supertype。

另一方面,Book实现了一个称为Entity的接口。此接口对任何持久域对象类型进行建模。该界面定义一个可选的id字段。该领域的可选性质是由于其价值是由Mongoose内部设置的。但是,如果您不希望域模型包括此依赖关系,则可以确保域对象指定id?: string字段。这样做没有错,因为id是MongoDB文档中的规范主要键。

最后,许多数据库操作返回koude32类型的对象。这种类型的灵感来自Java koude32 type。其目的是建模“无结果”的概念。这种类型是返回null对象或抛出异常的绝佳替代方法。 Optional的使用比必须将T | null指定为结果类型或having to handle an exception that does not represent an application exceptional condition更安全,更清晰。是的,这是使node-abstract-repository有点自以为是的功能,但我相信这是值得的。

结论

node-abstract-repository库提供了以快速简便的方式为基于node.js的应用程序创建自定义存储库的方法。它可以想象在持久性和域逻辑之间实现有效的分离。它的当前实现是一种杂种包装器,其中包括对MongoDB中多态性域对象进行常见的CRUD操作所需的所有样板代码。因此,开发人员可以专注于自定义存储库的特定于域的数据库操作。

此外,该库旨在允许开发人员使用许多有趣的猫鼬功能(例如,创建和验证多态性模型以及挂钩实现,只有两个提及两个),而无需处理大多数Mongoose内部复杂性。<<<<<<<<<<<<<<<<<<< /p>

Alev Takil上的Unsplash

的封面图像