Utilizando o ORM Prisma em uma arquitetura desacoplada.

Apresentação:

Fala pessoal, tudo certo?! Meu nome é João Pedro, e estou aqui para falar sobre arquitetura de software com vocês.

O questionamento que quero trazer é: Como podemos aplicar o ORM prisma em um contexto de Clean Architecture (Arquitetura Limpa/Desacoplada)?

Contexto é sempre importante:

Nesse exemplo, estarei usando a linguagem de programação TypeScript e o ORM prisma, e o framework Jest para os testes automatizados. Ou seja, o contexto das tecnologias gira em torno do ecossistema JavaScript. Mas vou fazer o máximo para passar o conceito de maneira que você possa aplicar em qualquer linguagem.

Motivação:

Eu sempre vejo alguns tutoriais e conteúdos que falam sobre a Clean Architecture, mas raramente vejo algum que aborte o assunto de maneira prática. É isso que vamos conversar agora.

Por que o prisma?

O prisma é um excelente ORM. Ele é muito simples de configurar e sua API é fantástica. Suas funcionalidades são muito boas, e ele é muito rápido. Além disso, ele é multi-banco de dados, ou seja, você pode usar o mesmo código para diferentes bancos de dados, como MySQL, PostgreSQL, SQLite, MongoDB, etc.

Se você ainda não conhece o prisma, aqui estão dois vídeos sensacionais para você dar uma olhada:

Desenvolvimento:

Prisma aplicado no padrão repository:

O padrão repository é um padrão de projeto que visa criar uma ligação entre as entidades do domínio com o banco de dados.

Imagine que temos a seguinte entidade User:

export class User {
  public readonly id: string;
  public readonly name: string;
  public readonly email: string;

  constructor(id: string, name: string, email: string) {
    this.id = id;
    this.name = name;
    this.email = email;
  }

  // Implementações de lógicas de negócio...
}

Para termos essa abstração de banco de dados, criamos uma interface UserRepository:

export interface UserRepository {
  save(user: User): Promise<void>;
  findByEmail(email: string): Promise<User | undefined>;
}

Observe que em todos os métodos da interface UserRepository temos uma entidade User como parâmetro e retorno. É nesse ponto que criamos uma relação entre o nosso domínio e a base de dados: através de uma abstração. Aqui utilizo o conceito base de dados pois não necessariamente será um banco de dados, pode ser um cache, fila, arquivo, etc. Qualquer coisa em que possamos fazer esse transporte de dados, que irá construir nossa entidade. Mas no nosso exemplo, iremos focar na utilização do prisma.

  • A declaração de um Modelo de User no prisma seria algo do tipo:
model User {
  id    String   @id @default(cuid())
  name  String
  email String   @unique
}

E é nesse ponto que aplicamos as regras da Clean Architecture. Vamos para a implementação do UserRepository:

export class PrismaUserRepository implements UserRepository {
  constructor(private readonly prisma: PrismaClient) {}

  public async save(user: User): Promise<void> {
    await this.prisma.user.create({
      data: {
        id: user.id,
        name: user.name,
        email: user.email,
      },
    });
  }

  public async findByEmail(email: string): Promise<User | undefined> {
    const user = await this.prisma.user.findUnique({
      where: {
        email,
      },
    });

    if (!user) return undefined;

    return new User(user.id, user.name, user.email);
  }
}

O fato dessa classe implementar a abstração (UserRepository) de base de dados faz com que essa ela possa ser injetada em qualquer outra classe da aplicação, sem que essa outra classe dependa dessa implementação. É possível injetar essa classe em um service, use case etc.

Alguns pontos importantes:

  • Por que receber uma instância do prisma pelo construtor da classe? Não é mais fácil criar uma instância dentro da própria classe?

Nesse momento trago o famoso framework da Clean Architecture:

The Clean Architecture Framework

