DEV Community

Cover image for Mongoose e Padrões de Design: Armazene o que você consulta
Eduardo Rabelo
Eduardo Rabelo

Posted on • Updated on

Mongoose e Padrões de Design: Armazene o que você consulta

Créditos


Uma pegadinha comum ao usar .populate do Mongoose é 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.

// 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 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:

// 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.

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 que se comporta de maneira semelhante a uma junção externa esquerda (SQL LEFT 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.

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, o que, por sua vez, pode causar queries lentas.

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!

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.

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. Meu exemplo preferido é o seguinte: digamos que você esteja criando um aplicativo de entrega de gás. 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.

Top comments (1)

Collapse
 
gojo11122 profile image
gojo

Casino stands out as a venture backed by substantial capital from China. Its transformation from the (former) Pharaoh 헤라카지노 Casino demonstrates our commitment to evolution and improvement, ensuring that we provide the best possible gaming experience to our users.