Sequelize selecting id instead of foreign key - mysql

I have a Book table, a Category table and a join table named BookCategories. Here's an oversimplified structure:
books {
id,
name,
hash // unique
}
categories {
id,
name
}
book_categories {
id,
book_hash,
category_id
}
Because of the way the app works, it was decided to use the book_hash in the join table instead of the book_id.
Here are the Sequelize relations between the 3 tables:
// book
Book.hasMany(models.bookCategories, { foreignKey: 'book_hash' });
// category
Category.hasMany(models.bookCategories, { foreignKey: 'category_id' });
// bookCategories
BookCategories.belongsTo(models.book, { foreignKey: 'collection_hash' });
BookCategories.belongsTo(models.category, { foreignKey: 'category_id' });
I'm using Sequelize with GraphQL, so when creating the books field on the CategoryType, everything works great:
books: {
type: new GraphQLList(BookType),
args: defaultListArgs(),
async resolve(f, args) {
return await f.getBookCategories(args);
// normally doesn't return, uses results to get book hashes and return books
}
}
It runs the following queries:
SELECT `id`, `name` FROM `categories` ASC;
SELECT `id`, `book_hash`, `category_id` FROM `book_categories` AS `bookCategories` WHERE `bookCategories`.`category_id` IN (1, 2, 3, 4, 5);
But when doing it the other way around, requesting the bookCategories through the BookType, it doesn't:
categories: {
type: new GraphQLList(CategoryType),
args: defaultListArgs(),
async resolve(f, args) {
return await f.getBookCategories(args);
// normally doesn't return, uses results to get category ids and return categories
}
}
It runs these queries:
SELECT `id`, `name`, `hash`, FROM `books` AS `book` WHERE `book`.`hash` = 'ft3qvqlj';
SELECT `id`, `book_hash`, `category_id` FROM `book_categories` AS `bookCategories` WHERE `bookCategories`.`book_hash` IN (93586);
As you can see, even though I set the foreign key to the hash on both relations, it still uses the book id to do the query. Does it have to do with the fact that hash is not the primary key on the books table?
Is there a way to fix this or do I have to redo the database structure?

Related

How can i fetch data from another table referenced by foreign key?

Heres my tables that a wanna fetch the data, i am using express to make a get request from the next app:
model Provider {
id String #id #default(cuid())
name String
email String
password String
phone String
photo String?
service Service? #relation(fields: [serviceId], references: [id])
serviceId String?
location Location? #relation(fields: [locationId], references: [id])
locationId String?
createdAt DateTime #default(now())
text String
starAverage Float
medals Medals[]
comment Comment[]
}
model Service {
id String #id #default(cuid())
type String
provider Provider[]
}
I wanna fetch the type of the service of the services table of a provider, not the serviceId, and this is my route.
router.get('/list', async (req: Request, res: Response) => {
const allClients = await prisma.client.findMany()
res.json({allClients})
})
this is how i am fetching the data of the rest API using axios
const [providers, setProviders] = useState([] as any[])
useEffect(() => {
axios.get('http://localhost:5140/providers/list')//my list of all providers
.then(res => {
console.log(res)
setProviders(res.data)
}).catch(err => {
console.log(err)
})
}, )
const renderedProviders = Object.values(providers).map(provider => {
return (
<div
className="card"
style={{ width: "18rem"}}
key={provider.id}
>
<img className="card-img-top" src="..."/>
<div className="card-body">
<h3>{provider.name}</h3>
<p>{provider.starAverage} estrekas</p>
<p>{provider.serviceId}</p>
</div>
</div>
);
});
return (
<div className="d-flex flex-row flex-wrap justify-content-between">
{renderedProviders}
</div>
)
for now a only get the serviceId of a provider, not the type of the service
To fetch data from another table referenced by a foreign key in a database, you can use a JOIN clause in your SQL query. A JOIN clause allows you to combine rows from two or more tables based on a related column between the tables.
this how you can use a JOIN clause to fetch data from two tables, users and orders, that are related by a foreign key.
SELECT users.*, orders.*
FROM users
JOIN orders ON orders.user_id = users.id
the JOIN clause combines rows from the users and orders tables based on the user_id column in the orders table and the id column in the users table. The SELECT clause specifies the columns to be retrieved from the users and orders tables.
Edited
how you can reference this in the express route and in the http request from axios ?
you can use the sequelize.query(Sequelize is a promise-based Node.js ORM) method to execute a raw SQL query.
app.get('/users/:id', (req, res) => {
const { id } = req.params;
const query = `
SELECT users.*, orders.*
FROM users
JOIN orders ON orders.user_id = users.id
WHERE users.id = :id
`;
const replacements = { id };
sequelize.query(query).then(([results, metadata]) => {
res.send(results);
});
});
the sequelize.query method is used to execute a raw SQL query that includes a JOIN clause to fetch data from the users and orders tables.

