Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Self-Referencing Many-to-many Relation for followers/followed relationship #1276

Closed
ezmiller opened this issue Jun 5, 2016 · 4 comments
Closed

Comments

@ezmiller
Copy link

ezmiller commented Jun 5, 2016

Hi All, I'm trying to work out the most expressively clear and "correct" way of setting up a many-to-many relationship for a User model to build-out the standard followers/followed relationship that exists on many social applications.

I can see two possible approaches: 1) Using belongsToMany() alone, or 2) Using belongsToMany() with a throught(). Neither have worked for me, but let me show you first what I'm doing for (1). I start with this Model setup:

const User = bookshelf.Model.extend({
  tableName: 'users',
  initialize: function() {
    this.on('creating', this.encryptPassword);
  },
  hasTimestamps: true,
  posts: function() {
    return this.hasMany(Posts, 'author');
  },
  comments: function() {
    return this.hasMany(Comments);
  },
  following: function() {
    return this.belongsToMany(UserFollow, 'users_users', 'follower_id', 'user_id');
  },
  followers: function() {
    return this.belongsToMany(UserFollow, 'users_users', 'user_id', 'follower_id');
  },
});

And to match that I have a knex migration that adds a users_users table like so:

exports.up = function(knex, Promise) {
  return knex.schema.createTable('users_users', (tbl) => {
    tbl.integer('user_id').references('users.id');
    tbl.integer('follower_id').references('users.id');
    tbl.unique(['user_id', 'follower_id']);  
      // This b/c I read a few places that Bookshelf might require a composite key.
  });
};

exports.down = function(knex, Promise) {
  return knex.schema.dropTable('users_users');
};

Then I have the following test:

it('User model can record a follower', (done) => {
    const saveUsr = (data) => {
      return User.forge().save(data, {transacting: transaction});
    };
    Promise.all([saveUsr(mockUser), saveUsr(anotherMockUser)])
      .then((results) => {
        let usr1 = results[0];
        let usr2 = results[1];
        return usr2.followers().attach(usr1.id, {transacting: transaction});
      })
      .then((usrWithFollower) => {
        return usrWithFollower.fetch({
          withRelated: ['followers'],
          transacting: transaction
        });
      })
      .then((usr) => {
        console.log(JSON.stringify(usr));
        console.log('followers: ', usr.get('followers'));
        done();
      })
      .catch((err) => { done(err); });
   });

I'm not really testing much here as I've not been able to get the thing, working, but the result of JSON.stringify(user) in the last promise then callback is the following:

[
   {
      "id":1,
      "name":"Sally Low",
      "username":"sally",
      "email":"sally@example.org",
      "created_at":"2016-06-05T15:24:41.835Z",
      "updated_at":"2016-06-05T15:24:41.835Z",
      "password":"$2a$10$613tL/baML9obsRN4Yg7MeU/2RiH7oK/nFUwfpQ1GYuA15VUFrFZu",
      "followers": [],
      "_pivot_user_id":2,
      "_pivot_follower_id":1
   }
]

As you can see, it looks as if some sort of relation has been setup here given that there are these _pivot_ props, but what they are is not clear to me, and I'm not sure why the followers field is coming back empty.

If anyone can set me straight here or illuminate the situation in any way, I'd be much obliged.

@vellotis
Copy link
Contributor

vellotis commented Jun 6, 2016

@ezmiller I guess it is a mixup of your otherKey and foreignKey identifiers.
model.belongsToMany(Target, [table], [foreignKey], [otherKey])
I may be wrong, but try this way

const User = bookshelf.Model.extend({
  tableName: 'users',
  initialize: function() {
    this.on('creating', this.encryptPassword);
  },
  hasTimestamps: true,
  posts: function() {
    return this.hasMany(Posts, 'author');
  },
  comments: function() {
    return this.hasMany(Comments);
  },
  following: function() {
    return this.belongsToMany(UserFollow, 'users_users', 'user_id', 'follower_id');
  },
  followers: function() {
    return this.belongsToMany(UserFollow, 'users_users', 'follower_id', 'user_id');
  },
});

@vellotis
Copy link
Contributor

vellotis commented Jun 7, 2016

Initally a reply to deleted comment by @ahmedcanuck

How about a polymorphic relationship? That's what we did and it worked out really well for us.

@ahmedcanuck I prefer to think that for the current case polymorphic relationship is redundant. @ezmiller's solution is elegant enough.

@ahmedcanuck
Copy link

@ezmiller I realized that after posting and hence why I deleted my message. It was relevant in our case since we were trying to allow the following of more than just users.

But in your case this doesn't make sense.
What I'd personally do in your case is do it the way @vellotis suggested :)

vellotis added a commit to vellotis/bookshelf that referenced this issue Jun 14, 2016
Current specification of belongsToMany has a bug. Its `otherKey` and `foreignKey` parameter description are mistakenly partially swapped.

Many issues have come up because of this problem. eg. bookshelf#397, bookshelf#1031, bookshelf#1276
@vellotis
Copy link
Contributor

Closing as resolved by #1288

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants