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!
Related
I need to retrieve an object and also get the relations and nested relations.
So, I have the three models below:
User model:
module.exports = {
attributes: {
name: {
type: 'string'
},
pets: {
collection: 'pet',
via: 'owner',
}
}
Pet model:
module.exports = {
attributes: {
name: {
type: 'string'
},
owner: {
model: 'user'
},
vaccines: {
collection: 'vaccine',
via: 'pet',
}
}
Vaccine model:
module.exports = {
attributes: {
name: {
type: 'string'
},
pet: {
model: 'pet'
}
}
Calling User.findOne(name: 'everton').populate('pets').exec(....) I get the user and associated Pets. How can I also get the associated vaccines with each pet? I didn't find references about this in the official documentation.
I've ran into this issue as well, and as far as I know, nested association queries are not built into sails yet (as of this post).
You can use promises to handle the nested population for you, but this can get rather hairy if you are populating many levels.
Something like:
User.findOne(name: 'everton')
.populate('pets')
.then(function(user) {
user.pets.forEach(function (pet) {
//load pet's vaccines
});
});
This has been a widely discussed topic on sails.js and there's actually an open pull request that adds the majority of this feature. Check out https://github.com/balderdashy/waterline/pull/1052
While the answer of Kevin Le is correct it can get a little messy, because you're executing async functions inside a loop. Of course it works, but let's say you want to return the user with all pets and vaccines once it's finished - how do you do that?
There are several ways to solve this problem. One is to use the async library which offers a bunch of util functions to work with async code. The library is already included in sails and you can use it globally by default.
User.findOneByName('TestUser')
.populate('pets')
.then(function (user) {
var pets = user.pets;
// async.each() will perform a for each loop and execute
// a fallback after the last iteration is finished
async.each(pets, function (pet, cb) {
Vaccine.find({pet: pet.id})
.then(function(vaccines){
// I didn't find a way to reuse the attribute name
pet.connectedVaccines = vaccines;
cb();
})
}, function(){
// this callback will be executed once all vaccines are received
return res.json(user);
});
});
There is an alternative approach solving this issue with bluebird promises, which are also part of sails. It's probably more performant than the previous one, because it fetches all vaccines with just one database request. On the other hand it's harder to read...
User.findOneByName('TestUser')
.populate('pets')
.then(function (user) {
var pets = user.pets,
petsIds = [];
// to avoid looping over the async function
// all pet ids get collected...
pets.forEach(function(pet){
petsIds.push(pet.id);
});
// ... to get all vaccines with one db call
var vaccines = Vaccine.find({pet: petsIds})
.then(function(vaccines){
return vaccines;
});
// with bluebird this array...
return [user, vaccines];
})
//... will be passed here as soon as the vaccines are finished loading
.spread(function(user, vaccines){
// for the same output as before the vaccines get attached to
// the according pet object
user.pets.forEach(function(pet){
// as seen above the attribute name can't get used
// to store the data
pet.connectedVaccines = vaccines.filter(function(vaccine){
return vaccine.pet == pet.id;
});
});
// then the user with all nested data can get returned
return res.json(user);
});
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.
I'm starting using Node.JS, bookshelf.js and bookshelf-pagemaker.
My database contain 2 tables :
- Asset with 3 main rows (idasset, name, idarrangement)
- Arrangement_details with 2 main row (idarrangement_details, material)
I would like get all Asset where material = 2 for example.
The Asset model :
var AssetsCollection = DB.Model.extend({
tableName: 'assets',
idAttribute: 'idassets',
arrangementdetails: function () {
return this.belongsTo(ArrangementDetail, 'idarrangement_details');
}
});
I've tried this code but it crash because arrangementdetails is not joined.
var pm = require('bookshelf-pagemaker')(DB);
pm(AssetsCollection.AssetsCollection)
.forge()
.limit(req.params.limit)
.offset(req.params.page)
.query(function(qb){
qb.where('Arrangement_details.material', '=', 4)
})
.paginate({request: req, withRelated: ['arrangementdetails'])
.end({})
.then(function (results) {
callback(null, {code: 200 , res: results});
});
It's possible to do that ?
Regards,
DarKou
you need to join the tables in order to search a related one. without seeing your table structure this will be somewhat a shot in the dark, but you should use a join in the query callback along with your where similar to the following. to be clear there is no functional issue with either bookshelf or pagemaker with what you are trying to accomplish.
it appears you have 2 tables, with the following
table: assets, idAttribute: idassets
table: Arrangement_details, idAttribute: idarrangement_details
var pm = require('bookshelf-pagemaker')(DB);
pm(AssetsCollection.AssetsCollection)
.forge()
//.limit(req.params.limit) -- not needed as you are already passing req to paginate
//.offset(req.params.page) -- not needed as you are already passing req to paginate
.query(function(qb){
qb.join('Arrangement_details', 'Arrangement_details.idarrangement_details', '=', 'assets.idarrangement_details')
.where('Arrangement_details.material', '=', 4);
})
.paginate({request: req, withRelated: ['arrangementdetails'])
.end()
.then(function (results) {
callback(null, {code: 200 , res: results});
});
i would however encourage you use less confusing names for your tables/fields similar like the following where id is the idAttribute for both and details_id is the foreign key for the belongsTo relationship
var AssetsCollection = DB.Model.extend({
tableName: 'assets',
idAttribute: 'id',
arrangementDetails: function () {
return this.belongsTo(ArrangementDetail, 'details_id');
}
});
var ArrangementDetail = DB.Model.extend({
tableName: 'arrangement_details',
idAttribute: 'id'
});
var pm = require('bookshelf-pagemaker')(DB);
pm(AssetsCollection.AssetsCollection)
.forge()
.query(function(qb){
qb.join('arrangement_details', 'arrangement_details.id', '=', 'assets.details_id')
.where('arrangement_details.material', '=', 4);
})
.paginate({request: req, withRelated: ['arrangementDetails'])
.end()
.then(function (results) {
callback(null, {code: 200 , res: results});
});
I'm trying to get users count belongs to specific company.
Here is my model;
var Company = Bookshelf.Model.extend({
tableName: 'companies',
users: function () {
return this.hasMany(User.Model, "company_id");
},
users_count : function(){
return new User.Model().query(function(qb){
qb.where("company_id",9);
qb.count();
}).fetch();
},
organization: function () {
return this.belongsTo(Organization.Model, "organization_id");
}
});
method "users" works very well, no problem.
method "users_count" query works well, but cant get value to "company" model.
in routes, i'm using bookshelf models like this;
new Company.Model({id:req.params.id})
.fetch({withRelated:['users']})
.then(function(model){
res.send(model.toJSON())
})
.catch(function(error){
res.send(error);
});
How should i use users_count method, i'm kinda confused (probably because of promises)
Collection#count()
If you upgrade to 0.8.2 you can use the new Collection#count method.
Company.forge({id: req.params.id}).users().count().then(userCount =>
res.send('company has ' + userCount + ' users!');
);
Problem with your example
The problem with your users_count method is that it tries to make Bookshelf turn the result of your query into Models.
users_count : function(){
return new User.Model().query(function(qb){
qb.where("company_id",9);
qb.count();
}).fetch(); // Fetch wanted an array of `user` records.
},
This should work in this instance.
users_count : function(){
return new User.Model().query()
.where("company_id",9)
.count()
},
See relevant discussion here.
EDIT: How to get this in your attributes.
Maybe try something like this:
knex = bookshelf.knex;
var Company = bookshelf.Model.extend({
tableName: 'companies',
initialize: function() {
this.on('fetching', function(model, attributes, options) {
var userCountWrapped = knex.raw(this.getUsersCountQuery()).wrap('(', ') as user_count');
options.query.select('*', userCountWrapped);
}
}
users: function () {
return this.hasMany(User.Model, "company_id");
},
getUsersCountQuery: function() {
return User.Model.query()
.where("company_id",9)
.count();
}
organization: function () {
return this.belongsTo(Organization.Model, "organization_id");
}
});
Check out the bookshelf-eloquent extension. The withCount() function is probably what you are looking for. Your code would look something like this:
let company = await Company.where('id', req.params.id)
.withCount('users').first();
User.collection().query(function (qb) {
qb.join('courses', 'users.id', 'courses.user_id');
qb.groupBy('users.id');
qb.select("users.*");
qb.count('* as course_count');
qb.orderBy("course_count", "desc");
})
I'm trying to do something like the following:
model.updateAttributes({syncedAt: 'NOW()'});
Obviously, that doesn't work because it just gets passed as a string. I want to avoid passing a node constructed timestamp, because later I compare it to another 'ON UPDATE CURRENT_TIMESTAMP' field and the database and source could be running different times.
Is my only option to just make a database procedure and call that?
You can use Sequelize.fn to wrap it appropriately:
instance.updateAttributes({syncedAt: sequelize.fn('NOW')});
Here's a full working example:
'use strict';
var Sequelize = require('sequelize');
var sequelize = new Sequelize(/*database*/'test', /*username*/'test', /*password*/'test',
{host: 'localhost', dialect: 'postgres'});
var model = sequelize.define('model', {
syncedAt: {type: Sequelize.DATE}
});
sequelize.sync({force: true})
.then(function () {
return model.create({});
})
.then(function () {
return model.find({});
})
.then(function(instance){
return instance.updateAttributes({syncedAt: sequelize.fn('NOW')});
})
.then(function () {
process.exit(0);
})
.catch(function(err){
console.log('Caught error! ' + err);
});
That produces
UPDATE "models" SET "syncedAt"=NOW(),"updatedAt"='2015-02-09 18:05:28.989 +00:00' WHERE "id"=1
Worth mentioning (for people coming here via search) that NOW() isn't standard and doesn't work on SQL server - so don't do this if you care about portability.
sequelize.literal('CURRENT_TIMESTAMP')
may work better
you can use: sequelize.literal('CURRENT_TIMESTAMP').
Example:
await PurchaseModel.update( {purchase_date : sequelize.literal('CURRENT_TIMESTAMP') }, { where: {id: purchaseId} } );