mongoose-association
TypeScript icon, indicating that this package has built-in type declarations

0.0.44 • Public • Published

mongoose-association

A cleaner and faster way to setup mongoose populate with virtual field using normalized associations. This library is built for async await node environment minimum 7.6 This library is built for mongo 3.6, and mongoose version >=4.11.0

Setup

npm install mongoose-association

  const mongoose = require('mongoose')
  const { mongooseAssociation } = require('mongoose-association')
  mongooseAssociation(mongoose)

Associations

mongoose-association has 4 types of association to relation mongoose schemas.

  • belongsTo o ->
  • polymorphic o =>
  • hasOne through -> o
  • hasMany through ->> o

belongsTo

this relation specify the reference lies directly on the schema and related to a single model using a foreignField

belongsTo(foreignModelName, { as, localField, foreignField } = {}, schemaOptions = {}) {
 
}
foreignModelName required
as optional
localField optional
foreignField optional
schemaOptions optional - for additional mongoose schema feature e.g. { required: true }

polymorphic

this relation specify the reference lies directly on the schema and related to multiple models

polymorphic([foreignModelName, ...], { as, localField, foreignField, typeField } = {}, schemaOptions = {}) {
 
}
foreignModelName required - Array of modelNames
as optional
localField optional
foreignField optional
typeField optional
schemaOptions optional

hasOne

this relation specify the reference lies on another schema and it is unique

hasOne(foreignModelName, { as, with, localField, foreignField, through, throughAs, throughWith } = {}) {
 
}
foreignModelName required
as optional
with optional
through optional
throughAs optional
throughWith optional

hasMany

this relation specify the reference lies on another schema and it is non-unique

hasMany(foreignModelName, { as, with, localField, foreignField, through, throughAs, throughWith } = {}) {
 
}
foreignModelName required
as optional
with optional
through optional
throughAs optional
throughWith optional

Naming Convention

As mongoose model classes are typically singular thus mongoose-association stick to naming models using singular words. As typically javascript utilize camel casing thus all auto generated naming is done using camel case

Glossary

foreignModelName

the modelName property for a mongoose model holding the related schema.

as

the property on mongoose model instance that holds the related object

with

for has associations, specify which as property is referenced.

foreignField

the schema property for the relational reference typically an mongoose ObjectID

localField

the model instance property that holds relational record

through

through is used to associate via another document type other than the two document types forming the relation

throughAs

specifies the reference between the through model and associated model.

throughWith

specifies the reference between the through model and association origin model.

Schema Building

Once we have apply the plugin to mongoose, we can start defining some schemas

const mongoose = require('mongoose')
const { Schema } = mongoose
 
const riderSchema = new Schema()
riderSchema.belongsTo('Bike').index(1, { sparse: true })
riderSchema.belongsTo('Helmet')

Right here we have defined a schema for Rider. Using the belongsTo method with the model Bike can automatically have the localField defined as bike, this results in a standard mongoose virtual property bike on each instance. all mongoose-association defined virtuals returns a promise and is designed to function with await. A localField was also automatically defined as bikeId. This will be the auto generated property storing the reference on the databased mongoose document. Here we have also applied an index on the localField generated by belongsTo

const bikeSchema = new Schema()
bikeSchema.hasOne('Rider')
bikeSchema.hasOne('Helmet', {
  through: 'Rider'
})

In this weird world each bike only have zero or one Rider and Helmet. we can now define a reverse relationship using hasOne method. Because we know how Rider behaves with both Bike and Helmet. We can infer that a bike can only have zero or one Helmet. This inference can only be made through the Rider

const helmetSchema = new Schema()
helmetSchema.hasOne('Rider')
helmetSchema.hasOne('Bike', {
  through: 'Rider'
})

mongo's limitation on $lookup restrict the through relationship to span only across three document types.

const registrationSchema = new Schema()
registrationSchema.belongsTo('Car')
registrationSchema.belongsTo('Alien', {
  as: 'owner'
})

Lets visit another scenario that is a bit foreign, where we run an Alien Car Registration. There are two different ways an Alien can interact with a Registration. One is by assigning the as to be owner, which renders the localField to be ownerId. this allows the Alien to be the "owner" of the Car.

registrationSchema.belongsTo('Alien', {
  as: 'approver',
  localField: 'approver_id'
})

