Sequelize: Avoid duplicates when create with manyToMany associations - sql

I'm developing a small e-commerce to sell cinema tickets. I'm using Sequelize to design all models and their associations. The issue I'm facing is related with the route defined to create the final order:
Order.create({
userId: req.body.userId, // Number
sessionId: req.body.sessionId, // Number
seats: req.body.seats, // Array of objects
offsiteProducts: req.body.offsiteProducts // Array of objects
},
{
include: [
{
model: Seat,
attributes: ['area', 'number', 'roomId']
},
{
model: OffsiteProduct,
attributes: ['name', 'unitPrice']
}
]
}).then(order => {
res.json(order);
})
The relation between models is as follows:
User.hasMany(Order);
Order.belongsTo(User);
Session.hasMany(Order);
Order.belongsTo(Session);
Seat.belongsToMany(Order, { through: "reserved_seats" });
Order.belongsToMany(Seat, { through: "reserved_seats" });
OffsiteProduct.belongsToMany(Order, { through: ReservedOffsiteProduct });
Order.belongsToMany(OffsiteProduct, { through: ReservedOffsiteProduct });
For the "one-to-many" relationships, passing a foreign key is enough for Sequelize to associate models properly.
But for "many-to-many" associations ("belongsToMany" in Sequelize) it would duplicate the data entered for seats and offsite products and create it both as part of the order and as a new independent seat and offsite product respectively.
How can I avoid this behaviour and include the arrays of seats and offsite products only inside the final order? Thanks.

One way to do this is to execute everything in a transaction. That way, it will be atomic, i.e. "all or nothing".
Sequelize provides some methods for many-to-many associations. In this case, a separate call could be made for each junction table.
With this in mind, the following should accomplish the inserts without any duplicates:
let t = await sequelize.transaction()
try {
let order = await Order.create({
userId: req.body.userId,
sessionId: req.body.sessionId
}, {
transaction: t
})
let seats = await Seat.findAll({
where: {
id: {
[Op.in]: req.body.seatIds
}
}
})
let offsiteProducts = await OffsiteProducts.findAll({
where: {
id: {
[Op.in]: req.body.offsiteProductIds
}
}
})
await order.addSeats(seats, { transaction: t })
await order.addOffsiteProducts(offsiteProducts, { transaction: t })
await t.commit()
} catch (err) {
if (t) {
await t.rollback()
}
}
Alternatively, if the primary keys for the rows in the OffsiteProducts and Seats tables are already known, the above could be shortened to:
let t = await sequelize.transaction()
try {
let order = await Order.create({
userId: req.body.userId,
sessionId: req.body.sessionId
}, {
transaction: t
})
await order.addSeats(req.body.seatIds, { transaction: t })
await order.addOffsiteProducts(req.body.offsiteProductIds, { transaction: t })
await t.commit()
} catch (err) {
if (t) {
await t.rollback()
}
}
There's a bit more explanation for passing arrays of primary keys to the .addSeats and .addOffsiteProducts methods in the docs here.

Related

TypeORM - Getting objects of provided id, which is one relation away

