Does updating a document always result in a fetch?

When I run the following command, my APM logs seem to indicate that 5 requests take place:

Chats.update({_id:chatId},{
  $addToSet:{messages:message,unread:userId},                                                                   
  $set: {date:new Date()},                                                                                
  $unset: {archived:''}                                                                                  
})

Screen Shot 2020-02-13 at 8.44.11 AM

They’re all initiated from the same line, as well.

Is this normal? Those fetch methods seem quite inefficient as they don’t specify any fields – the entire document is fetched.

from your trace, it looks like you are using collection-hooks, are the extra fetches happening there?
The second line shows a find running as a result of a hook, so it’s likely the fetch happens there too

Interesting observation! Digging into it, the only package I have installed that uses collection-hooks is todda00:friendly-slugs. And indeed, this seems to be related to that adding before and after to each and every call, even if I don’t have friendlySlugs on that collection. Argh. Gonna be a pain to replace that library haha.

No need to worry, I can give you the code for slugs, you can get it from Stackoverflow or just get it from the repo of the friendly-slugs. Here’s my version of it as a function:
name is what you need a slug for and type can be removed entirely or use it to put slugs in various places (Collections) with the same function.
I use this in onCreateUser. For best performance you would have a collection index on the slug field

const transliteration = [
  { from: 'àáâäåãа', to: 'a' },
  { from: 'б', to: 'b' },
  { from: 'ç', to: 'c' },
  { from: 'д', to: 'd' },
  { from: 'èéêëẽэе', to: 'e' },
  { from: 'ф', to: 'f' },
  { from: 'г', to: 'g' },
  { from: 'х', to: 'h' },
  { from: 'ìíîïи', to: 'i' },
  { from: 'к', to: 'k' },
  { from: 'л', to: 'l' },
  { from: 'м', to: 'm' },
  { from: 'ñн', to: 'n' },
  { from: 'òóôöõо', to: 'o' },
  { from: 'п', to: 'p' },
  { from: 'р', to: 'r' },
  { from: 'с', to: 's' },
  { from: 'т', to: 't' },
  { from: 'ùúûüу', to: 'u' },
  { from: 'в', to: 'v' },
  { from: 'йы', to: 'y' },
  { from: 'з', to: 'z' },
  { from: 'æ', to: 'ae' },
  { from: 'ч', to: 'ch' },
  { from: 'щ', to: 'sch' },
  { from: 'ш', to: 'sh' },
  { from: 'ц', to: 'ts' },
  { from: 'я', to: 'ya' },
  { from: 'ю', to: 'yu' },
  { from: 'ж', to: 'zh' },
  { from: 'ъь', to: '' }
]

const createUserSlug = (name, type) => {
  const lowCaseStr = name.toLowerCase()
  let slug = lowCaseStr
    .replace(/'/g, '') // Remove all apostrophes
    .replace(/[^0-9a-z.]/g, '') // Remove anything that is not 0-9, a-z, dot
    /* eslint-disable */
    .replace(/\-\-+/g, '') // Remove multiple -
    /* eslint-enable */
    .replace(/^-+/, '') // Trim - from start of text
    .replace(/-+$/, '') // Trim - from end of text
  transliteration.map(item => {
    slug = slug.replace(new RegExp('[' + item.from + ']', 'g'), item.to)
  })

  if (type === 'user') {
    if (Meteor.users.findOne({ slug })) {
      do {
        const randomIndex = Math.floor(Math.random() * 9) + 1
        slug = slug + randomIndex.toString()
      }
      while (Meteor.users.findOne({ slug }))
    }
    return slug
  } else if (type === 'org') {
    if (Organizations.findOne({ slug })) {
      do {
        const randomIndex = Math.floor(Math.random() * 9) + 1
        slug = slug + randomIndex.toString()
      }
      while (Organizations.findOne({ slug }))
    }
    return slug
  } else if (type === 'school') {
    if (Schools.findOne({ slug })) {
      do {
        const randomIndex = Math.floor(Math.random() * 9) + 1
        slug = slug + randomIndex.toString()
      }
      while (Schools.findOne({ slug }))
    }
    return slug
  }
}



Thanks Paul! I wrote something similar – one thing is that I need to maintain the friendlySlugs structure that exists. Here’s where I landed:

import slugify from 'slugify'
newFriendlySlug = function(data,field,collection) {
    if (!data || !field || !collection) return new Date().getTime()
    var slug = slugify(data[field]) 
    var count = collection.find({'friendlySlugs.slug.base':slug}).count()
    data.friendlySlugs = {slug:{
        base:slug,
        index:count
    }}
    if (count > 0) {
        var slug = slug + '-' + count
    }
    data.slug = slug
    return data
}

To insert a new document

var data = {
  name:'South Africa',
  code: 'ZA',
}
var data = newFriendlySlug(data,'name',Countries)
Countries.insert(data)

The inserted document looks like this:

{
  name:'South Africa',
  code:'ZA',
  slug:'south-africa',
  friendlySlugs:{
    slug:{
      base:'south-africa',
      index:0,
    }
  }
}

I had a similar problem with collection-hooks and it’s instance on fetching the entire document before and after the update, to pass to the hooks - even if there are no hooks defined or the hooks don’t need it.

See this bug report:

2 Likes

FYI, and if it helps, I’ve written my own (more-efficient) clone of mizzao:partitioner, which includes its own (more efficient) clone (hooks.js) of collection-hooks which doesn’t fetch the document before and after each callback.

These are not intended to be public replacements for those two well-respected packages - the only reason my own forks are published as meteor packages are so that I can share them between my own projects more easily. They work for me in my limited use-cases, but I don’t have time to support them. e.g. hooks.js only supports one hook, not multiple hooks.

However, the hooks.js file might be helpful if you need to fix fiendly-slugs to make it more efficient…

1 Like