Como usar Prisma com múltiplos bancos de dados no mesmo projeto

1. Cenários e motivações para múltiplos bancos com Prisma

1.1. Separação de domínios: banco de leitura vs. banco de escrita (CQRS)

Em aplicações de alto desempenho, separar operações de leitura e escrita em bancos distintos é uma estratégia comum. O Prisma permite configurar um banco exclusivo para consultas (read replica) e outro para comandos (write master). Isso reduz contenção de recursos e melhora a escalabilidade.

1.2. Microserviços monolíticos: bancos isolados por módulo de negócio

Mesmo em um monólito, você pode querer isolar dados de módulos diferentes (ex: financeiro, estoque, usuários) em bancos separados. Cada módulo tem seu próprio schema Prisma, garantindo independência e facilitando futuras extrações para microserviços.

1.3. Migração incremental: convivência de banco legado e novo banco

Ao modernizar um sistema, é comum manter o banco legado funcionando enquanto migra dados para um novo banco. Com múltiplos schemas Prisma, ambos coexistem no mesmo projeto, permitindo migração gradual sem interromper operações.

2. Estrutura de projeto e configuração de múltiplos schemas

2.1. Organização de diretórios

A melhor prática é criar pastas separadas para cada banco:

meu-projeto/
├── prisma/
│   ├── db1/
│   │   └── schema.prisma
│   └── db2/
│       └── schema.prisma
├── src/
│   ├── db1-client.ts
│   ├── db2-client.ts
│   └── index.ts
├── package.json
└── .env

2.2. Arquivo schema.prisma dedicado para cada banco

prisma/db1/schema.prisma:

generator client {
  provider = "prisma-client-js"
  output   = "../../node_modules/.prisma/client/db1"
}

datasource db {
  provider = "postgresql"
  url      = env("DATABASE_URL_DB1")
}

model User {
  id    Int     @id @default(autoincrement())
  name  String
  email String  @unique
  posts Post[]
}

model Post {
  id        Int     @id @default(autoincrement())
  title     String
  content   String?
  authorId  Int
  author    User    @relation(fields: [authorId], references: [id])
}

prisma/db2/schema.prisma:

generator client {
  provider = "prisma-client-js"
  output   = "../../node_modules/.prisma/client/db2"
}

datasource db {
  provider = "postgresql"
  url      = env("DATABASE_URL_DB2")
}

model Product {
  id    Int     @id @default(autoincrement())
  name  String
  price Float
  stock Int
}

model Order {
  id        Int       @id @default(autoincrement())
  productId Int
  quantity  Int
  total     Float
  product   Product   @relation(fields: [productId], references: [id])
}

2.3. Configuração de variáveis de ambiente

No arquivo .env:

DATABASE_URL_DB1="postgresql://user:pass@localhost:5432/db1"
DATABASE_URL_DB2="postgresql://user:pass@localhost:5432/db2"

3. Geração de clientes Prisma independentes

3.1. Comando com --schema

Para gerar o cliente para cada banco:

npx prisma generate --schema=prisma/db1/schema.prisma
npx prisma generate --schema=prisma/db2/schema.prisma

3.2. Scripts no package.json

Adicione scripts para facilitar:

"scripts": {
  "generate:db1": "prisma generate --schema=prisma/db1/schema.prisma",
  "generate:db2": "prisma generate --schema=prisma/db2/schema.prisma",
  "generate:all": "npm run generate:db1 && npm run generate:db2"
}

3.3. Importação e instanciação de clientes separados

src/db1-client.ts:

import { PrismaClient } from '../node_modules/.prisma/client/db1/index.js'

const prismaDb1 = new PrismaClient()
export default prismaDb1

src/db2-client.ts:

import { PrismaClient } from '../node_modules/.prisma/client/db2/index.js'

const prismaDb2 = new PrismaClient()
export default prismaDb2

src/index.ts:

import prismaDb1 from './db1-client'
import prismaDb2 from './db2-client'

async function main() {
  // Usando banco 1
  const user = await prismaDb1.user.create({
    data: { name: 'João', email: 'joao@email.com' }
  })

  // Usando banco 2
  const product = await prismaDb2.product.create({
    data: { name: 'Notebook', price: 4500.00, stock: 10 }
  })

  console.log({ user, product })
}

main()

4. Execução de migrações em bancos distintos

4.1. Comando migrate dev com --schema

npx prisma migrate dev --schema=prisma/db1/schema.prisma --name init
npx prisma migrate dev --schema=prisma/db2/schema.prisma --name init