I want to get objects from table providing id, which is in relation with table, which is in another relation. It looks like this:
Hand is in relation manyToOne with Action (hand can have only one action),
Action is in relation manyToOne with Situation (action can have only one situation)
I'm trying to make GET request for hands in which I'm providing situationId.
Simplified entities:
#Entity()
export class Hand {
#PrimaryGeneratedColumn()
hand_id: number;
#Column()
hand: string;
#ManyToOne(type => Action, action => action.simplifiedhands, { eager: true, onDelete: 'CASCADE', onUpdate: 'CASCADE' })
action: Action;
}
#Entity()
export class Action {
#PrimaryColumn()
action_id: number;
#ManyToOne(type => Situation, situation => situation.actions, { onDelete: 'CASCADE', onUpdate: 'CASCADE' })
#JoinColumn({name: 'situation'})
situation: Situation;
#OneToMany(type => Hand, hand => hand.action)
hands: Hand[];
#OneToMany(type => Hand, hand => hand.action)
hands: Hand[];
}
#Entity()
export class Situation {
#PrimaryColumn()
situation_id: number;
#ManyToOne(type => Strategy, strategy => strategy.situations, { onDelete: 'CASCADE', onUpdate: 'CASCADE' })
strategy: Strategy;
#OneToMany(type => Action, action => action.situation)
actions: Action[];
}
What approaches didn't work for me so far (just example variants):
return await this.handsRepository.find({
relations: ["action", "action.situation"],
where: {
"situation": id
}
});
and
return await this.handsRepository.find({
join: {
alias: "hands",
leftJoinAndSelect: {
"action": "hand.action",
"situation": "action.situation"
}
},
where: {
"situation": id
}
});
Generally both 'works' but provide all the records, like there were no where condition.
The keys in the object you assign to where should be members of the entity of the repository, in your case Hand, since situation is a member of action it's not working. I'm surprised you didn't mention any errors.
You can do one of the following (example for postgres)
Using query builder:
return await this.handsRepository.createQueryBuilder(Hand, 'hand')
.leftJoin('hand.action', 'action')
.leftJoin('action.situation', 'situation')
.where('situation.id = :id', { id })
.getMany();
Or, you can try the following (success is not guaranteed):
return await this.handsRepository.find({
relations: ["action", "action.situation"],
where: {
action: {
situation: { id }
}
}
});

Sequelize query with a where clause on an include of an include