Similarly an Alien can also be an approver of the registration, and the localField can be modified to use snake case approver_id or any format preferred.

const alienSchema = new Schema()
alienSchema.hasMany('Registration', {
  as: 'ownedRegistration',
  with: 'owner'
})

With this robust system we can declare that each Alien may have many Registration. by default the as would be the downcase pluralize version of the modelName, in this case as registrations. We also had to apply owner for with, otherwise we are unable to distinguish which interaction Alien has with the Registration

alienSchema.hasMany('Registration', {
  as: 'approvedRegistrations',
  with: 'approver'
})

Alien can also be the approver of Registration. This is where we can define that each Alien to hasMany approvedRegistrations using as. The localField is fetch via the reverse association as approver_id.

alienSchema.hasMany('Car', {
  through: 'Registration',
  with: 'owner'
})

hasMany through is normally used for many to many relationships. with function in reverse of as in term of its ability to defined the localField and foreignField used in the mongo $lookup. In this case the owner will be used to reference ownerId. This relationship will result in the as of cars, and using the localField of carId

alienSchema.hasMany('Car', {
  through: 'Registration',
  with: 'approver',
  as: 'approvedCars'
})

Similar to above case of the cars, this association stores approvedCars through Registration using the approver for as.

const carSchema = new Schema()
carSchema.hasOne('Registration')
carSchema.hasOne('Alien', {
  through: 'Registration',
  throughAs: 'owner'
})

This is a reverse hasMany relationship coming from Car. The relationship to Alien is through Registration thoughAs owner.

const assemblySchema = new Schema()
const vehicleAssociation = assemblySchema.polymorphic(['Bike', 'Car'], {
  as: 'vehicle'
})
const partAssociation = assemblySchema.belongsTo('Part')
assemblySchema.indexAssociations([vehicleAssociation, 1], [partAssociation, 1], { unique: true })

polymorphic is last type of relation, and it is most similar of to belongsTo, with the exception that more than one model can be associated. A as is required for polymorphic association because mongoose-association doesn't pick favors in auto generation. Though the localField is inferred from the as as vehicleId. polymorphic also generates an additional schema property storing the document type modelName this property is auto generated using the foreignField as vehicleIdType. Using the index Association method we were able to create a compound index on the assembly schema using both the localField and typeField of the polymorphic association along with the localField of the belongsTo with Part

const partSchema = new Schema()
partSchema.hasMany('Assembly')
partSchema.hasMany('Bike', {
  through: 'Assembly',
  throughAs: 'vehicle'
})
partSchema.hasMany('Car', {
  through: 'Assembly',
  throughAs: 'vehicle'
})

A model can also define hasMany through a polymorphic relationship. with perform identical functionality in this scheme.

carSchema.hasMany('Assembly', {
  with: 'vehicle'
})
carSchema.hasMany('Part', {
  through: 'Assembly',
  with: 'vehicle'
})
 
bikeSchema.hasMany('Assembly', {
  with: 'vehicle'
})
bikeSchema.hasMany('Part', {
  through: 'Assembly',
  with: 'vehicle',
  as: 'components'
})

The reverse hasMany relationships that is mostly auto generated

carSchema.hasOne('Rating', {
  with: 'vehicle'
})
bikeSchema.hasOne('Rating', {
  with: 'vehicle'
})
 
const ratingSchema = new Schema()
ratingSchema.polymorphic(['Bike', 'Car'], {
  as: 'vehicle'
})
ratingSchema.belongsTo('Alien')
ratingSchema.hasOne('Rider', {
  through: 'Bike'
})
riderSchema.hasOne('Rating', {
  through: 'Bike'
})
 
alienSchema.hasOne('Rating')
alienSchema.hasOne('Car', {
  through: 'Rating',
  throughAs: 'vehicle',
  as: 'ratedCar'
})

Make sure the model is defined after all the schema fields. Otherwise the getters and setters on the model instance will miss behave

