Using 2 x hasMany to represent N:M relations has been deprecated. Please use belongsToMany instead

946 views
Skip to first unread message

Chad Robinson

unread,
Jan 3, 2015, 9:42:29 AM1/3/15
to sequ...@googlegroups.com
I have a schema with three tables: users, channels, and roles. Each user may own, produce, or follow one or more channels. Each channel may be owned by, followed by, or produced by, one or more users.

The logical structure is a M:M mapping table, which I called roles. A role entry is a mapping between a user and a channel, with an additional field that identifies the type of role involved. This works very well, is efficient, and matched the old app's data structure - happy days.

But lately I'm getting this error in my logs:
Using 2 x hasMany to represent N:M relations has been deprecated. Please use belongsToMany instead

This is obviously because each user.hasMany(role) and each channel.hasMany(role) and vice-versa. BelongsToMany does work - I can use 'through' to create the specific role mapping table... BUT there doesn't appear to be a way to add fields to that table. Even if I define a "role" schema, SequelizeJS ignores the fields I add to it. I don't want to do something like channel.hasMany(user, { as: 'owner' }) and etc because who knows how many roles I'll have over time - I have three now, but it could easily grow. I don't want separate lists of owners, fans, followers, producers, etc. etc. I just want lists of related users and the types of their relationships.

I'm concerned about this deprecation because it seems to be an edge case the library authors haven't planned for. Is there a way to add fields to a M:M mapping table? I can't find one in the docs.

Mick Hansen

unread,
Jan 3, 2015, 10:15:06 AM1/3/15
to Chad Robinson, sequ...@googlegroups.com
You can use `through` to point at a model, you can then define additional fields on that model.
--
Mick Hansen
@mhansendev
mhansen.io

Chad Robinson

unread,
Jan 4, 2015, 9:16:10 AM1/4/15
to sequ...@googlegroups.com, crob...@medialantern.com
This doesn't appear to work. Sequelize accepts the 'belongsToMany' relationships between the two tables, but ignores the definition of the mapping table itself. So if I have:

User.belongsToMany(Channel, { as: 'channels', through: 'roles' });
Channel.belongsToMany(User, { as: 'users', through: 'roles' });

SequelizeJS will make a 'roles' table with userId and channelId fields. However, if I try to do:

Role = sequelize.define('role', { type: DataTypes.STRING(191) });

The 'type' field is not created on the table. It also appears that this technique discards the role element entirely when doing nested-include loads, so you can't see fields like 'type' above (if that was working), or even SequelizeJS-auto-created fields like createdAt. When loading a user, the user's channels are loaded directly - you don't get the fields that indicate when the role itself was assigned.

Is there a workaround for this? If not, I remain concerned about the deprecation of this option. The old method worked fine, both with custom fields and nested loading. I can't understand what purpose is served by deprecating it.

Mick Hansen