I'm struggling to create a query with sequelize.
Some context
I have the following models:
A Manifestation can have [0..n] Event
An Event belongs to one Manifestation (an Event cannot exist without a Manifestation)
A Place can have [0..n] Event
An Event belongs to one Place (an Event cannot exist without a Place)
A Manifestation can have [1..n] Place
A Place can have [0..n] Manifestation
I model the relations as the following:
Manifestation.hasMany(Event, { onDelete: 'CASCADE', hooks: true })
Event.belongsTo(Manifestation)
Place.hasMany(Event, { onDelete: 'CASCADE', hooks: true })
Event.belongsTo(Place)
Manifestation.belongsToMany(Place, { through: 'manifestation_place' })
Place.belongsToMany(Manifestation, { through: 'manifestation_place' })
For me it seems rather correct, but don't hesitate if you have remarks.
The question
I'm trying to query the Place in order to get all Manifestation and Event happening in a given Place. But for the Event ones, I want to include them within their Manifestation even if the Manifestation doesn't happen in the given Place.
Below is the "JSON" structure I'm trying to achieve:
{
id: 1,
name: "Place Name",
address: "Place address",
latitude: 47.00000,
longitude: -1.540000,
manifestations: [
{
id: 10,
title: "Manifestation one",
placeId: 1,
events: []
},
{
id: 11,
title: "Manifestation two",
placeId: 3,
events: [
id: 5,
title: "3333",
manifestationId: 11,
placeId: 1
]
}
]
}
So I want to include the Manifestation with id: 11, because one of its Event occurs in the given Place (with id: 1)
Update (04/06/20): For now I rely on javascript to get the expected result
I figured out it would be nice if I posted my current solution before asking.
router.get('/test', async (req, res) => {
try {
const placesPromise = place.findAll()
const manifestationsPromise = manifestation.findAll({
include: [
{ model: event },
{
model: place,
attributes: ['id'],
},
],
})
const [places, untransformedManifestations] = await Promise.all([
placesPromise,
manifestationsPromise,
])
const manifestations = untransformedManifestations.map(m => {
const values = m.toJSON()
const places = values.places.map(p => p.id)
return { ...values, places }
})
const result = places
.map(p => {
const values = p.toJSON()
const relatedManifestations = manifestations
.filter(m => {
const eventsPlaceId = m.events.map(e => e.placeId)
return (
m.places.includes(values.id) ||
eventsPlaceId.includes(values.id)
)
})
.map(m => {
const filteredEvents = m.events.filter(
e => e.placeId === values.id
)
return { ...m, events: filteredEvents }
})
return { ...values, manifestations: relatedManifestations }
})
.filter(p => p.manifestations.length)
return res.status(200).json(result)
} catch (err) {
console.log(err)
return res.status(500).send()
}
})
But I'm pretty sure I could do that directly with sequelize. Any ideas or recommendations ?
Thanks
This is not optimum. But you can try it out:
const findPlace = (id) => {
return new Promise(resolve => {
db.Place.findOne({
where: {
id: id
}
}).then(place => {
db.Manefestation.findAll({
include: [{
model: db.Event,
where: {
placeId: id
}
}]
}).then(manifestations => {
const out = Object.assign({}, {
id: place.id,
name: place.name,
address: place.address,
latitude: place.latitude,
longitude: place.longitude,
manifestations: manifestations.reduce((res, manifestation) => {
if (manifestation.placeId === place.id || manifestation.Event.length > 0) {
res.push({
id: manifestation.id,
title: manifestation.id,
placeId: manifestation.placeId,
events: manifestation.Event
})
}
return res;
}, [])
})
})
resolve(out);
})
})
}
From this, you get all manifestations that assigned to place or have any event that assigns. All included events in the manefestations are assigned to the place.
Edit :
You will be able to use the following one too:
const findPlace = (id) => {
return new Promise(resolve => {
db.Place.findOne({
include: [{
model: db.Manefestation,
include: [{
model: db.Event,
where: {
placeId: id
}
}]
}],
where: {
id: id
}
}).then(place => {
db.Manefestation.findAll({
include: [{
model: db.Event,
where: {
placeId: id
}
}],
where: {
placeId: {
$not: id
}
}
}).then(manifestations => {
place.Manefestation = place.Manefestation.concat(manifestations.filter(m=>m.Event.length>0))
resolve(place);// or you can rename, reassign keys here
})
})
})
}
Here I take only direct manifestations in the first query. Then, manifestations that not included and concatenate.
I do not know if you figure it out by now. But the solution is provided below.
Search with Sequelize could get funny :). You have to include inside another include. If the query gets slow use separate:true.
Place.findAll({
include: [
{
model: Manifestation,
attributes: ['id'],
include: [{
model: Event ,
attributes: ['id']
}]
},
],
})
I tried to complete it in a single query but you will still need JavaScript to be able to get the type of output that you want.
(Note: 💡 You need manifestation which is not connected to places but should be included if a event is present of that place. The only SQL way to get that starts by doing a CROSS JOIN between all tables and then filtering out the results which will be a very hefty query)
I came up with this code(tried & executed) which doesn't need you to execute 2 findAll that fetches all data as what you are currently using. Instead it fetched only the data needed for final output in 1 query.
const places = await Place.findAll({
include: [{
model: Manifestation,
// attributes: ['id']
through: {
attributes: [], // this helps not get keys/data of join table
},
}, {
model: Event,
include: [{
model: Manifestation,
// attributes: ['id']
}],
}
],
});
console.log('original output places:', JSON.stringify(places, null, 2));
const result = places.map(p => {
// destructuring to separate out place, manifestation, event object keys
const {
manifestations,
events,
...placeData
} = p.toJSON();
// building modified manifestation with events array
const _manifestations = manifestations.map(m => {
return ({ ...m, events: [] })
});
// going through places->events to push them to respective manifestation events array
// + add manifestation which is not directly associated to place but event is of that manifestation
events.map(e => {
const {
manifestation: e_manifestation, // renaming variable
...eventData
} = e;
const mIndex = _manifestations.findIndex(m1 => m1.id === e.manifestationId)
if (mIndex === -1) { // if manifestation not found add it with the events array
_manifestations.push({ ...e_manifestation, events: [eventData] });
} else { // if found push it into events array
_manifestations[mIndex].events.push(eventData);
}
});
// returning a place object with manifestations array that contains events array
return ({ ...placeData, manifestations: _manifestations });
})
// filter `.filter(p => p.manifestations.length)` as used in your question
console.log('modified places', JSON.stringify(result, null, 2));

Sequelize Many to Many Relationship using Through does not insert additional attributes