4.2. Gerenciamento de ordem de migrações

Se houver dependência entre bancos (ex: db2 referencia dados do db1), execute migrações na ordem correta:

"scripts": {
  "migrate:all": "npm run migrate:db1 && npm run migrate:db2",
  "migrate:db1": "prisma migrate dev --schema=prisma/db1/schema.prisma",
  "migrate:db2": "prisma migrate dev --schema=prisma/db2/schema.prisma"
}

4.3. Estratégias de rollback e versionamento

Para deploy, use prisma migrate deploy com o schema correto:

"scripts": {
  "deploy:db1": "prisma migrate deploy --schema=prisma/db1/schema.prisma",
  "deploy:db2": "prisma migrate deploy --schema=prisma/db2/schema.prisma"
}

Cada banco mantém sua própria tabela _prisma_migrations, permitindo rollback independente.

5. Operações transacionais entre bancos diferentes

5.1. Limitação do Prisma

O Prisma não suporta transações distribuídas. Cada PrismaClient gerencia transações apenas no seu banco:

// Isso funciona apenas para um banco
await prismaDb1.$transaction([
  prismaDb1.user.create({ data: { name: 'A', email: 'a@a.com' } }),
  prismaDb1.user.create({ data: { name: 'B', email: 'b@b.com' } })
])

5.2. Padrão Saga para consistência eventual

Implemente um saga manual para operações entre bancos:

async function createUserAndOrder(userData: any, orderData: any) {
  let user

  try {
    // Passo 1: criar usuário no db1
    user = await prismaDb1.user.create({ data: userData })

    // Passo 2: criar pedido no db2
    await prismaDb2.order.create({
      data: { ...orderData, userId: user.id }
    })
  } catch (error) {
    // Compensação: deletar usuário se pedido falhar
    if (user) {
      await prismaDb1.user.delete({ where: { id: user.id } })
    }
    throw error
  }
}

5.3. Implementação manual com retry

Use bibliotecas como p-retry para tentativas:

import pRetry from 'p-retry'

async function createWithRetry(data: any) {
  return pRetry(() => prismaDb1.user.create({ data }), {
    retries: 3,
    onFailedAttempt: error => {
      console.log(`Tentativa ${error.attemptNumber} falhou`)
    }
  })
}

6. Consultas e relacionamentos entre bancos (cross-database)

6.1. Ausência de foreign keys

Como não há foreign keys entre bancos, modele relacionamentos via IDs manuais:

// No db2, Order armazena userId manualmente
model Order {
  id        Int   @id @default(autoincrement())
  userId    Int   // referência manual ao User do db1
  productId Int
  quantity  Int
}

6.2. Joins em nível de aplicação

Faça duas queries e combine os resultados:

async function getUserWithOrders(userId: number) {
  const user = await prismaDb1.user.findUnique({ where: { id: userId } })
  const orders = await prismaDb2.order.findMany({ where: { userId } })

  return { ...user, orders }
}

6.3. Cache de dados de referência

Para evitar múltiplas chamadas, use cache em memória:

const userCache = new Map<number, any>()

async function getCachedUser(userId: number) {
  if (!userCache.has(userId)) {
    const user = await prismaDb1.user.findUnique({ where: { id: userId } })
    userCache.set(userId, user)
  }
  return userCache.get(userId)
}

7. Boas práticas, testes e deploy com múltiplos bancos

7.1. Singleton por banco

Evite criar múltiplas instâncias do mesmo cliente:

// db1-client.ts
let prismaDb1: PrismaClient

export function getDb1Client(): PrismaClient {
  if (!prismaDb1) {
    prismaDb1 = new PrismaClient()
  }
  return prismaDb1
}

7.2. Testes unitários com bancos isolados

Use bancos de teste separados:

// .env.test
DATABASE_URL_DB1="postgresql://user:pass@localhost:5432/db1_test"
DATABASE_URL_DB2="postgresql://user:pass@localhost:5432/db2_test"

Crie fixtures independentes para cada banco:

// test/fixtures/db1-fixtures.ts
export async function createTestUser() {
  return prismaDb1.user.create({
    data: { name: 'Test', email: 'test@test.com' }
  })
}

7.3. Pipeline CI/CD

No pipeline, execute migrações em paralelo:

jobs:
  migrate:
    strategy:
      matrix:
        db: [db1, db2]
    steps:
      - run: npm run migrate:${{ matrix.db }}

Valide a integridade com scripts:

"scripts": {
  "validate": "node scripts/check-consistency.js"
}

Referências