Sorting of the sequencing belongstomany relationship does not work

The post and user tables created through sequencing have a belongstomany relationship, and a mapping table called like is created.
db.Post.belongsToMany(db.User, { through: 'Like', as: 'Likers' });
db.User.belongsToMany(db.Post, { through: 'Like', as: 'Liked' });
Using this, I wrote the following router to sort posts by the most likes.
const express = require('express');
const { Sequelize, Op } = require('sequelize');
const { Post, User, Image, Comment } = require('../models');
const router = express.Router();
router.get('/top', async (req, res, next) => { // loadTopPostsAPI / GET /posts/top
try {
const posts = await Post.findAll({
limit: 20,
offset: 0,
// Sort posts by the most likes
order: [[Sequelize.literal("(COUNT(`Likers->Like`.`PostId`))"), "ASC"]],
include: [{
model: User, // Post author
attributes: ['id', 'nickname'],
}, {
model: Image, // Post image
}, {
model: Comment, // Post Comment
include: [{
model: User, // Post Comment author
attributes: ['id', 'nickname'],
}],
}, {
model: User, // People who liked the post
as: 'Likers',
attributes: ['id'],
}],
})
res.status(200).json(posts);
} catch (error) {
console.error(error);
next(error);
}
});
But when I run the router, I get the following error
code: 'ER_BAD_FIELD_ERROR',
errno: 1054,
sqlState: '42S22',
sqlMessage: "Unknown column 'Likers->Like.PostId' in 'order clause'",
sql: 'SELECT `Post`.*, `User`.`id` AS `User.id`, `User`.`nickname` AS `User.nickname`, `Images`.`id` AS `Images.id`, `Images`.`src` AS `Images.src`, `Images`.`createdAt` AS `Images.createdAt`, `Images`.`updatedAt` AS `Images.updatedAt`, `Images`.`PostId` AS `Images.PostId`, `Comments`.`id` AS `Comments.id`, `Comments`.`content` AS `Comments.content`, `Comments`.`createdAt` AS `Comments.createdAt`, `Comments`.`updatedAt` AS `Comments.updatedAt`, `Comments`.`UserId` AS `Comments.UserId`, `Comments`.`PostId` AS `Comments.PostId`, `Comments->User`.`id` AS `Comments.User.id`, `Comments->User`.`nickname` AS `Comments.User.nickname`, `Likers`.`id` AS `Likers.id`, `Likers->Like`.`createdAt` AS `Likers.Like.createdAt`, `Likers->Like`.`updatedAt` AS `Likers.Like.updatedAt`, `Likers->Like`.`PostId` AS `Likers.Like.PostId`, `Likers->Like`.`UserId` AS `Likers.Like.UserId` FROM (SELECT `Post`.`id`, `Post`.`title`, `Post`.`desc`, `Post`.`ingredient`, `Post`.`recipes`, `Post`.`tips`, `Post`.`tags`, `Post`.`createdAt`, `Post`.`updatedAt`, `Post`.`UserId` FROM `posts` AS `Post` ORDER BY (COUNT(`Likers->Like`.`PostId`)) ASC LIMIT 0, 20) AS `Post` LEFT OUTER JOIN `users` AS `User` ON `Post`.`UserId` = `User`.`id` LEFT OUTER JOIN `images` AS `Images` ON `Post`.`id` = `Images`.`PostId` LEFT OUTER JOIN `comments` AS `Comments` ON `Post`.`id` = `Comments`.`PostId` LEFT OUTER JOIN `users` AS `Comments->User` ON `Comments`.`UserId` = `Comments->User`.`id` LEFT OUTER JOIN ( `Like` AS `Likers->Like` INNER JOIN `users` AS `Likers` ON `Likers`.`id` = `Likers->Like`.`UserId`) ON `Post`.`id` = `Likers->Like`.`PostId` ORDER BY (COUNT(`Likers->Like`.`PostId`)) ASC;',
parameters: undefined
},
How can I sort by resolving the above error?
Sequelize tries to form Subquery by default with associations and ORDER BY clause is composed within the subquery. However, SQL's ORDER BY has to be at the top level, so many cases when you need ORDER BY, OFFSET, LIMIT, you need to disable the subquery by adding subQuery: false. This will make Sequelize to form the query with JOIN instead of subquery.
await Post.findAll({
limit: 20,
offset: 0,
// Sort posts by the most likes
order: [[Sequelize.literal("(COUNT(`Likers->Like`.`PostId`))"), "ASC"]],
subQuery: false,
...
})
I bet this will make your current error go away but you have a new aggregation error, because this is trying to count full records which is disabled by MySQL by default. (ref: mysql error "ERROR 3029 (HY000): Expression #1 of ORDER BY contains aggregate function and applies to the result of a non-aggregated query")
To fix this issue and do count Likes by Post id, add PARTITION BY.
await Post.findAll({
limit: 20,
offset: 0,
// Sort posts by the most likes
order: [[Sequelize.literal("COUNT(`Likers->Like`.`PostId`) OVER (PARTITION BY `Post`.`id`)"), "ASC"]],
subQuery: false,
...
})