A resposta para essa pergunta tem dois pontos:

  1. A implementação de um repositório e a instância do prisma são responsabilidades de camadas diferentes:

A implementação do repositório está na camada de Interface Adapters (camada verde na imagem acima), essa camada é responsável por converter a interface do prisma para a interface do nosso domínio. Isso fica claro no método findByEmail, no qual através de um e-mail, utilizamos a interface do prisma e retornamos uma interface de User.

Já a instanciação do prisma fica na camada de Frameworks and Drivers (camada azul na imagem acima). É nessa camada "mais externa" na qual saberá instanciar esse framework, e injetar essa instância no repositório.

  1. Cada instância do prisma é uma conexão com o banco de dados:

O prisma tem essa particularidade: cada vez que uma nova instância da classe Prisma Client é feita, uma lazy connection com o banco de dados é criada. A própria documentação do prisma aconselha que você tenha apenas uma instância do prisma por aplicação. Em ambiente de desenvolvimento e produção, isso pode der resolvido com um singleton, mas isso é assunto para outro post.

Como podemos testar isso?

E aqui chegamos em outro ponto bastante importante: testes. Como podemos ter certeza que essa implementação está realmente funcionando? Criamos toda uma API REST para levantar um servidor e fazer uma requisição? Rezamos aos deuses da programação para que tudo funcione? Não, vamos utilizar testes automatizados.

E aqui podemos seguir por dois caminhos:

  1. "Mockar" a instância do prisma:

A própria documentação do prisma nos dá uma série de passos para fazer isso. Pessoalmente, eu não escolho essa abordagem para meus projetos, acho que "mockar" essa instância não traz tantos benefícios, pois não nos traz uma reposta exata do banco de dados. Mas se você quiser seguir por esse caminho, a documentação do prisma é bem completa.

  1. Utilizar um banco de dados de teste:

A documentação do prisma também tem uma série de passos para fazer isso. Eu prefiro essa abordagem, pois nos traz uma resposta mais próxima do banco de dados real. Essa abordagem consiste em criar um docker com um banco de dados de teste, no qual vamos rodar nossos testes.

Não vou me atentar em configurar o docker, pois isso é feito explicado na documentação, vamos seguir direto para o código:

Primeiramente, vamos criar um arquivo que exporta o prisma por padrão:

  • PrismaClient.ts
import { PrismaClient } from "@prisma/client";

const prisma = new PrismaClient();

export default prisma;

Desse modo, importamos uma instância "cacheada" do prisma, e não precisamos nos preocupar em criar uma nova instância toda vez que precisarmos utilizar-lo.

Agora, vamos criar um arquivo de testes para o nosso repositório:

  • PrismaUserRepository.spec.ts
import prisma from "./prismaClient";
import PrismaUserRepository from "./PrismaUserRepository";

describe("PrismaUserRepository", () => {
  const userRepository = new PrismaUserRepository(prisma);

  beforeAll(async () => {
    await prisma.user.deleteMany();
  });

  afterAll(async () => {
    await prisma.$disconnect();
  });

  it("should be able to create a new user", async () => {
    await userRepository.save({
      name: "John Doe",
      email: "john@doe.com",
    });

    const user = await userRepository.findByEmail("john@doe.com")

    expect(user).toHaveProperty("id");
    expect(user.name).toBe("John Doe");
  }):
});

Nesse teste, estamos utilizando os dois métodos do repositório: save e findByEmail. O teste é bem simples: criamos um usuário, e depois buscamos esse usuário pelo e-mail. Se o usuário for criado, e o nome for o mesmo, o teste passa.

Por mais que esse seja um teste simples, já mostra o poder da nossa arquitetura: Podemos testar essa classe sem depender de nenhuma outra coisa, somente dela mesma. Além disso, eu escrevi um teste em que fazemos uma conexão com um banco real, mas nada nos impede de testarmos essa classe com uma conexão "mockada" (como no tópico 1), pois essa conexão é injetada no repositório (através do construtor), o que nos dá total flexibilidade para testar do jeito que quisermos.

