sequelize: Load/include association for already existing instance

Not sure if this is possible. So I can fetch associations when I am finding object via include:

models.User.find({ where: { id: 1 }, include: [models.Avatar]).then(function(user) {
  user.Avatar // will have avatar object associated with user object.
});

But when I am creating user and use setter to set association, how to get user object in the end to have user.Avatar?

user.setAvatar(avatar).then(function(user) {
  user.Avatar // not defined?
});

Should I always manually assign object after setting relationship?

About this issue

  • Original URL
  • State: open
  • Created 9 years ago
  • Reactions: 48
  • Comments: 27 (9 by maintainers)

Most upvoted comments

Please don’t +1 issues. It clutters the thread without adding value to the discussion and spams maintainers with notifications. Use GitHub reactions to upvote features. image

I’m shocked that Sequelize doesn’t support this feature. As PHPer before, we benefit this feature provided by Eloquent ORM in Laravel for a long time. I prefer to borrow the load API from Eloquent for loading nested associations with the same queries syntax as Sequelize. For example,

user.load([ { association: 'posts', include: [ 'comments' ] } ]);

Comparing to lazy load mentioned above, it would be easier to load all related associations in one call and optimize the SQL queries as much as possible.

Is this still on the roadmap? It seems very counter-intuitive that lazy loaded data is not actually stored in the parent model, making it kind of pointless since it requires re-running queries every time when you want to access the data.

Instead of:

const worker = Worker.findOne();
Worker.getProject() // Project
worker.project  // undefined 

To have:

const worker = Worker.findOne();
Worker.getProject() // Project
worker.project  // Project

I made myself a lazyload() helper function as a stopgap. (Written in TypeScript)

import { capitalize } from 'lodash';
import { Model } from 'sequelize-typescript';
import { isUndefined } from 'util';

export async function lazyload<T extends Model<any>>(entity: T, association: keyof T): Promise<T> {
	if (!isUndefined(entity[association])) {
		return Promise.resolve(entity);
	}

	const loader = 'get' + capitalize(association);
	entity[association] = await entity[loader]();

	return entity;
}

And in the same area, it would be nice if, following a lazy loading, the result would be stored in the instance. Actually, it might be a little more complicated than that as it’s the lazy loading mechanism that could be improved.

For example, I have various controllers that load an object and they all pass it to a common utility method to convert that object into a JSON for the HTTP response. In this utility, I often need to read the relations of the entity and I can’t be sure that the controller code loaded all the required relations. To avoid issuing another query to the database if the relation is already loaded, I do something like: return Promise.props( id: team.id name: team.name manager: if team.Manager? then UserService.getBasicUserForView( team.Manager ) else team.getManager().then (manager)-> UserService.getBasicUserForView( manager ) league: if team.League? then LeagueService.getBasicLeagueForView( team.League ) else team.getLeague().then (league)-> LeagueService.getBasicLeagueForView( league ) # Get players and we will convert them later players: team.Players ? team.getPlayers() )

It would be nice if: 1 - team.getManager would automatically persist the result into team.Manager so if for some reason that relation is used again, it would be already loaded 2 - team.getManager skipped the database query if the object was already loaded

That way, I could simply do the following and the database would be hit only if needed: return Promise.props( id: team.id name: team.name manager: team.getManager().then (manager)-> UserService.getBasicUserForView( manager ) league: team.getLeague().then (league)-> LeagueService.getBasicLeagueForView( league ) # Get players and we will convert them later players: team.Players ? team.getPlayers() )

I can see some problems when filters are used in the getter for toMany relations. The rule could be that the getter set the relation property if no filter was applied. Also, the getter would return the cached value if the getter is called without any filter. There could also be an option in the getter to force a dabase read.

I’m coming from java and hibernate and I assumed that this is how it was already implemented. I was surprised when I looked at the query log and realized that the lazy loaded associations are not kept in memory after the lazy init.

BTW, great work guys. I just finished migration my project from waterline to Sequelize and there is no looking back. This project and your support is amazing.

Manuel

On 29 March 2015 at 15:44, Pavel Karoukin notifications@github.com wrote:

Not sure if this is possible. So I can fetch associations when I am finding object via include:

models.User.find({ where: { id: 1 }, include: [models.Avatar]).then(function(user) { user.Avatar // will have avatar object associated with user object. });

But when I am creating user and use setter to set association, how to get user object in the end to have user.Avatar?

user.setAvatar(avatar).then(function(user) { user.Avatar // not defined? });

Should I always manually assign object after setting relationship?

— Reply to this email directly or view it on GitHub https://github.com/sequelize/sequelize/issues/3424.

@matthewstrom that worked nicely! Added it to all classes

sequelize.Model.prototype.lazyLoad = async function lazyLoad(associations, { refresh } = { refresh: false }) {
  await Promise.all(associations.map(async association => {
    if (!this[association] || refresh) {
      const loader = `get${capitalize(association)}`;
      this[association] = await this[loader]();
    }
  }));

  return Promise.resolve(this);
};

Any update on this? I know it’s been a while For the moment, I’m forced to do something like this if I want to make sure I don’t hit the DB unnecessarily

const getOrganization = user.organization
  ? Promise.resolve(user.organization)
  : user.getOrganization();

I think it could easily be implemented as an option in the getter. Just pass { lazy: true } or something and the getter just resolves the cached value if present

This is exactly what my point was, it feels like you need to just do way too much work to get associated values out sometimes.

Surely building against the base item is a better way of doing things, rather than separate entities.

With active record a call of ‘author.books’ just means you have access to the authors books right away.

Maybe, its just that the sequelize documentation is poor, I came from years of Rails, and every single Node ORM I tried playing with seemed to have a clear lack of good, clear documentation. Sequelize was the best, but still lacking. (Active Record association docs for reference: http://guides.rubyonrails.org/association_basics.html)


Again, I’m not trying to shit on the work you guys are doing, however, I think a lot could be learned from more mature libraries. 😃

On Tue, Dec 5, 2017 at 10:31 AM darkalor notifications@github.com wrote:

Is this still on the roadmap? It seems very counter-intuitive that lazy loaded data is not actually stored in the parent model, making it kind of pointless since it requires re-running queries every time when you want to access the data.

Instead of:

const worker = Worker.findOne(); Worker.getProject() // Project worker.project // undefined

To have:

const worker = Worker.findOne(); Worker.getProject() // Project worker.project // Project

— You are receiving this because you commented. Reply to this email directly, view it on GitHub https://github.com/sequelize/sequelize/issues/3424#issuecomment-349262772, or mute the thread https://github.com/notifications/unsubscribe-auth/AE2D298oISMHMmGw2RlwPPRziRZG8br-ks5s9RtjgaJpZM4D2tw5 .

It seems model.$get('association') is what we were looking for, as in user.$get('comments') where comments is the name of the association, not the model. At least if you use sequelize-typescript, it is documented here: https://github.com/sequelize/sequelize-typescript#type-safe-usage-of-auto-generated-functions

It is very confusing that I can not write an abstract code that suppose to process model associations like:

async processUser(user) {
  let comments = await user.getComments()
  ...
}

In order to make this method take benefit of eager loading I would need to check how the comments where loaded into the user model:

let comments = user.Comments || await user.getComments()

And there is still no guarantee that user.Comments is the same as a defined association.

The way eager loading is implemented in Sequelize breaks the whole idea of OOD where you expect all instances of the same class to have the same API but not conditional properties depending on how the a class instance was constructed (aka queried).

I think get<association name> should always return a Promise. However, if the association was eager loaded without additional conditions (like custom where), it should just reuse that result internally like Promise.relolve(eagerLoadingResult).

I find that very counter-intuitive, but… okay.

@rsshilli that is as designed. getDepartments() is for lazy loading, that is why it is a method and not a property and returns a Promise. user.Departments is only present if you eager loaded the association through include.

Hey guys any news on this? I’m kinda shocked sequelize doesn’t support this feature… very basic in most ORMs