I have a many to many relationship between: Step and Control Through ControlsConfig.
When creating a Control object and call addStep function and specify the additional attributes (which exist in the relation table), Sequelize creates the records in the relational table ControlsConfig but the additional attributes are NULLs.
PS: The tables are creating correctly in the database.
Table 1: Step
Table 2: Control
Relation table: ControlsConfig
Step
var Step = sequelize.define('Step', {
title: { type: DataTypes.STRING, allowNull: false },
description: DataTypes.STRING,
type: { type: DataTypes.ENUM('task', 'approval'), allowNull: false, defaultValue: 'task' },
order: DataTypes.INTEGER
});
Step.associate = function(models) {
models.Step.belongsTo(models.User);
models.Step.belongsTo(models.Template);
models.Step.hasMany(models.Action);
};
Control
var Control = sequelize.define('Control', {
label: { type: DataTypes.STRING, allowNull: false },
order: { type: DataTypes.INTEGER },
type: { type: DataTypes.ENUM('text', 'yes/no') },
config: { type: DataTypes.TEXT },
controlUiId: { type: DataTypes.STRING }
});
Control.associate = function(models) {
models.Control.belongsTo(models.Section);
};
ControlsConfigs
module.exports = (sequelize, DataTypes) => {
var ControlsConfig = sequelize.define('ControlsConfig', {
visibility: { type: DataTypes.ENUM('hidden', 'readonly', 'editable', 'required') },
config: { type: DataTypes.TEXT }
});
ControlsConfig.associate = function(models) {
models.Control.belongsToMany(models.Step, { through: models.ControlsConfig });
models.Step.belongsToMany(models.Control, { through: models.ControlsConfig });
models.ControlsConfig.belongsTo(models.Template);
};
return ControlsConfig;
};
Insertion:
try {
var step1 = await Step.create({ /*bla bla*/ });
var control1 = await Control.create({ /*bla bla*/ });
var OK = await control1.addStep(step1, {through: { config: 'THIS FIELD ALWAYS APPEARS NULL' }});
} catch (error) { /* No errors*/ }
I am following the same strategy stated at the documentation
//If you want additional attributes in your join table, you can define a model for the join table in sequelize, before you define the association, and then tell sequelize that it should use that model for joining, instead of creating a new one:
const User = sequelize.define('user', {})
const Project = sequelize.define('project', {})
const UserProjects = sequelize.define('userProjects', {
status: DataTypes.STRING
})
User.belongsToMany(Project, { through: UserProjects })
Project.belongsToMany(User, { through: UserProjects })
//To add a new project to a user and set its status, you pass extra options.through to the setter, which contains the attributes for the join table
user.addProject(project, { through: { status: 'started' }})
You have to pass edit: true to the addProject and addStep method.
See this answer it has a similar issue
Sequelize belongsToMany additional attributes in join table

GraphQL queries with tables join using Node.js

