model.fetch with related models bookshelfjs - node.js

I have below models
company.js
var Company = DB.Model.extend({
tableName: 'company',
hasTimestamps: true,
hasTimestamps: ['created_at', 'updated_at']
});
user.js
var User = DB.Model.extend({
tableName: 'user',
hasTimestamps: true,
hasTimestamps: ['created_at', 'updated_at'],
companies: function() {
return this.belongsToMany(Company);
}
});
With a many-to-many relation between Company and User which handle via the following table in the database.
user_company.js
var UserCompany = DB.Model.extend({
tableName: 'user_company',
hasTimestamps: true,
hasTimestamps: ['created_at', 'updated_at'],
users: function() {
return this.belongsToMany(User);
},
companies: function() {
return this.belongsToMany(Company);
}
});
The problem is when I run following query.
var user = new User({ id: req.params.id });
user.fetch({withRelated: ['companies']}).then(function( user ) {
console.log(user);
}).catch(function( error ) {
console.log(error);
});
It logs following error because it is looking for company_user table instead of user_company.
{ [Error: select `company`.*, `company_user`.`user_id` as `_pivot_user_id`, `company_user`.`company_id` as `_pivot_company_id` from `company` inner join `company_user` on `company_user`.`company_id` = `company`.`id` where `company_user`.`user_id` in (2) - ER_NO_SUCH_TABLE: Table 'navardeboon.company_user' doesn't exist]
code: 'ER_NO_SUCH_TABLE',
errno: 1146,
sqlState: '42S02',
index: 0 }
Is there any way to tell it to look for a certain table while fetching relations?

With Bookshelf.js it is VERY important, how the tables and ids are named in your database. Bookshelf.js does some interesting things with foreign keys (i.e. converts it to singular and appends _id).
When using Bookshelfjs's many-to-many feature, you don't need UserCompany model. However, you need to following the naming conventions of the tables and ids for this to work.
Here's an example of many-to-many models. Firstly, the database:
exports.up = function(knex, Promise) {
return knex.schema.createTable('books', function(table) {
table.increments('id').primary();
table.string('name');
}).createTable('authors', function(table) {
table.increments('id').primary();
table.string('name');
}).createTable('authors_books', function(table) {
table.integer('author_id').references('authors.id');
table.integer('book_id').references('books.id');
});
};
Please note how the junction table is named: alphabetically ordered (authors_books). If you'd write books_authors, the many-to-many features wouldn't work out of the box (you'd have to specify the table name explicitly in the model). Also note the foreign keys (singular of authors with _id appended, i.e. author_id).
Now let's look at the models.
var Book = bookshelf.Model.extend({
tableName: 'books',
authors: function() {
return this.belongsToMany(Author);
}
});
var Author = bookshelf.Model.extend({
tableName: 'authors',
books: function() {
return this.belongsToMany(Book);
}
});
Now that our database has the correct naming of the tables and ids, we can just use belongsToMany and this works! There is no need for a AuthorBook model, Bookshelf.js does this for you!
Here's the advanced description: http://bookshelfjs.org/#Model-instance-belongsToMany

Actually I found a very simple solution for it. You just need to mention table name like this:
var User = DB.Model.extend({
tableName: 'user',
hasTimestamps: true,
hasTimestamps: ['created_at', 'updated_at'],
companies: function() {
return this.belongsToMany(Company, **'user_company'**);
}
})
and as #uglycode said, no need to have UserCompany model anymore.

Related

Sequelize insert additional fields into junction table

as described in sequelize documentation here,
Foo.belongsToMany(Bar, { through: Baz })
then there is a method to insert into junction table:
fooInstance.addBars([5,4])
It will insert into Baz junction table two fields: FooId,BarId.
But i need to insert another field value too. sth like this:
fooInstance.addBars([{BarId: 5, otherField:'xxx'}, ...]
How i can achieve that without manual insert?
See Advanced Many-to-Many guide.
const User_Profile = sequelize.define('User_Profile', {
selfGranted: DataTypes.BOOLEAN
}, { timestamps: false });
User.belongsToMany(Profile, { through: User_Profile });
Profile.belongsToMany(User, { through: User_Profile });
With this, we can now track an extra information at the through table, namely the selfGranted boolean. For example, when calling the user.addProfile() we can pass values for the extra columns using the through option.
Example:
const amidala = await User.create({ username: 'p4dm3', points: 1000 });
const queen = await Profile.create({ name: 'Queen' });
await amidala.addProfile(queen, { through: { selfGranted: false } });

How do I reference an association when creating a row in sequelize without assuming the foreign key column name?

I have the following code:
#!/usr/bin/env node
'use strict';
var Sequelize = require('sequelize');
var sequelize = new Sequelize('sqlite:file.sqlite');
var User = sequelize.define('User', { email: Sequelize.STRING});
var Thing = sequelize.define('Thing', { name: Sequelize.STRING});
Thing.belongsTo(User);
sequelize.sync({force: true}).then(function () {
return User.create({email: 'asdf#example.org'});
}).then(function (user) {
return Thing.create({
name: 'A thing',
User: user
}, {
include: [User]
});
}).then(function (thing) {
return Thing.findOne({where: {id: thing.id}, include: [User]});
}).then(function (thing) {
console.log(JSON.stringify(thing));
});
I get the following output:
ohnobinki#gibby ~/public_html/turbocase1 $ ./sqltest.js
Executing (default): INSERT INTO `Users` (`id`,`email`,`updatedAt`,`createdAt`) VALUES (NULL,'asdf#example.org','2015-12-03 06:11:36.904 +00:00','2015-12-03 06:11:36.904 +00:00');
Executing (default): INSERT INTO `Users` (`id`,`email`,`createdAt`,`updatedAt`) VALUES (1,'asdf#example.org','2015-12-03 06:11:36.904 +00:00','2015-12-03 06:11:37.022 +00:00');
Unhandled rejection SequelizeUniqueConstraintError: Validation error
at Query.formatError (/home/ohnobinki/public_html/turbocase1/node_modules/sequelize/lib/dialects/sqlite/query.js:231:14)
at Statement.<anonymous> (/home/ohnobinki/public_html/turbocase1/node_modules/sequelize/lib/dialects/sqlite/query.js:47:29)
at Statement.replacement (/home/ohnobinki/public_html/turbocase1/node_modules/sqlite3/lib/trace.js:20:31)
It seems that specifying {include: [User]} instructs Sequelize to create a new User instance matching the contents of user. That is not my goal. In fact, I find it hard to believe that such behaviour would ever be useful—I at least have no use for it. I want to be able to have a long-living User record in the database and at arbitrary times create new Things which refer to the User. In my shown example, I wait for the User to be created, but in actual code it would likely have been freshly loaded through User.findOne().
I have seen other questions and answers say that I have to explicitly specify the implicitly-created UserId column in my Thing.create() call. When Sequelize provides an API like Thing.belongsTo(User), I shouldn’t have to be aware of the fact that a Thing.UserId field is created. So what is the clean API-respecting way of creating a new Thing which refers to a particular User without having to guess the name of the UserId field? When I load a Thing and specify {include: [User]}, I access the loaded user through the thing.User property. I don’t think I’m supposed to know about or try to access a thing.UserId field. In my Thing.belongsTo(User) call, I never specify UserId, I just treat that like an implementation detail I shouldn’t care about. How can I continue to avoid caring about that implementation detail when creating a Thing?
The Thing.create() call that works but looks wrong to me:
Thing.create({
name: 'A thing',
UserId: user.id
});
Option 1 - risks DB inconsistency
Sequelize dynamically generates methods for setting associations on instances, e.g. thing.setUser(user);. In your use case:
sequelize.sync({force: true})
.then(function () {
return Promise.all([
User.create({email: 'asdf#example.org'}),
Thing.create({name: 'A thing'})
]);
})
.spread(function(user, thing) {
return thing.setUser(user);
})
.then(function(thing) {
console.log(JSON.stringify(thing));
});
Option 2 - does not work/buggy
It isn't documented, but from a code dive I think the following should work. It doesn't but that seems to be because of a couple of bugs:
// ...
.then(function () {
return models.User.create({email: 'asdf#example.org'});
})
.then(function(user) {
// Fails with SequelizeUniqueConstraintError - the User instance inherits isNewRecord from the Thing instance, but it has already been saved
return models.Thing.create({
name: 'thingthing',
User: user
}, {
include: [{
model: models.User
}],
fields: ['name'] // seems nec to specify all non-included fields because of line 277 in instance.js - another bug?
});
})
Replacing models.User.create with models.User.build doesn't work because the built but not saved instance's primary key is null. Instance#_setInclude ignores the instance if its primary key is null.
Option 3
Wrapping the Thing's create in a transaction prevents an inconsistent state.
sq.sync({ force: true })
.then(models.User.create.bind(models.User, { email: 'asdf#example.org' }))
.then(function(user) {
return sq.transaction(function(tr) {
return models.Thing.create({name: 'A thing'})
.then(function(thing) { return thing.setUser(user); });
});
})
.then(print_result.bind(null, 'Thing with User...'))
.catch(swallow_rejected_promise.bind(null, 'main promise chain'))
.finally(function() {
return sq.close();
});
I have uploaded a script demo'ing option 2 and option 3 here
Tested on sequelize#6.5.1 sqlite3#5.0.2 I can use User.associations.Comments.foreignKey as in:
const Comment = sequelize.define('Comment', {
body: { type: DataTypes.STRING },
});
const User = sequelize.define('User', {
name: { type: DataTypes.STRING },
});
User.hasMany(Comment)
Comment.belongsTo(User)
console.dir(User);
await sequelize.sync({force: true});
const u0 = await User.create({name: 'u0'})
const u1 = await User.create({name: 'u1'})
await Comment.create({body: 'u0c0', [User.associations.Comments.foreignKey]: u0.id});
The association is also returned during creation, so you could also:
const Comments = User.hasMany(Comment)
await Comment.create({body: 'u0c0', [Comments.foreignKey]: u0.id});
and on many-to-many through tables you get foreignKey and otherKey for the second foreign key.
User.associations.Comments.foreignKey contains the foreignKey UserId.
Or analogously with aliases:
User.hasMany(Post, {as: 'authoredPosts', foreignKey: 'authorId'});
Post.belongsTo(User, {as: 'author', foreignKey: 'authorId'});
User.hasMany(Post, {as: 'reviewedPosts', foreignKey: 'reviewerId'});
Post.belongsTo(User, {as: 'reviewer', foreignKey: 'reviewerId'});
await sequelize.sync({force: true});
// Create data.
const users = await User.bulkCreate([
{name: 'user0'},
{name: 'user1'},
])
const posts = await Post.bulkCreate([
{body: 'body00', authorId: users[0].id, reviewerId: users[0].id},
{body: 'body01', [User.associations.authoredPosts.foreignKey]: users[0].id,
[User.associations.reviewedPosts.foreignKey]: users[1].id},
])
But that syntax is so long that I'm tempted to just hardcode the keys everywhere.

Set timestamps on a pivot model with Bookshelf.js

I've got two Bookshelf models in a many-to-many relationship and I'd like to have timestamps updated when I'm attaching or detaching some relations.
Here's my models:
var Video = Bookshelf.Model.extend({
tableName: 'video',
program: function(){
return this.belongsToMany(Bookshelf.model('Program'), 'programvideo', 'videoId', 'programId');
}
});
var Program = Bookshelf.Model.extend({
tableName: 'program',
videos: function(){
return this.belongsToMany(Bookshelf.model('Video'), 'programvideo', 'programId', 'videoId');
}
});
Everything works fine when I'm using
prgm.videos().attach(videos);
But is there any way to add timestamps to this relation? Do I need to define a pivot model in Bookshelf?
Thanks
Well, you could easily make a pivot model, create in migrations timestamps and enable timestamps in the model, and everything would work seamless!
However, if you'd like to solve this without additional model, you have to define firstly withPivot in models, e.g.:
var Stations = bookshelf.Model.extend({
tableName: 'stations',
stationsRoutes: function() {
return this.belongsToMany(Routes, 'stations_routes').withPivot('time');
}
});
var Routes = bookshelf.Model.extend({
tableName: 'routes',
stationsRoutes: function() {
return this.belongsToMany(Stations, 'stations_routes').withPivot('time');
}
});
Then, each time when you attach data, you have to call updatePivot, e.g.:
router.get('/updatePivot/:id', function(req, res) {
new Routes({
'id': req.params.id
}).fetch({
withRelated: ['stationsRoutes']
}).then(function(result) {
result.stationsRoutes().attach({
station_id: 3
}).then(function() {
result.stationsRoutes().updatePivot({
'time': '09:09'
}/*, {
query: function(qb) {
qb.where({
'id': 8
});
}
}*/).then(function() {
result.load('stationsRoutes').then(function(result_reloaded){
res.json(result_reloaded);
});
});
});
});
});
I've commented the piece of code where you can filter a specific row in the junction table that gets updated (if left out, all corresponding rows get updated).
Hope this helps!

Sequelize not creating Junction tables for many-to-many relationships

I don't see the junction tables getting created for my code below (for UserEvents as:Attendees). When I try to add 'Attendee' to an Event using event.setAttendees([user]), it sets the EventId on the user table (thus making it a one-to-many relationship)
During sync logging, no tables with 'userevents' related name gets created
var User = db.import(__dirname + '/user');
var Event = db.import(__dirname + '/event');
Event.hasMany(User, { as: 'Attendees'});
User.hasMany(Event);
db.sync({logging: console.log}).success(function() {
logger.info('DB Initialization successful!');
createDefaultData()
}).error(function(err) {
logger.error('DB Initialization failed!', err);
});
var createDefaultData = function() {
User.create({email:"test#test.com", firstName: "tolga", lastName: "ekmen", password: "qwer", location: "13424"});
}
When using an alias ({ as: 'Attendees'}) you have to tell sequelize that you want a join table, by specifying through, which can either be a string or a model:
Event.hasMany(User, { as: 'Attendees', through: 'user_events' });
User.hasMany(Event, { , through: 'user_events' });

Try to create HABTM connection Sequelize.js

I'm trying to create a HABTM relationship with Sequelize but I can't get it done.... I still receive an error message:
return (tableName1.toLowerCase() < tableName2.toLowerCase()) ? (tableName1
^
TypeError: Cannot call method 'toLowerCase' of undefined
I have a User model, a Book model and an UserBooks model. And ofcourse my database contains a "users" table, "user_books" table and "books" table.
UserBooks model:
module.exports = function(schema, DataTypes) {
var UserBooks = schema.define('UserBooks', {
}, {
tableName: 'user_books', // this will define the table's name
timestamps: false // this will deactivate the timestamp columns
});
UserBooks.sync();
return UserBooks;
};
User model:
module.exports = function(schema, DataTypes) {
var User = schema.define('User', {
keywords: DataTypes.STRING
}, {
tableName: 'users', // this will define the table's name
timestamps: false ,// this will deactivate the timestamp columns
syncOnAssociation:false
});
User.hasMany(Book, { foreignKey: 'user_id', through: UserBooks });
User.sync();
return User;
};
Book model:
module.exports = function(schema, DataTypes) {
var Book = schema.define('Book', {
keywords: DataTypes.STRING
}, {
tableName: 'books', // this will define the table's name
timestamps: false ,// this will deactivate the timestamp columns
syncOnAssociation:false
});
Book.hasMany(User, { foreignKey: 'book_id', through: UserBooks });
Book.sync();
return Book;
};
In your User model you are trying to create an association with a model that is not defined in that scope. In User.js, you only have access to User, not Book or UserBooks which are undefined. Thats whats causing your error.
You can either create associations in the place where you import all your models into your app, or in the models file by importing the models you want to associate with (bevare of circular imports). Your user model could be changed to:
module.exports = function(schema, DataTypes) {
var Book = schema.import(__dirname + '/book');
var UserBooks = schema.import(__dirname + '/userbooks');
var User = schema.define('User', {
keywords: DataTypes.STRING
}, {
tableName: 'users', // this will define the table's name
timestamps: false ,// this will deactivate the timestamp columns
syncOnAssociation:false
});
User.hasMany(Book, { foreignKey: 'user_id', through: UserBooks });
Book.hasMany(User, { foreignKey: 'book_id', through: UserBooks });
return User;
};
For another example of how to do it, see http://sequelizejs.com/articles/express#minimal-express-app
Also, I've removed the call to User.sync from your code. Sync is an async call, while import is sync. This means that your are defining your model, starting to sync it to the DB, and then returning it, before you know that it has finished syncing. This means you could potentially be trying to work create instances with it before the table has been created. Instead, you should use sequelize.sync to sync all your models at once, and attach a callback to wait for the sync to finish (see the link I posted for a code example)

Resources