unread,
Jan 4, 2015, 9:17:06 AM1/4/15
to Chad Robinson, sequ...@googlegroups.com
As i said you need to point the through at a model rather than a table string (although i can see how that's not clear).
You need to use `through: Role`

Chad Robinson

unread,
Jan 4, 2015, 9:30:40 AM1/4/15
to sequ...@googlegroups.com, crob...@medialantern.com
Thanks, Mick, that definitely helped... but doing a nested load now flips the order of the objects. You get:

User
   [That user's channels]
        {The role for each channel, now embedded in the channel}

Is there any way to get the original join order, which was:

User
    [That user's roles]
        {The channel for each role}

The entire app is built around the first model and I'd have to have to rewrite the results of every query...

Mick Hansen

unread,
Jan 4, 2015, 9:59:45 AM1/4/15
to Chad Robinson, sequ...@googlegroups.com
What do you mean it now does a flip of the order? It's been that way since we've supported extra attributes on the through model.

To support the case you wan't you have to setup relations directly to the join model and then include that.

Chad Robinson

unread,
Jan 4, 2015, 10:20:29 AM1/4/15
to sequ...@googlegroups.com, crob...@medialantern.com
But... I was totally fine with that. It's what I was doing already. The only reason I started this thread is because for the past few releases I've been getting this deprecation warning:

Using 2 x hasMany to represent N:M relations has been deprecated. Please use belongsToMany instead

This is a really useful pattern - I hope it's not going to be eliminated soon!

Mick Hansen

unread,
Jan 4, 2015, 10:58:39 AM1/4/15
to Chad Robinson, sequ...@googlegroups.com
That deprecation warning will only appear when using N:M with join tables.
If you are using something like A.hasMany(C); B.hasMany(C); C.belongsTo(A); C.belongsTo(B); where C would be the join model it will still work.

We are just moving A.hasMany(B, through: C) to belongsToMany instead (to further differ between 1:M and N:M relations)

Mick Hansen

unread,
Jan 4, 2015, 10:59:32 AM1/4/15
to Chad Robinson, sequ...@googlegroups.com
I think i may have misunderstood your original question.
The deprecation warning you're getting (not an error, just an warning) is likely from another relation and not from user.hasMany(role) and channel.hasMany(role)

Chad Robinson

unread,
Jan 4, 2015, 12:31:46 PM1/4/15
to sequ...@googlegroups.com, crob...@medialantern.com
I appreciate you bearing with this. :)

Unfortunately, no, it's definitely coming from these two. I still get the error if I reduce my model to just these three components (user, channel, role). And it goes away if I switch to User.BelongsToMany(channel) and Channel.BelongsToMany(user), both "through" the role model. (Sorry for the confusion about that - I do see that in the docs now. It's just that there are two "Associations" pages and only the Documentation-section entry makes this clear.)

BelongsToMany is almost an answer, actually. It's just that if you want the result of LEFT JOINs, you want to be able to do the equivalent of
User.findOne({ where: { id: 1 }, include: { model: Role, include: { model: Channel }}});

With two hasMany relationships from User and Channel to Role, and Role.belongsTo(User) / Role.belongsTo(Channel), this works great in either direction. You get your user, it includes an array of that user's roles, and each role includes that role's channel's fields.

If you switch to BelongsToMany, though, you get an error if you try to directly include Role in the query, because Role is no longer directly related to User. You CAN do this:
User.findOne({ where: { id: 1 }, include: { model: Channel }});

This is admittedly shorter and easier to read... but it's harder to code around the result because you pay the price of what you saved here in the iterations across the results. The preference is to let the database do its job and return as close to the final result as possible...

Is there a workaround that allows this? The goal is to allow an eager-loading query between three tables, A, B, and C, where A.belongsToMany(C, through: B), and C.belongsToMany(A, through: B). The desired result is an A record that contains an array of B records, each of which includes the appropriate C record.

If not, any idea how long this deprecation will last? If it's going to be there for years then it's no big deal. I'm only worried about support for it going away in the next few months.

Mick Hansen

unread,
Jan 4, 2015, 12:50:30 PM1/4/15
to Chad Robinson, sequ...@googlegroups.com
Well you should really be able to achieve the same things you were before with the new pattern - You always had to do the HM/BT x 2 to have the join table between the source and the target.

Having the join table data between source and target makes sense in a lot of cases, and there's a lot of senses where it doesn't (where join tables don't hold data) - and it's this way for legacy reasons, but i could see us providing a flag for the functionality you want with belongsToMany alone.

Ideally anything we deprecate we'll want to remove within a few versions, but unless it actively interfers with the betterment/development of the project we won't remove them for a good while.

Can you show me your relations as you had them before where they worked and the way you have them now? I'm sure you should be able to do what you are attempting to do at the moment.

Chad Robinson

unread,
Jan 4, 2015, 1:04:08 PM1/4/15
to sequ...@googlegroups.com, crob...@medialantern.com
Thanks, Mick. Here goes.

Before:

var User = sequelize.define('user', { fields });
var Channel = sequelize.define('channel', { fields });

var Role = sequelize.define('role', { type: DataTypes.STRING(20); }); // Some custom fields to store with the role

User.hasMany(Role);
Channel.hasMany(Role);
Role.belongsTo(User);
Role.belongsTo(Channel);

This produces a 'roles' table with the custom fields (like 'type'), and a userId / channelId field pair with appropriate foreign keys. You can then do:

User.findOne({
   
where: { username: connection.params.username },

    include
: [{
        model
: Role,
        include
: [{
            model
: Channel

       
}]
   
}]
}).then(function(user) {
    console
.log(user);
});

This will output a user record like:

{
    id
: 1,
    name
: 'Your name',
    roles
: [{
        id
: 1,
        userId
: 1,
        channelId
: 1,
        type
: 'follower',
        channel
: {
            id
: 1,
            name
: 'Some channel'
       
}
   
}]
}

Changing this to:

User.belongsToMany(Channel, { as: 'channels', through: Role });
Channel.belongsToMany(User, { as: 'users', through: Role });

produces the same database schema - roles will have userId and channelId added automatically, with appropriate foreign keys. But you can no longer "include" the "roles" table in the query. The only thing you can include is "channels", which returns:

{
    id
: 1,
    name
: 'Your name',
    channels
: [{
        id
: 1,
        role
: {
            id
: 1,
            type
: 'follower',
            name
: 'Some channel',
            userId
: 1,
            channelId
: 1
       
}
   
}]
}

For a number of code use-cases (iterating a user's roles, caching channel objects for later re-use, etc.) it is much more appropriate to return the first data structure. A user doesn't actually "have" channels, the user has roles and the roles happen to point to a channel.

So... if there's a way (or could be a way some day) to have a bi-directional BelongsToMany relationship where the mapping table can be part of the eager-loading include hierarchy, that would be ideal!

Mick Hansen

unread,
Jan 4, 2015, 1:10:06 PM1/4/15
to Chad Robinson, sequ...@googlegroups.com
User.hasMany(Role);
Channel.hasMany(Role);
Role.belongsTo(User);
Role.belongsTo(Channel);

Should really not be triggering the warning, that must be a bug.

Mick Hansen

unread,
Jan 4, 2015, 1:11:02 PM1/4/15
to Chad Robinson, sequ...@googlegroups.com
What i mean is that your before code should still work perfectly fine, and is definitely a case we want to support and i think that the warning is in error in this case (although it should only be happening when it sees it needs to use a join table).

Mick Hansen

unread,
Jan 4, 2015, 1:11:40 PM1/4/15
to Chad Robinson, sequ...@googlegroups.com
As for having all that magic be a part of BTM with a flag or similar, please open a feature request at https://github.com/sequelize/sequelize/issues
Reply all
Reply to author
Forward
0 new messages