How can I INSERT if row doesn't exist, else UPDATE that row?

At the front-end, whenever I press submit an answer to a question, it'll create 1 result_ID that has these columns.
result_ID is auto-increment, question_ID is relation with the same question_ID from questions table.
If it's the first time the user chooses the answer, it'll create an answer_result (i parse in answer_ID) and answer_checkResult (value 1 or 0 to identify it's correct or incorrect), and a history_ID to identify each record separately.
History_ID is a different table that has the quiz_ID (to identify topic) and user_ID
example: History_ID 221 has 4 questions in there, and has 4 answers with 4 answer_result.
What I don't know is how can I create a situation if the row doesn't exist, it'll run INSERT INTO situation, and else if it already exists (because the user can change the answer multiple times in 1 question), it'll UPDATE. I've just created only the INSERT INTO option, but I don't know how to do the update in this model at the same time with INSERT INTO.
This is my history_result.model that I've created, I don't know how to create an if-else to update and create at the same time...
history_result.model
const HistoryResult = function (history_result) {
this.question_ID = history_result.question_ID;
this.answer_result = history_result.answer_result;
this.answer_checkResult = history_result.answer_checkResult;
this.history_ID = history_result.history_ID;
};
HistoryResult.create = async (newHistoryResult, result) => {
await db.query(
`INSERT INTO history_result SET question_ID = ?, answer_result = ?, answer_checkResult = ?, history_ID = ?`,
[
newHistoryResult.question_ID,
newHistoryResult.answer_result,
newHistoryResult.answer_checkResult,
newHistoryResult.history_ID,
],
(err, data) => {
if (err) {
result(err, null);
return;
} else {
return result(null, data);
}
}
);
};
And here's how I create the history_result controller
const HistoryResult = require("../models/history_result.model");
exports.createHistoryResult = async (req, res) => {
let { history_ID } = req.params;
let { question_ID, answer_result, answer_checkResult } = req.body;
let historyResult = new HistoryResult({
question_ID: question_ID,
answer_result: answer_result,
answer_checkResult: answer_checkResult,
history_ID: history_ID,
});
HistoryResult.create(historyResult, (err, data) => {
if (err) {
res.status(500).send({
message: err.message || "Error while creating result",
});
}
res.send(data);
});
};
Is there anyways I can achieve this? Thanks.
Yes, you can.
but first you have to make question_ID as PRIMARY KEY. And second parameter that you pass to db.query is object that contains history_result's attributes
INSERT INTO history_result
SET ?
ON DUPLICATE KEY UPDATE
answer_result = VALUES(answer_result),
answer_checkResult = VALUES(answer_checkResult),
history_ID = VALUES(history_ID)
db.query(query, objectHere, (err, data) => {
if (err) {
result(err, null);
return;
} else {
return result(null, data);
}
}))
First, please read the MySQL Insert or Update on duplicate-key update tutorial,
or this Official MySQL INSERT ... ON DUPLICATE KEY UPDATE Statement document
Now back to your question. As I understand, the question_ID and history_ID pair in the history_result table would be unique, as each user will only give one answer to a question in a quiz.
First you would need to create a unique index constraints of the pair (question_ID, history_ID) of your table.
ALTER TABLE history_result
ADD CONSTRAINT uc_question_history
UNIQUE (question_ID,history_ID);
And then issue an INSERT ON DUPLICATE KEY UPDATE statement to achive the effect.
INSERT INTO history_result
(
question_ID, answer_result, history_ID
)
VALUES
(14, 21, 12)
ON DUPLICATE KEY UPDATE
answer_result = 21;
If the question_ID = 14 and history_ID = 12 row already existed (scenario that user has already answer this question), it will trigger to update the answer_result. If not, it will insert a new record.
The DUPLICATE KEY constraint is met if a new row is a duplicate in UNIQUE index or PRIMARY KEY. In our case, it's the unique index of (question_ID, history_ID), hence the UPDATE statement will be invoked.

