From 38eb67ad2ec46b58dbc701073ce0e980fad7788b Mon Sep 17 00:00:00 2001 From: Ciro Santilli Date: Sun, 20 Jun 2021 15:32:45 +0100 Subject: [PATCH] managed --- .../lkmc/nodejs/sequelize/association.js | 19 +-- .../association_many_to_many_double.js | 16 +++ .../association_many_to_many_same_model.js | 50 +++++++- ...sociation_many_to_many_same_model_super.js | 114 ++++++++++++++++++ .../sequelize/association_nested_include.js | 93 +++++++++----- .../association_nested_include_super.js | 112 +++++++++++++++++ .../nodejs/sequelize/ignore_duplicates.js | 47 ++++++++ rootfs_overlay/lkmc/nodejs/sequelize/index.js | 2 +- 8 files changed, 410 insertions(+), 43 deletions(-) create mode 100755 rootfs_overlay/lkmc/nodejs/sequelize/association_many_to_many_same_model_super.js create mode 100755 rootfs_overlay/lkmc/nodejs/sequelize/association_nested_include_super.js create mode 100755 rootfs_overlay/lkmc/nodejs/sequelize/ignore_duplicates.js diff --git a/rootfs_overlay/lkmc/nodejs/sequelize/association.js b/rootfs_overlay/lkmc/nodejs/sequelize/association.js index 85c04aa..88d037a 100755 --- a/rootfs_overlay/lkmc/nodejs/sequelize/association.js +++ b/rootfs_overlay/lkmc/nodejs/sequelize/association.js @@ -13,12 +13,13 @@ const sequelize = new Sequelize({ (async () => { 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'}) @@ -62,7 +63,6 @@ await Comment.create({body: 'u1c0', UserId: u1.id}); // Nicer higher level way. { - console.log(Object.getOwnPropertyNames(u0)); const u0Comments = await u0.getComments({ include: [{ model: User }], order: [['id', 'ASC']], @@ -73,15 +73,16 @@ await Comment.create({body: 'u1c0', UserId: u1.id}); assert(u0Comments[1].User.name === 'u0'); } -// No way to create new item with association without explicit foreign key?? +// If you REALLY wanted to not repeat the UserId magic constant everywhere, you could use User.associations.Comments.foreignKey +// But it is such a mouthful, that nobody likely ever uses it? // https://stackoverflow.com/questions/34059081/how-do-i-reference-an-association-when-creating-a-row-in-sequelize-without-assum -// This does not work as we would like: { - await Comment.create({body: 'u0c2', User: u0}); - // We'd want 3 here. + await Comment.create({body: 'u0c2', [User.associations.Comments.foreignKey]: u0.id}); + // Syntax that we really would like instead. + //await Comment.create({body: 'u0c2', User: u0}); assert((await Comment.findAll({ - where: { UserId: u0.id }, - })).length === 2); + where: { [User.associations.Comments.foreignKey]: u0.id }, + })).length === 3); } // Removal auto-cascades. diff --git a/rootfs_overlay/lkmc/nodejs/sequelize/association_many_to_many_double.js b/rootfs_overlay/lkmc/nodejs/sequelize/association_many_to_many_double.js index 0a8252f..9c856de 100755 --- a/rootfs_overlay/lkmc/nodejs/sequelize/association_many_to_many_double.js +++ b/rootfs_overlay/lkmc/nodejs/sequelize/association_many_to_many_double.js @@ -108,6 +108,7 @@ const post2Followers = await post2.getFollowers({order: [['name', 'ASC']]}) assert(post2Followers.length === 0); // Same as getLikedPosts but with the user ID instead of the model object. +// as is mandatory to disambiguate which one we want to get. { const user0Likes = await Post.findAll({ include: [{ @@ -122,6 +123,21 @@ assert(post2Followers.length === 0); assert(user0Likes.length === 2); } +// Alternatively, we can also pass the association object instead of model + as. +// This is actually nicer! +{ + const user0Likes = await Post.findAll({ + include: [{ + association: Post.associations.likers, + where: {id: user0.id}, + }], + order: [['body', 'ASC']], + }) + assert(user0Likes[0].body === 'post0'); + assert(user0Likes[1].body === 'post1'); + assert(user0Likes.length === 2); +} + // Yet another way that can be more useful in nested includes. { const user0Likes = (await User.findOne({ diff --git a/rootfs_overlay/lkmc/nodejs/sequelize/association_many_to_many_same_model.js b/rootfs_overlay/lkmc/nodejs/sequelize/association_many_to_many_same_model.js index 2417563..1a185aa 100755 --- a/rootfs_overlay/lkmc/nodejs/sequelize/association_many_to_many_same_model.js +++ b/rootfs_overlay/lkmc/nodejs/sequelize/association_many_to_many_same_model.js @@ -10,6 +10,9 @@ const { Sequelize, DataTypes } = require('sequelize'); const sequelize = new Sequelize({ dialect: 'sqlite', storage: 'tmp.' + path.basename(__filename) + '.sqlite', + define: { + timestamps: false + }, }); (async () => { @@ -52,16 +55,61 @@ assert(user3Follows[0].name === 'user0'); assert(user3Follows.length === 1); // Same but with ID instead of object. +// Also get rid of all useless fields from the trough table. { const user0Follows = (await User.findOne({ where: {id: user0.id}, - include: [{model: User, as: 'Follows'}], + attributes: [], + include: [{ + model: User, + as: 'Follows', + through: {attributes: []}, + }], })).Follows assert(user0Follows[0].name === 'user1'); assert(user0Follows[1].name === 'user2'); assert(user0Follows.length === 2); } +//// Yet another method with the many-to-many reversed. +//// TODO close to working, but on is being ignored... +//{ +// const user0Follows = await User.findAll({ +// include: [{ +// model: User, +// as: 'Follows', +// on: { +// '$User.UserFollowUser.FollowIdasdf$': { [Sequelize.Op.col]: 'User.user_id' }, +// '$User.UserFollowUser.UserId$': user0.id, +// }, +// attributes: [], +// through: {attributes: []}, +// }], +// order: [['name', 'ASC']], +// }) +// // TODO +// //assert(user0Follows[0].name === 'user1'); +// //assert(user0Follows[1].name === 'user2'); +// //assert(user0Follows.length === 2); +//} + +// Find users that follow user0 +{ + const followsUser0 = await User.findAll({ + include: [{ + model: User, + as: 'Follows', + where: {id: user0.id}, + attributes: [], + through: {attributes: []} + }], + order: [['name', 'ASC']], + }) + assert(followsUser0[0].name === 'user2'); + assert(followsUser0[1].name === 'user3'); + assert(followsUser0.length === 2); +} + // has methods assert(!await user0.hasFollow(user0)) assert(!await user0.hasFollow(user0.id)) diff --git a/rootfs_overlay/lkmc/nodejs/sequelize/association_many_to_many_same_model_super.js b/rootfs_overlay/lkmc/nodejs/sequelize/association_many_to_many_same_model_super.js new file mode 100755 index 0000000..0ea1671 --- /dev/null +++ b/rootfs_overlay/lkmc/nodejs/sequelize/association_many_to_many_same_model_super.js @@ -0,0 +1,114 @@ +#!/usr/bin/env node + +// Like association_many_to_many_same_model but with a super many to many, +// i.e. explicit through table relations). + +const assert = require('assert'); +const path = require('path'); + +const { Sequelize, DataTypes, Op } = require('sequelize'); + +const sequelize = new Sequelize({ + dialect: 'sqlite', + storage: 'tmp.' + path.basename(__filename) + '.sqlite', + define: { + timestamps: false + }, +}); + +(async () => { + +// Create the tables. +const User = sequelize.define('User', { + name: { type: DataTypes.STRING }, +}); +const UserFollowUser = sequelize.define('UserFollowUser', { + UserId: { + type: DataTypes.INTEGER, + references: { + model: User, + key: 'id' + } + }, + FollowId: { + type: DataTypes.INTEGER, + references: { + model: User, + key: 'id' + } + }, + } +); + +// Super many to many. Only works with explicit table for some reason. +User.belongsToMany(User, {through: UserFollowUser, as: 'Follows'}); +UserFollowUser.belongsTo(User) +User.hasMany(UserFollowUser) + +await sequelize.sync({force: true}); + +// Create some users. + +const user0 = await User.create({name: 'user0'}) +const user1 = await User.create({name: 'user1'}) +const user2 = await User.create({name: 'user2'}) +const user3 = await User.create({name: 'user3'}) +await user0.addFollows([user1, user2]) +await user2.addFollow(user0) +await user3.addFollow(user0) + +// Find all users that a user follows. +const user0Follows = await user0.getFollows({order: [['name', 'ASC']]}) +assert(user0Follows[0].name === 'user1'); +assert(user0Follows[1].name === 'user2'); +assert(user0Follows.length === 2); + +const user1Follows = await user1.getFollows({order: [['name', 'ASC']]}) +assert(user1Follows.length === 0); + +const user2Follows = await user2.getFollows({order: [['name', 'ASC']]}) +assert(user2Follows[0].name === 'user0'); +assert(user2Follows.length === 1); + +const user3Follows = await user3.getFollows({order: [['name', 'ASC']]}) +assert(user3Follows[0].name === 'user0'); +assert(user3Follows.length === 1); + +// Same but with explicit id. +{ + const user0Follows = (await User.findOne({ + where: {id: user0.id}, + attributes: [], + include: [{ + model: User, + as: 'Follows', + through: {attributes: []}, + }], + })).Follows + assert(user0Follows[0].name === 'user1'); + assert(user0Follows[1].name === 'user2'); + assert(user0Follows.length === 2); +} + +// Another method with the many-to-many reversed. +// Using the super many to many is the only way I know of doing this so far. +// which is a pain. +{ + const user0Follows = await User.findAll({ + include: [{ + model: UserFollowUser, + attributes: [], + on: { + FollowId: { [Op.col]: 'User.id' }, + }, + where: {UserId: user0.id} + }], + order: [['name', 'ASC']], + }) + assert(user0Follows[0].name === 'user1'); + assert(user0Follows[1].name === 'user2'); + assert(user0Follows.length === 2); +} + +await sequelize.close(); +})(); diff --git a/rootfs_overlay/lkmc/nodejs/sequelize/association_nested_include.js b/rootfs_overlay/lkmc/nodejs/sequelize/association_nested_include.js index 1800edc..df5fb76 100755 --- a/rootfs_overlay/lkmc/nodejs/sequelize/association_nested_include.js +++ b/rootfs_overlay/lkmc/nodejs/sequelize/association_nested_include.js @@ -11,6 +11,9 @@ const { Sequelize, DataTypes } = require('sequelize'); const sequelize = new Sequelize({ dialect: 'sqlite', storage: 'tmp.' + path.basename(__filename) + '.sqlite', + define: { + timestamps: false + }, }); (async () => { @@ -18,10 +21,10 @@ const sequelize = new Sequelize({ // Create the tables. const User = sequelize.define('User', { name: { type: DataTypes.STRING }, -}, {}); +}); const Post = sequelize.define('Post', { body: { type: DataTypes.STRING }, -}, {}); +}); User.belongsToMany(User, {through: 'UserFollowUser', as: 'Follows'}); User.hasMany(Post); Post.belongsTo(User); @@ -173,6 +176,31 @@ await users[0].addFollows([users[1], users[2]]) assert(user0FollowsLimit2[0].name === 'user1') assert(user0FollowsLimit2.length === 1) + // Get just the count of the posts authored by useres followed by user0. + // attributes: [] excludes all other data from the SELECT of the querries + // to optimize things a bit. + // https://stackoverflow.com/questions/37817808/counting-associated-entries-with-sequelize + { + const user0Follows = await User.findByPk(users[0].id, { + attributes: [ + [Sequelize.fn('COUNT', Sequelize.col('Follows.Posts.id')), 'count'] + ], + include: [ + { + model: User, + as: 'Follows', + attributes: [], + through: {attributes: []}, + include: [{ + model: Post, + attributes: [], + }], + }, + ], + }) + assert.strictEqual(user0Follows.dataValues.count, 4); + } + // Case in which our post-sorting is needed. // TODO: possible to get sequelize to do this for us by returning // a flat array directly? @@ -229,36 +257,37 @@ await users[0].addFollows([users[1], users[2]]) assert(postsFound.length === 4) } - //// This is likely almost it. We just have to understand the undocumented custom on: - //// to specify from which side of the UserFollowsUser we are coming. - //{ - // const postsFound = await Post.findAll({ - // order: [[ - // 'body', - // 'DESC' - // ]], - // subQuery: false, - // include: [ - // { - // model: User, - // on: {'id': '$Post.User.FollowId$'}, - // include: [ - // { - // model: User, - // as: 'Follows', - // where: {id: users[0].id}, - // } - // ], - // }, - // ], - // }) - // console.error(postsFound.length); - // assert.strictEqual(postsFound[0].body, 'body6') - // assert.strictEqual(postsFound[1].body, 'body5') - // assert.strictEqual(postsFound[0].body, 'body1') - // assert.strictEqual(postsFound[1].body, 'body2') - // assert.strictEqual(postsFound.length, 4) - //} + //// This almost achieves the flat array return. We just have to understand the undocumented custom on: + //// to specify from which side of the UserFollowsUser we are coming. The on: + //// is ignored without super many to many unfortunately, the below just returns all posts. + { + const postsFound = await Post.findAll({ + order: [[ + 'body', + 'DESC' + ]], + subQuery: false, + include: [ + { + model: User, + //on: {idasdf: '$Post.User.FollowId$'}, + include: [ + { + model: User, + as: 'Follows', + where: {id: users[0].id}, + } + ], + }, + ], + }) + console.error(postsFound.length); + //assert.strictEqual(postsFound[0].body, 'body6') + //assert.strictEqual(postsFound[1].body, 'body5') + //assert.strictEqual(postsFound[2].body, 'body2') + //assert.strictEqual(postsFound[3].body, 'body1') + assert.strictEqual(postsFound.length, 4) + } } await sequelize.close(); diff --git a/rootfs_overlay/lkmc/nodejs/sequelize/association_nested_include_super.js b/rootfs_overlay/lkmc/nodejs/sequelize/association_nested_include_super.js new file mode 100755 index 0000000..5ee3d4c --- /dev/null +++ b/rootfs_overlay/lkmc/nodejs/sequelize/association_nested_include_super.js @@ -0,0 +1,112 @@ +#!/usr/bin/env node + +// Like association_nested_include.js but with a super many to many. + +const assert = require('assert'); +const path = require('path'); + +const { Sequelize, DataTypes, Op } = require('sequelize'); + +const sequelize = new Sequelize({ + dialect: 'sqlite', + storage: 'tmp.' + path.basename(__filename) + '.sqlite', + define: { + timestamps: false + }, +}); + +(async () => { + +// Create the tables. +const User = sequelize.define('User', { + name: { type: DataTypes.STRING }, +}); +const Post = sequelize.define('Post', { + body: { type: DataTypes.STRING }, +}); +const UserFollowUser = sequelize.define('UserFollowUser', { + UserId: { + type: DataTypes.INTEGER, + references: { + model: User, + key: 'id' + } + }, + FollowId: { + type: DataTypes.INTEGER, + references: { + model: User, + key: 'id' + } + }, + } +); + +// Super many to many. +User.belongsToMany(User, {through: UserFollowUser, as: 'Follows'}); +UserFollowUser.belongsTo(User) +User.hasMany(UserFollowUser) + +User.hasMany(Post); +Post.belongsTo(User); + +await sequelize.sync({force: true}); + +// Create data. +const users = await User.bulkCreate([ + {name: 'user0'}, + {name: 'user1'}, + {name: 'user2'}, + {name: 'user3'}, +]) +const posts = await Post.bulkCreate([ + {body: 'body0', UserId: users[0].id}, + {body: 'body1', UserId: users[1].id}, + {body: 'body2', UserId: users[2].id}, + {body: 'body3', UserId: users[3].id}, + {body: 'body4', UserId: users[0].id}, + {body: 'body5', UserId: users[1].id}, + {body: 'body6', UserId: users[2].id}, + {body: 'body7', UserId: users[3].id}, +]) +await users[0].addFollows([users[1], users[2]]) + +// Get all the posts by authors that user0 follows. +// without post process sorting. We only managed to to this +// with a super many to many, because that allows us to specify +// a reversed order in the through table with `on`, since we need to +// match with `FollowId` and not `UserId`. +{ + const postsFound = await Post.findAll({ + order: [[ + 'body', + 'DESC' + ]], + include: [ + { + model: User, + attributes: [], + required: true, + include: [ + { + model: UserFollowUser, + on: { + FollowId: {[Op.col]: 'User.id' }, + }, + attributes: [], + where: {UserId: users[0].id}, + } + ], + }, + ], + }) + console.error(postsFound.length); + assert.strictEqual(postsFound[0].body, 'body6') + assert.strictEqual(postsFound[1].body, 'body5') + assert.strictEqual(postsFound[2].body, 'body2') + assert.strictEqual(postsFound[3].body, 'body1') + assert.strictEqual(postsFound.length, 4) +} + +await sequelize.close(); +})(); diff --git a/rootfs_overlay/lkmc/nodejs/sequelize/ignore_duplicates.js b/rootfs_overlay/lkmc/nodejs/sequelize/ignore_duplicates.js new file mode 100755 index 0000000..324fb4f --- /dev/null +++ b/rootfs_overlay/lkmc/nodejs/sequelize/ignore_duplicates.js @@ -0,0 +1,47 @@ +#!/usr/bin/env node + +const assert = require('assert'); +const path = require('path'); +const { Sequelize, DataTypes } = require('sequelize'); +const sequelize = new Sequelize({ + dialect: 'sqlite', + storage: 'tmp.' + path.basename(__filename) + '.sqlite', + define: { + timestamps: false + }, +}); +(async () => { +const Tag = sequelize.define('Tag', { + name: { + type: DataTypes.STRING, + unique: true, + }, +}); +await sequelize.sync({force: true}) +await Tag.create({name: 't0'}) + +// Individual create does not have the option for some reason. +// Apparently you're just supposed to catch. +// https://github.com/sequelize/sequelize/issues/4513 +//await Tag.create({name: 't0', ignoreDuplicates: true}) + +// SQLite: INSERT OR IGNORE INTO as desired. +const tags = await Tag.bulkCreate( + [ + {name: 't0'}, + {name: 't1'}, + {name: 't1'}, + {name: 't2'}, + ], + { + ignoreDuplicates: true, + } +) +const tagsFound = await Tag.findAll({order: [['name', 'ASC']]}) +assert.strictEqual(tagsFound[0].name, 't0') +assert.strictEqual(tagsFound[1].name, 't1') +assert.strictEqual(tagsFound[2].name, 't2') +assert.strictEqual(tagsFound.length, 3) + +await sequelize.close(); +})(); diff --git a/rootfs_overlay/lkmc/nodejs/sequelize/index.js b/rootfs_overlay/lkmc/nodejs/sequelize/index.js index ad35ce4..a9d60a0 100755 --- a/rootfs_overlay/lkmc/nodejs/sequelize/index.js +++ b/rootfs_overlay/lkmc/nodejs/sequelize/index.js @@ -50,7 +50,7 @@ const IntegerNames = sequelize.define('IntegerNames', { name: { type: DataTypes.STRING, }, -}, {}); +}); // Create the database defined by `sequelize.define`. await IntegerNames.sync({force: true})