Conclusão

Como foi dito antes, o prisma é sensacional, eu recomendo bastante que se você estuda ou trabalha utilizando tecnologias relacionadas com o ecossistema javascript, dê uma olhada nele. A documentação é bem completa, e o suporte é sensacional.

  • Referências:

Além dos links que eu já deixei no texto, eu recomendo que você dê uma olhada nesse vídeo do professor Otávio Lemos:

Se você quiser conhecer mais sobre arquitetura de software, eu super recomendo você seguir o canal do professor Otávio Lemos, o do Rodrigo Branas e do Full Cycle, esses três canais são incríveis, e eu aprendo muito com eles.

Espero que esse artigo tenha sigo útil para você, e que tenha aprendido algo novo. Se você acha que algo ficou faltando, ou mal explicado, ou se você tem alguma dúvida, não deixe de comentar.

Para me alcançar, você pode me encontrar no linkedIn.

Valeu!

Esse foi um post extremamente esclarecedor para mim. Utilizo Clean Arch em todos meus projetos desde 2021. Uma dúvida que sempre me vem nos momentos de desenvolvimento: qual é a camada ideal para colocar o "prisma scheme"?

Atualmente, estou colocando na camada de external interfaces, junto com os repositórios. Espero uma resposta!

Fala Nathan! Acho que a sua pergunta levanta uma questão bem interessante: A diferença entre camadas e pastas quando estamos falando de **Clean Architecture**. - Quando falamos de **camadas**, estamos estabelecendo uma separação lógica no nosso código, onde cada camada tem sua responsabilidade única e bem definida. Uma camada mais interna **não pode** conhecer uma mais externa, porém uma camada mais externa pode conhecer uma mais interna. É isso que chamados de **Dependency Rule** (Regra de dependência). Isso é puramente lógico, não tem nada a ver com organização de pastas ou arquivos. - Já as pastas e arquivos são separações **físicas** do nosso código. Nosso projeto pode conter 1, 2... N pastas, mas isso não significa que estamos seguindo a Clean Architecture. **Podem existir camadas sem pastas e pastas sem camadas.** Uma coisa não tem nada a ver com a outra. Sendo específico na Clean Architecture, o arquivo `schema.prisma` estará na **camada** mais externa de todas, a **camada de infraestrutura**. Se você seguir a Clean Arch, isso deve ser previamente estabelecido. Agora, em nível de **pastas**, acredito que você pode colocar o arquivo `schema.prisma` juntamente com com os arquivos de repositório faz bastante sentido. Uma vez que nossas implementações de repositório são direcionadas ao prisma, faz sentido que eles estejam juntos (a nível de pastas, claro). Espero ter ajudado! Qualquer dúvida, só falar! Abraço!

Eu uso esse padrao em praticamente todo projeto que eu faço, porem nao uso classe, crio funcoes e depois exporto objeto com o tipo correto que defeni no interface do repositories. Acho mais facil que abordagem orientada a Objeto. Tambem faco isso com providers, no seu caso eh Prisma, eu normalmente crio uma past providers para prover DBs, Email, Server , etc. Ajuda muito na hora de refactoring.

Poste mais sobre prisma, ensine desde o mais simples, seria legal acompanhar, eu to entrando em contato agora, confesso que ainda estou bem inseguro, mas com o tempo acho que vou ficar bom !! Parabéns !!

Fala joao, você já implementou algo com TRPC e Prisma? Estou buscando exemplos de como arquiteturas com essas duas ferramentas.

Opa JanderSilv, beleza? Cara, nunca tive a oportunidade de aplicar tRPC em um projeto muito grande não. Mas a lógica deveria ser a mesma de aplicar o Prisma: isolar a utilização dessa biblioteca em uma camada mais externa da aplicação e reutilizar isso por meio de uma abstração (interface). Espero ter ajudado, valeu!