Mongoose e Padrões de Design: Armazene o que você consulta
Tags: ptbr, webdev, mongodb, database
Published: 2019-10-12 00:09:16

# Créditos

- [Mongoose Design Pattern: Store What You Query For](http://thecodebarbarian.com/mongoose-schema-design-pattern-store-what-you-query-for.html), escrito originalmente por [Valeri Karpov](https://twitter.com/code_barbarian)

---

Uma pegadinha comum ao usar [.populate do Mongoose](https://mongoosejs.com/docs/populate.html) é que você não pode filtrar por campos na coleção estrangeira. Por exemplo, suponha que você tenha 2 modelos: **Book** e **Author** e você deseja filtrar os livros pelo nome do autor.

```js
// 2 modelos: Book e Author
const Book = mongoose.model('Book', Schema({
  title: String,
  author: {
    type: mongoose.ObjectId,
    ref: 'Author'
  }
}));
const Author = mongoose.model('Author', Schema({
  name: String
}));

// Criando livros e autores
const [author1, author2] = await Author.create([
  { name: 'Michael Crichton' },
  { name: 'Ian Fleming' }
]);
const books = await Book.create([
  { title: 'Jurassic Park', author: author1._id },
  { title: 'Casino Royale', author: author2._id }
]);

// Preenchendo livros e filtrando por nome do autor
const books = Book.find().populate({
  path: 'author',
  match: { name: 'Ian Fleming' }
});

books.length; // 2
books[0].author; // null
books[1].author; // { name: 'Ian Fleming' }
```

No exemplo acima, mesmo que os filtros do [.populate com match](http://thecodebarbarian.com/mongoose-5-5-static-hooks-and-populate-match-functions#populate-match-function) sejam baseados no nome do autor, o Mongoose ainda retorna todos os livros, incluindo aqueles `author.name` que não correspondem ao valor usado. Caso `author.name` não seja 'Ian Fleming', a propriedade `author` do livro será `null`.

Isso ocorre porque, sob o capô, o Mongoose transforma `Book.find().populate('author')` em duas consultas:

1. `const books = await Book.find({})`
2. `Author.find({ _id: { $in: books.map(b => b.author) }, name: 'Ian Fleming' })`

Então, `.populate()` encontra todos os livros primeiro e depois encontra os autores correspondentes.

# Armazene o que você consulta

Se você precisar filtrar livros pelo nome do autor de maneira eficiente, o caminho certo é armazenar o nome do autor no documento do livro:

```js
// 2 modelos: Book e Author
const Book = mongoose.model('Book', Schema({
  title: String,
  author: {
    type: mongoose.ObjectId,
    ref: 'Author'
  },
  authorName: String
}));

const authorSchema = Schema({ name: String });
// Adicionamos um middleware para atualizar o não-referenciado (desreferenciado) `authorName`
authorSchema.pre('save', async function() {
  if (this.isModified('name')) {
    await Book.updateMany({ authorId: this.author }, { authorName: this.name });
  }
});
const Author = mongoose.model('Author', authorSchema);
```

Dessa forma, você pode filtrar e classificar pelo nome do autor sem um extra `.populate()`. O padrão de armazenar o nome do autor no `bookSchema` e atualizar a coleção de livros sempre que o nome de um autor mudar é chamado de **desreferenciação** . A **desreferenciação** ou incorporação de dados de uma coleção em outra coleção é como você pode executar o MongoDB em grande escala sem usar soluções de cache como o [memcached](https://memcached.org/).

Se você estiver criando uma aplicação que faz muitas leitura ao banco, é provável que atualize os nomes dos autores com pouca frequência, mas filtrar e classificar os livros por nome de autor é algo frequente. Uma dica de memorização útil para essa regra geral é "armazenar o que você consulta". Faça as consultas que você executa com mais frequência com rapidez, com o custo de fazer atualizações pouco frequentes um pouco mais lentas.

# Diferenças ao usar `$lookup`

[O MongoDB 3.6 introduziu o operador de agregação $lookup](http://thecodebarbarian.com/a-nodejs-perspective-on-mongodb-36-lookup-expr) que se comporta de maneira semelhante a [uma junção externa esquerda (_SQL LEFT JOIN_)](https://www.dofactory.com/sql/left-outer-join) . Em outras palavras, mesmo se você não desreferir a propriedade `author`, poderá usar a estrutura de agregação do `$lookup` e classificar os livros pelo nome do autor.

```js
await Author.create([
  { name: 'Michael Crichton' },
  { name: 'Ian Fleming' }
]);
await Book.create([
  { title: 'Jurassic Park', author: author1._id },
  { title: 'Casino Royale', author: author2._id }
]);

const books = await Book.aggregate([
  {
    $lookup: {
      from: 'Author',
      localField: 'author',
      foreignField: '_id',
      as: 'authorDoc'
    }
  },
  {
    $sort: {
      'authorDoc.name': 1
    }
  }
]);

books[0].title; // 'Casino Royale'
books[1].title; // 'Jurassic Park'
```

Por que o Mongoose não faz uso do `$lookup`? A questão se resume a um desempenho consistente. Como o `$lookup` executa uma pesquisa separada para todos os documentos que entram no estágio de `$lookup`, [seu desempenho diminui para `O(n^2)` no caso de erros no índice](http://thecodebarbarian.com/slow-trains-in-mongodb-and-nodejs#break-up-one-slow-operation-into-many-fast-operations), o que, por sua vez, pode causar [queries lentas](http://thecodebarbarian.com/slow-trains-in-mongodb-and-nodejs).

Por outro lado, o Mongoose executa 1 consulta para cada `.populate()`, o que leva a uma melhor taxa de transferência e apenas uma varredura de coleção no evento de uma falha no índice.

# Mas existem [anomalias de atualização](https://www.essentialsql.com/get-ready-to-learn-sql-11-database-third-normal-form-explained-in-simple-english/)!

Se você atualizar manualmente o banco de dados ou tiver um aplicativo separado que não atualiza corretamente o `authorName`, poderá haver um caso em que o nome do autor no modelo **Book** não esteja alinhado com o modelo **Author**. Embora anomalias de atualização como essa sejam certamente possíveis, elas são raras na produção: as causas mais prováveis ​​são uma atualização manual no banco de dados que ignora o aplicativo ou um desenvolvedor usando um padrão que ignora o middleware do Mongoose.

```js
const authorSchema = Schema({ name: String });
// Adicionamos um middleware para atualizar o não-referenciado (desreferenciado) `authorName`
 `authorName`
authorSchema.pre('save', async function() {
  if (this.isModified('name')) {
    await Book.updateMany({ authorId: this.author }, { authorName: this.name });
  }
});
const Author = mongoose.model('Author', authorSchema);

// Não irá acionar o middleware 'save'. Você terá que chamar um `pre('updateOne')` manualmente.
await Author.updateOne({}, { name: 'test' });
```

Se ocorrerem anomalias de atualização, elas serão fáceis de corrigir com uma migração. É muito mais fácil identificar e corrigir anomalias de atualização do que a degradação generalizada do desempenho da sua aplicação.

Além disso, [às vezes os dados "desatualizados" são um recurso, não um bug](http://thecodebarbarian.com/managing-embedded-documents-with-monogram#updating-an-existing-document). Meu exemplo preferido é o seguinte: digamos que você esteja criando um [aplicativo de entrega de gás](https://trybooster.com/). Cada solicitação está associada a um veículo. Se um cliente atualiza seu veículo de um Toyota Camry 2015 para um BMW X1 2018, isso deve afetar as solicitações feitas há dois anos?

# Continuando

"Armazene o que você consulta" é como garantir um desempenho consistente do MongoDB ao usar o Mongoose. Na minha experiência, a maioria dos problemas de desempenho do MongoDB se deve a índices ausentes e/ou a uma agregação excessivamente complexa que pode ser substituída por uma única consulta com alguns ajustes no esquema. Da próxima vez que você estiver coçando a cabeça, se perguntando por que sua agregação é lenta, pense em quais propriedades você pode desreferenciar para otimizar suas necessidades de consulta.