const Rider = mongoose.model('Rider', riderSchema)
const Bike = mongoose.model('Bike', bikeSchema)
const Helmet = mongoose.model('Helmet', helmetSchema)
const registration = mongoose.model('Registration', registrationSchema)
const Alien = mongoose.model('Alien', alienSchema)
const Car = mongoose.model('Car', carSchema)
const Assembly = mongoose.model('Assembly', assemblySchema)
const Part = mongoose.model('Part', partSchema)
const Rating = mongoose.model('Rating', ratingSchema)
const Settings = mongoose.model('Settings', settingsSchema)

Persisting Relationship

creating records with relationship

const bike = await new Bike().save()
const helmet = await new Helment().save()
const rider = await new Rider({
  bike,
  helmet
}).save()
const bikeId = rider.bikeId // returns ObjectId

updating relationship on record

const anotherBike = await new Bike().save()
rider.bike = anotherBike
await rider.save()

working with polymorphic relationship

const bike = await new Bike().save()
const rating = await new Rating({
  vehicle: bike
}).save()
const car = await new Car().save()
rating.vehicle = car
rating.save()

Populating Association

from the object itself

const rider = await Rider.findOne() // request count 1
rider.populateAssociation('bike.rating', 'helmet') // request count 4
const bike = await rider.bike // request count 4
const helmet = await rider.helmet // request count 4
const rating = await bike.rating // request count 4

from the model

const rider = await Rider.findOne() // request count 1
Rider.populateAssociation(rider, 'bike.rating', 'helmet') // request count 4
const bike = await rider.bike // request count 4
const helmet = await rider.helmet // request count 4
const rating = await bike.rating // request count 4

from the query is slightly more efficient allows for further optimization

const rider = await Rider.findOne().populateAssociation('bike.rating', 'helmet') // request count 2
const bike = await rider.bike // request count 2
const helmet = await rider.helmet // request count 2
const rating = await bike.rating // request count 2

Fetching From New Query Instead of Cache

const rider = await Rider.findOne().populateAssociation('bike.rating', 'helmet') // request count 2
const bike = await rider.fetchBike() // request count 3
const sameBike = await rider.fetchBike().populateAssociation('rating') // request count 4
const rating = await sameBike.rating // request count 4

unsetting association cache

const rider = await Rider.findOne().populateAssociation('bike.rating', 'helmet') // request count 2
const bike = await rider.unsetBike().bike // request count 3

helper methods to do more meta programming

const rider = await Rider.findOne().populateAssociation('bike.rating', 'helmet') // request count 2
const bike = await rider.fetch('bike') // request count 3
const sameBike = await rider.unset('bike').bike // request count 4
const anotherSameBike = await rider.unset().bike // request count 5

Explaining Query/Aggregate Population Plan

All mongoose Queries and Aggregates now have the explain method which returns the population plan it will use to optimize the fetching of associations.

Rider.findOne({ _id: riders[0]._id }).populateAssociation('helmet', 'bike.assemblies.part')._explain()
[
  [
    'aggregate', 'Rider', [{
      $match: {
        _id: 5 b47845d8222031fd88ef049
      }
    }, {
      $limit: 1
    }, {
      $lookup: {
        from: 'helmets',
        'let': {
          localField: '$helmetId'
        },
        pipeline: [{
          $match: {
            $expr: {
              $eq: ['$$localField', '$_id']
            }
          }
        }],
        as: 'helmet'
      }
    },
    {
      $unwind: '$helmet'
    }, {
      $lookup: {
        from: 'bikes',
        let: {
          localField: '$bikeId'
        },
        pipeline: [{
          $match: {
            $expr: {
              $eq: ['$$localField', '$_id']
            }
          }
        }],
        as: 'bike'
      }
    },
    {
      $unwind: '$bike'
    }
  ], [
    'aggregate', 'Assembly', [{
      $match: {
        vehicleId: {
          $in: [ 'Bike._id' ]
        },
        vehicleIdType: 'Bike'
      }
    }, {
      $lookup: {
        from: 'parts',
        let: { localField: '$partId' },
        pipeline: [{
          $match: {
            $expr: {
              $eq: [ '$$localField', '$_id' ]
            }
          }
        }],
        as: 'part'
      }
    }, {
      $unwind: '$part'
    }
  ]
]

built @ OneVigor Logo

Package Sidebar

Install

npm i mongoose-association

Weekly Downloads

4

Version

0.0.44

License

ISC

Unpacked Size

290 kB

Total Files

92

Last publish

Collaborators

  • vickenstein