AWS Aurora DB Combine Data from Multiple SQL Statements

I have three tables with data schema, like:
TABLE user (
user_id BINARY(16) PRIMARY KEY NOT NULL,
created DATETIME NOT NULL,
last_updated DATETIME,
coordinator BINARY(16),
num_updates INT NOT NULL
);
TABLE summary (
user_id BINARY(16) PRIMARY KEY NOT NULL,
calculation_time DATETIME NOT NULL,
calculation_method VARCHAR(25) NOT NULL,
label VARCHAR(50) NOT NULL,
critical_count INT NOT NULL,
median_risk FLOAT(10)
);
TABLE actions(
user_id BINARY(16) PRIMARY KEY NOT NULL,
label VARCHAR(50) NOT NULL,
access_count INT NOT NULL,
median FLOAT(10)
);
The data for all the users (user table) is simply fetched using the lambda handler function in the following manner:
const AWS = require('aws-sdk');
const rdsDataService = new AWS.RDSDataService();
module.exports.hello = async (event, context, callback) => {
const req_id = "5a9dbfca-74d6-471a-af27-31beb4b53bb2";
const sql = 'SELECT * FROM user WHERE user_id=:id';
try {
const params = {
resourceArn: 'arn:aws:rds:us-west-********************',
secretArn: 'arn:aws:secretsmanager:us-west**************',
sql,
database: 'dev_db1',
continueAfterTimeout: true,
includeResultMetadata: true,
parameters: [{ 'name': 'id', 'value': { 'stringValue': `${req_id}` } }]
}
const db_res = await rdsDataService.executeStatement(params).promise();
const convertToJson = (dbresponse) => {
const columns = dbresponse.columnMetadata.map(col => col.name);
const row_data = dbresponse.records.map(row => {
const json_obj = {};
row.map((val, i) => {
json_obj[columns[i]] = Object.values(val)[0];
});
return json_obj;
});
return row_data;
};
const modified_data = convertToJson(db_res);
const response = {
body: {
statusCode: 200,
message: 'Data fetched successfully',
data: modified_data,
}
};
callback(null, response);
} catch (error) {
console.log('Error Received', error);
const error_res = {
body: {
statusCode: error.statusCode,
message: error.message,
data: null
}
}
callback(null, error_res);
}
};
If the same is followed for another table summary or actions, it also works. Now, I need to combine all the columns of these three tables and then return the data (returned rows should match on the basis of req_id).
My working snippet: https://dbfiddle.uk/?rdbms=mysql_5.7&fiddle=016ecc94c792611fbaca810605e81a6a
But the final result obtained contains the column user_id in duplicated form i.e. three times inclusion. I don't need the same column to be repeated thrice.
I am a bit new to handling MySQL queries, so unable to figure out the exact reason for the error even when the table exists. The MYSQL version used in Aurora is 5.7.
Any help to resolve the same is appreciated!
Plan A: Explicitly specify the columns you want. Extra benefit: You can get rid of the ids, which tend to be useless to others reading the output.
Plan B: (This option is not always possible.) Instead of JOIN .. ON t1.a = t2.a, say JOIN .. USING(a)
I like to use short aliases. Here's doing all things together:
SELECT u.last_name, u.first_name,
s.risk_score,
t.likes
FROM user AS u
JOIN summary AS s USING(user_id)
LEFT JOIN test AS t USING(user_id)
In general, it is not wise to have a 1:1 relationship (as you have via user_id); you may as well have all the columns in a single table.
try this
SELECT users.*, summary.* from users, summary WHERE users.user_id = summary.user_id
OR
SELECT * from users, summary WHERE users.user_id = summary.user_id

