question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

Singularize `as` in foreign keys in To-Many associations (amendment of PR #5957)

See original GitHub issue

PR #5957 recently changed the names of foreign keys in associations where an as alias is specified.

So now: Project.hasMany(User, {as: 'Workers'}) creates foreign key WorkersId rather than UserId.

This is great, but I do think the as should be singularized, so foreign key is WorkerId. This was already raised by @mickhansen in #5957, but I was too late to the party to get a comment in.

WorkersId (plural), in my view, is not what the user would expect. It’s inconsistent with the naming when no as alias is provided, and with the general Sequelize convention of singularizing/pluralizing. I can imagine it tripping people up quite often because it’s counter-intuitive.

Yes, using inflection to singularize as could in some few cases produce the wrong result, but in those cases the user can override it by specifying foreignKey. Personally, I think that’s preferable to having to specify foreignKey in all cases in order to maintain consistent naming.

I’d be happy to make a PR for this, if there’s agreement that this would be a good thing to do.

What do you think @mickhansen @janmeier @sushantdhiman?

Issue Analytics

  • State:closed
  • Created 7 years ago
  • Reactions:1
  • Comments:51 (51 by maintainers)

github_iconTop GitHub Comments

2reactions
overlookmotelcommented, Jun 7, 2016

OK, just to rewind a bit, here’s my summary of discussion so far:

  1. I think everyone agrees that singularizing as would be ideal if there was a foolproof way to do it.
  2. The concern is that inflection does not singularize with 100% accuracy.
  3. Consistency needs to be maintained so that behavior doesn’t change within a major release of Sequelize (i.e v4.9.1 behaves same as v4.0.0).

Now my personal view:

I think that inflection is accurate enough. It’s come a long way in past couple of years. It has 600,000 downloads a month, no open issues, and the maintainer is very quick to resolve issues that do arise. They now use Wiktionary as a reference, so questions of whether behavior is correct or not can be easily resolved. I think it’s safe to assume that its now working correctly for all but the most obscure words.

Personally (and you may think I’m mad) I don’t want to define foreignKey for every association. This is something I want (and expect) my ORM to do for me.

To deal with the need for consistency I propose 3 things:

  1. Update inflection to latest before release of Sequelize v4 and pin it so it does not change until v5.
  2. Ensure that Sequelize codebase only references Utils.inflection rather than referring to inflection directly. Then, if a user finds a bug in inflection that affects their app and they want to update it to a later version, they can use Sequelize.Utils.inflection = require('inflection') to inject a later version.
  3. The above restriction can be enforced in the test shims to throw an error if inflection is ever accessed directly.

The above would I think be the best route to do the “right” thing in 99% of cases and offer an easy, consistent way for users to deal with the 1%.

1reaction
overlookmotelcommented, Jun 12, 2016

My god this has opened a whole can of worms!

I’ve looked into this further and here’s what I’ve found. (sorry that this is really long)

One-to-many

The foreign key, if it’s going to be based on as, needs to be based on the other as. i.e. the as defined in the matching belongsTo statement.

State.hasMany( City, { as : 'Capital' } );
City.belongsTo( State, { as: 'CapitalOf' } );

So instead of StateId the foreign key becomes CapitalOfId. This makes more sense!

That’s actually what City.belongsTo( State, { as: 'CapitalOf' } ) already does in Sequelize v3.

However, if you define both hasMany and belongsTo as in the example above, Sequelize v3 doesn’t match the two statements as being two sides of the same association and you end up with two fields on City: StateId and CapitalOfId.

The only way to link them together is to define the same foreignKey on both.

State.hasMany( City, { as: 'Capital', foreignKey: 'capitalOfId' } );
City.belongsTo( State, { as: 'CapitalOf', foreignKey: 'capitalOfId' } );

Personally I don’t like this - I feel that the purpose of an ORM is to abstract away the database (including the column names) so I don’t feel that it’s good to be forced to used foreignKey to link hasMany and belongsTo statements.

But there has to be some element in common between the 2 statements, otherwise Sequelize can’t possibly know which statements to link together with which. This is one reason why I think defining a two-way association with a single statement would be simpler (see #6091).

But anyway… I think:

  • Previous behavior concerning as for one-to-many was better than what we have on latest master
  • Best thing is to roll back @sushantdhiman’s change for hasMany

Many-to-many

Sequelize v3 behavior

// people are members of many groups, groups have many members
Person.belongsToMany( Group, { through: 'PersonGroup' } );
Group.belongsToMany( Person, { through: 'PersonGroup' } );
// creates model `PersonGroup` with fields `PersonId` and `GroupId`

// with `as`
Person.belongsToMany( Group, { as: 'MyGroups', through: 'PersonGroup' } );
Group.belongsToMany( Person, { as: 'Members', through: 'PersonGroup' } );
// creates model `PersonGroup` with fields `PersonId` and `GroupId`

i.e. the as is not used at all for creating the keys. But it all works.

If you define foreignKey, the behaviour is (to my eyes) a bit wonky:

Person.belongsToMany( Group, { through: 'PersonGroup', foreignKey: 'GroupId' } );
Group.belongsToMany( Person, { through: 'PersonGroup', foreignKey: 'MemberId' } );

var bob = yield Person.create( { id: 1, name: 'Bob' } );
var frank = yield Person.create( { id: 1, name: 'Frank' } );
var fighters = yield Group.create( { id: 1, name: 'Fighters' } );
var lovers = yield Group.create( { id: 2, name: 'Lovers' } );

yield bob.addGroup( lovers ); // Bob is a lover not a fighter
// creates row in PersonGroup with Bob's ID as `GroupId` - should be `MemberId`

To make it work as expected, you have to switch the two foreignKey declarations:

Person.belongsToMany( Group, { through: 'PersonGroup', foreignKey: 'MemberId' } );
Group.belongsToMany( Person, { through: 'PersonGroup', foreignKey: 'GroupId' } );

This seems to me counter-intuitive.

Current master

Current master uses as and creates MembersId and MyGroupsId. This has two problems:

  1. Pluralized keys
  2. The two keys are the wrong way round - MyGroupsId replaces PersonId not GroupId. So if you create a relationship between a Person “Bob” and a Group, you end up with Bob’s ID saved as MyGroupsId where it should be in MembersId

There’s also an unrelated bug. If you don’t create a Person record for Frank, something even weirder happens - no rows get inserted into PersonGroup at all and no error is thrown. I’m guessing a foreign key error from the database is getting swallowed somewhere.

Solution

We could either:

  1. revert to Sequelize v3 behavior, or
  2. ignore as but switch the foreignKeys round, or
  3. use the as for both field names, so the fields become MemberId and MyGroupId (but the right way around)

Concerning whether to use as, I’m not bothered either way. For one-to-one or one-to-many relationships, using as as the base for the foreign key is necessary because you can have multiple relationships between the same two entities.

Person.belongsTo( Group, { as: 'Chief' } );
Person.belongsTo( Group, { as: 'Deputy' } );
// creates keys `ChiefId` and `DeputyId` - obviously two fields called `PersonId` wouldn't work!

But with many-to-many relationships, each association creates a new through table, so there’s no possibility of two foreign keys being called the same thing in the same table.

So I kind of see using as as a solution to a problem that doesn’t really exist.

However:

  • Either way, the behavior in current master is clearly wrong
  • In my opinion Sequelize v3 behavior is weird too

Many-to-many self-association

In a many-to-many _self-_association, providing as and through is mandatory.

Person.belongsToMany( Person, { as: 'Parents', through: 'PersonParent' } );
Person.belongsToMany( Person, { as: 'Children', through: 'PersonParent' } );

Sequelize v3

In Sequelize v3, the behavior is broken:

  • The first belongsToMany creates PersonParent model with fields PersonId and ParentId
  • The 2nd belongsToMany deletes the ParentId field! So you only end up with a single PersonId field in the join table.

To get it to work, you have to also specify foreignKey in both directions. But, again, the foreign keys have to be what seems to me to be the wrong way around:

This works:

Person.belongsToMany( Person, { as: 'Parents', through: 'PersonParent', foreignKey: 'ChildId' } );
Person.belongsToMany( Person, { as: 'Children', through: 'PersonParent', foreignKey: 'ParentId' } );

var bob = yield Person.create( { id: 1, name: 'Bob' } );
var frank = yield Person.create( { id: 2, name: 'Frank' } );
yield bob.addParent(frank); // Frank is Bob's dad
// creates row in PersonParent with Bob's ID as `ChildId` and Frank's ID as `ParentId`

Current master

Person.belongsToMany( Person, { as: 'Parents', through: 'PersonParent' } );
Person.belongsToMany( Person, { as: 'Children', through: 'PersonParent' } );

Foreign keys now don’t need to be provided. ParentsId and ChildrenId fields are created from the as - good stuff. BUT the fields are the wrong way around.

var bob = yield Person.create( { id: 1, name: 'Bob' } );
var frank = yield Person.create( { id: 2, name: 'Frank' } );
yield bob.addParent(frank); // Frank is Bob's dad
// creates row in PersonParent with Bob's ID as `ParentsId` and Frank's ID as `ChildrenId` - should be reversed

So I think for many-to-many self-associations, both Sequelize v3 and current master have it wrong.

Conclusions

All of the above is why I say that defining associations in Sequelize is confusing!

But one thing is for sure - the behavior on current master is wrong.

I see 3 possible ways forward:

  1. Revert @sushantdhiman’s PR and return to Sequelize v3 behavior. Personally I think it’s still wacky for many-to-many associations, but at least this doesn’t break BC.
  2. Try to fix Sequelize v3 behavior for many-to-many (and possibly use as as base for foreign key names). This would be a breaking change and I think will be confusing to old users who’ve got used to it, and will find changing over a head-scratcher.
  3. Totally overhaul associations, as suggested in #6091. A big breaking change, but at least there’d be new syntax so it’d be easier to get your head around this being “a new way”, rather than old code that looks right but suddenly doesn’t do what it used to.

Thoughts?

Read more comments on GitHub >

github_iconTop Results From Across the Web

Specifying the foreign keys in a belongsToMany-through ...
I want to set the used foreign keys when using a belongsToMany-through association in CakePHP 3.0. Consider the following two tables:.
Read more >
CDS Associations | SAP Help Portal
Associations define relationships between entities. ... The foreign key is not unique, so address6 is a “to-many” association. You can use foreign keys...
Read more >
Full text of "The Moving picture world" - Internet Archive
manaKer of the foreign department of that organization. ... He was well known to many exhibitors in Northern New York and the northern...
Read more >
Global Perspectives On ADHD Social Dimensions of Diagnosis and ...
A growing body of evidence indicates that attention deficit–hyperactivity dis-. order (ADHD) is being diagnosed and treated in an increasing number of countries ......
Read more >
A Guide to Active Record Associations - Ruby on Rails Guides
Rails offers two different ways to declare a many-to-many relationship between models. ... For belongs_to associations you need to create foreign keys, ...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found