I am learning GraphQL so I built a little project. Let's say I have 2 models, User and Comment.
const Comment = Model.define('Comment', {
content: {
type: DataType.TEXT,
allowNull: false,
validate: {
notEmpty: true,
},
},
});
const User = Model.define('User', {
name: {
type: DataType.STRING,
allowNull: false,
validate: {
notEmpty: true,
},
},
phone: DataType.STRING,
picture: DataType.STRING,
});
The relations are one-to-many, where a user can have many comments.
I have built the schema like this:
const UserType = new GraphQLObjectType({
name: 'User',
fields: () => ({
id: {
type: GraphQLString
},
name: {
type: GraphQLString
},
phone: {
type: GraphQLString
},
comments: {
type: new GraphQLList(CommentType),
resolve: user => user.getComments()
}
})
});
And the query:
const user = {
type: UserType,
args: {
id: {
type: new GraphQLNonNull(GraphQLString)
}
},
resolve(_, {id}) => User.findById(id)
};
Executing the query for a user and his comments is done with 1 request, like so:
{
User(id:"1"){
Comments{
content
}
}
}
As I understand, the client will get the results using 1 query, this is the benefit using GraphQL. But the server will execute 2 queries, one for the user and another one for his comments.
My question is, what are the best practices for building the GraphQL schema and types and combining join between tables, so that the server could also execute the query with 1 request?
The concept you are refering to is called batching. There are several libraries out there that offer this. For example:
Dataloader: generic utility maintained by Facebook that provides "a consistent API over various backends and reduce requests to those backends via batching and caching"
join-monster: "A GraphQL-to-SQL query execution layer for batch data fetching."
To anyone using .NET and the GraphQL for .NET package, I have made an extension method that converts the GraphQL Query into Entity Framework Includes.
public static class ResolveFieldContextExtensions
{
public static string GetIncludeString(this ResolveFieldContext<object> source)
{
return string.Join(',', GetIncludePaths(source.FieldAst));
}
private static IEnumerable<Field> GetChildren(IHaveSelectionSet root)
{
return root.SelectionSet.Selections.Cast<Field>()
.Where(x => x.SelectionSet.Selections.Any());
}
private static IEnumerable<string> GetIncludePaths(IHaveSelectionSet root)
{
var q = new Queue<Tuple<string, Field>>();
foreach (var child in GetChildren(root))
q.Enqueue(new Tuple<string, Field>(child.Name.ToPascalCase(), child));
while (q.Any())
{
var node = q.Dequeue();
var children = GetChildren(node.Item2).ToList();
if (children.Any())
{
foreach (var child in children)
q.Enqueue(new Tuple<string, Field>
(node.Item1 + "." + child.Name.ToPascalCase(), child));
}
else
{
yield return node.Item1;
}
}}}
Lets say we have the following query:
query {
getHistory {
id
product {
id
category {
id
subCategory {
id
}
subAnything {
id
}
}
}
}
}
We can create a variable in "resolve" method of the field:
var include = context.GetIncludeString();
which generates the following string:
"Product.Category.SubCategory,Product.Category.SubAnything"
and pass it to Entity Framework:
public Task<TEntity> Get(TKey id, string include)
{
var query = Context.Set<TEntity>();
if (!string.IsNullOrEmpty(include))
{
query = include.Split(',', StringSplitOptions.RemoveEmptyEntries)
.Aggregate(query, (q, p) => q.Include(p));
}
return query.SingleOrDefaultAsync(c => c.Id.Equals(id));
}

Transaction with Sequelize doesn't work

I want to build a simple webform where you can enter a persons firstname, lastname and select multiple groups for this person (but one for now)
I'm using node.js and sequelize to store the person in a MariaDB -Database.
Sequelize created the tables Persons, Groups and GroupsPersons according to the defined models.
var Sequelize = require("sequelize");
var sequelize = new Sequelize(config.database, config.username, config.password, config);
var Group = sequelize.define("Group", {
name: {
type: DataTypes.STRING,
allowNull: false
}
}
var Person = sequelize.define("Person", {
firstName: {
type: DataTypes.STRING,
allowNull: false
},
lastName: {
type: DataTypes.STRING,
allowNull: false
}
}
Person.belongsToMany(Group, {as: 'Groups'});
Group.belongsToMany(Person, {as: 'Persons'});
Because creating the person and assigning it into a group should be handled atomically in one step I decided to use a transaction, shown in the docs here:
http://sequelize.readthedocs.org/en/latest/docs/transactions/#using-transactions-with-other-sequelize-methods
var newPerson = {
firstName: 'Hans',
lastName: 'Fischer'
}
var id = 3 // group
sequelize.transaction(function (t) {
return Person.create(newPerson, {transaction: t}).then(function (person) {
return Group.find(id, {transction: t}).then(function(group){
if (!group) throw Error("Group not found for id: " + id);
return person.setGroups( [group], {transction: t});
})
});
}).then(function (result) {
// Transaction has been committed
// result is whatever the result of the promise chain returned to the transaction callback is
console.log(result);
}).catch(function (err) {
// Transaction has been rolled back
// err is whatever rejected the promise chain returned to the transaction callback is
console.error(err);
});`
But for some reason neither function (result) {.. for success nor the function in catch gets called. However, the complete SQL queries of the transaction were generated except COMMIT, so nothing was inserted into the db.
If I put it like this
return person.setGroups( [], {transction: t});
the transactions succeeds, but with no inserts into GroupsPersons of course.
Any ideas or suggestions?
Thanks for help!
{transaction: t} was misspelled, it works now