Getting wrong query generated by Sequelize Assocation

I have two table. One is Order and second one is OrderStatus.
In the orders table order_status_code is foreignKey that references on id to the OrderStatus table.
I have below model association definition.
Order.associate = function(models) {
// associations can be defined here
Order.hasOne(models.OrderItem,{foreignKey: "order_id"}),
Order.hasOne(models.OrderStatus, {foreignKey: "order_status_code"})
};
I am getting below error:
Unknown column 'OrderStatus.order_status_code' in 'field list
when I try to eager loading the OrderStatus.
const orders = await Order.findAll({
where: filter,
include: {
model: OrderStatus
}
})
Below is the query that is being shown on the console.
SELECT `Order`.`id`, `Order`.`buyer_id`, `Order`.`order_status_code`, `Order`.`order_detail`, `Order`.`order_date`, `Order`.`order_number`, `Order`.`created_at`, `Order`.`updated_at`, `OrderStatus`.`id` AS `OrderStatus.id`, `OrderStatus`.`order_status_code` AS `OrderStatus.order_status_code`, `OrderStatus`.`status` AS `OrderStatus.status`, `OrderStatus`.`created_at` AS `OrderStatus.created_at`, `OrderStatus`.`updated_at` AS `OrderStatus.updated_at` FROM `Orders` AS `Order` LEFT OUTER JOIN `OrderStatuses` AS `OrderStatus` ON `Order`.`order_status_code` = `OrderStatus`.`id` WHERE `Order`.`buyer_id` = 23;
I don't know why it is selecting OrderStatus.order_status_code
I fixed it by defining attributes to select from the included model and It fixed the problem for now.
const orders = await Order.findAll({
where: filter,
include: {
model: OrderStatus,
attributes:["status"]
}
})