MySQL many-to-many relationship in LoopBack - mysql

I'm having trouble getting LoopBack to perform a many-to-many join query. Considering the hasManyThrough example from the documentation:
var Physician = ds.createModel('Physician', {name: String});
var Patient = ds.createModel('Patient', {name: String});
var Appointment = ds.createModel('Appointment', {
physicianId: Number,
patientId: Number,
appointmentDate: Date
});
Appointment.belongsTo(Patient);
Appointment.belongsTo(Physician);
Physician.hasMany(Patient, {through: Appointment});
Patient.hasMany(Physician, {through: Appointment});
If I try to do a single search to find Patients associated with a particular doctor who have a zip code of 10012, I could try:
physician.patients({where: {zip: 10012}}, fn);
However, the search on the physician's patients is actually only searching on the Appointments table. Is there any way to do a simple search that will be performed on the specific physician's patients directly?

LoopBack implements the hasMany/through relation for physician.patients() as follows:
Appointment.find ({ where: { physicianId: 1 },
include: 'patient',
collect: 'patient' }, callback);
We're considering to support the filter for the 'include' which brings int 'patient' information.
I suggest you open an issue at https://github.com/strongloop/loopback-datasource-juggler/issues.

Related

Query model based on value inside one of its columns

I have a sequelize model called staff. One of the staff columns is called locations which holds the id's of all the locations this staff is available at. I want to create a query that uses a Location ID to get all staff active at that location. How can I do that using the ORM?
Assuming I have understood your question correctly and your locations column contains CSV data, then you need to use FIND_IN_SET() -
Staff.findAll({
where: sequelize.where(sequelize.fn('FIND_IN_SET', locationId, sequelize.col('locations')), {[Op.gt]: 0})
})
A better option would be to normalize your data as this query is non-SARGable.
In Sequelize, you can perform a query to filter staff based on their locations using the include and where options in your query.
const staff = await Staff.findAll({
include: [
{
model: Location,
as: 'locations',
where: { id: locationId },
through: { attributes: [] }
}
]
});
const findStaff = await StaffModel.findAll({
where: {
locationId: "your locationId"
}
})

Many-to-many Self Relation Prisma - One Field

I'm trying to create a friendship mechanic for my app using Prisma among other tools. In the docs it shows the following example for how to create a many-to-many self relation:
model User {
id Int #id #default(autoincrement())
name String?
followedBy Follows[] #relation("following")
following Follows[] #relation("follower")
}
model Follows {
follower User #relation("follower", fields: [followerId], references: [id])
followerId Int
following User #relation("following", fields: [followingId], references: [id])
followingId Int
##id([followerId, followingId])
}
I have implemented this and it works, however the issue is that for friendships, there is no 'following' and 'followedBy', you're just friends. At the moment, when I query, I have to query both fields in order to find all of a user's friends. Is there any way to define this type of relationship with only one field? Whereby we just have a single list of friends on a user?
I agree that it would be nice if Prisma could more natively support this sort of self-relation where the relationship is expected to be symmetric (e.g. userA is friends with userB if and only if userB is friends with userA).
However, as far as I can tell Prisma insists on having two "sides" of the relationship. (If someone knows better, I would love to hear it!) So what follows is the approach I am taking, which avoids having to query both relations to find a user's full set of friends.
Concept
We'll use one "side" of the relation to contain the complete set of friends. The other "side" exists solely to meet Prisma's requirement, and we'll never query it directly.
When adding or removing a friend relationship, we'll make two prisma calls, one to update each object.
Code
Schema file:
model User {
id Int #id #default(autoincrement())
name String?
friends User[] #relation("UserFriends")
// This second "side" of the UserFriends relation exists solely
// to satisfy prisma's requirements; we won't access it directly.
symmetricFriends User[] #relation("UserFriends")
}
Methods to add and remove friendships (there's plenty of redundant code in here that could be abstracted out, but I think it's clearer to read this way):
const addFriendship = async (userIdA: string, userIdB: string) => {
await prisma.user.update({
where: {id: userIdA},
data: {friends: {connect: [{id: userIdB}]}},
});
await prisma.user.update({
where: {id: userIdB},
data: {friends: {connect: [{id: userIdA}]}},
});
};
const removeFriendship = async (userIdA: string, userIdB: string) => {
await prisma.user.update({
where: {id: userIdA},
data: {friends: {disconnect: [{id: userIdB}]}},
});
await prisma.user.update({
where: {id: userIdB},
data: {friends: {disconnect: [{id: userIdA}]}},
});
}
With this approach, one can load a user and get all their friends in the expected manner, e.g.
const getUserWithFriends = async (userId) =>
await prisma.user.find({
where: {id: userId},
include: {friends: true},
});

How to use .findAll and find records from two different tables and a self reference?

I'm working with two tables in particular. Users and Friends. Users has a bunch of information that defines the User whereas Friends has two columns aside from id: user_id and friend_id where both of them are a reference to the User table.
I'm trying to find all of the users friends in as little calls to the db as possible and I currently have 2. One to retrieve the id of a user first from a request, then another to Friends where I compare the IDs from the first call and then a third call that passes the array of friends and find all of them in the Users table. This already feels like overkill and I think that with associations, there has to be a better way.
Modification of the tables unfortunately is not an option.
One thing that I saw from "http://docs.sequelizejs.com/manual/querying.html#relations---associations"
I tried but got an interesting error.. when trying to repurpose the code snippet in the link under Relations/Associations, I get "user is associated to friends multiple times. To identify the correct association, you must use the 'as' keyword to specify the alias of the association you want to include."
const userRecord = await User.findOne({
where: { id }
})
const friendsIDs = await Friends.findAll({
attributes: ["friend_id"],
where: {
user_id: userRecord.id
}
}).then(results => results.map(result => result.friend_id));
const Sequelize = require("sequelize");
const Op = Sequelize.Op;
return await User.findAll({
where: {
id: { [Op.in]: friendsIDs }
},
});
Above for my use case works. I'm just wondering if there are ways to cut down the number of calls to the db.
Turns out Sequelize handles this for you if you have the proper associations in place so yes, it was a one liner user.getFriends() for me.

Sequelize - how to get all Employees for all Locations

I'll preface this by saying I think my model associations may be incorrect
Basically what i'm trying to do is is return an array of all Employees for a company.
Get all locations that have the same companyId
With those locations, get all Profiles tied to the locationId.
Profiles are linked to Locations through Employees.
Below is my code.
The query:
Location.findAll({
where: { companyId: user.profile.companyId },
include: [
{
model: Employee
}
]
})
This generates the error "employee is not associated to location!".
My models:
Employee.belongsTo(Profile)
Employee.belongsTo(Location)
Profile.belongsTo(Company)
Location.belongsTo(Company)
Profile.belongsToMany(Location, {
through: Employee,
foreignKey: "profileId"
})
Location.belongsToMany(Profile, {
through: Employee,
foreignKey: "locationId"
})
EDIT:
Adding Location.hasMany(Employee) allows me to do the query however it still requires a for loop within another for loop to get the correct data structure needed.
const locations = await models.Location.findAll({
where: { companyId: user.profile.companyId },
include: [{ model: models.Profile }]
})
const response = []
locations.forEach(location => {
location.profiles.forEach(profile => {
response.push({ location, profile })
})
})
return response
The query below returns what exactly as is however its only for a single location. I need to run the same query but for multiple locations.
Employee.findAll({ where: { locationId }, include: [Profile, Location] })
You've specified that Location belongsToMany Locations, but you've failed to specify the other way around. You should specify that a Location hasMany Employees.
Specifying Employee.belongsTo(Location) allows you to include related Locations with Employees. Specifying Location.hasMany(Employee) allows you to include related Employees with Locations.
I was able to recreate your problem and fix it with this line.
Location.hasMany(Employee, {
//...
})

Strongloop Loopback Native SQL

I have the following situation in a loopback application:
I have a 'Person' class that extends from built in 'User'
I have students ('Person' class with Role 'student')
I have teachers ('Person' class with Role 'teacher')
I have classgroups (ClassGroup class related to Person class with hasManyAndBelongsTo)
Students and Teachers are added to classgroups in the same way. Teachers can have multiple classgroups, students not. If I want to see all the students that are in the same classgroups, as a teacher, I use a remote method on Person. In that method I use the following SQL (21 is the id of the teacher):
SELECT * FROM `Person`
JOIN `ClassGroupPerson`
ON `Person`.`id` = `ClassGroupPerson`.`personId`
JOIN `RoleMapping`
ON `RoleMapping`.`principalType` = "USER"
AND `RoleMapping`.`principalId` = `Person`.`id`
JOIN `Role`
ON `Role`.`name` = "student"
AND `Role`.`id` = `RoleMapping`.`roleId`
WHERE
`ClassGroupPerson`.`classGroupId` IN (
SELECT `classGroupId`
FROM `ClassGroupPerson`
WHERE `personId` = 21
)
In the documentation, it says, that using native Mysql is discouraged (http://docs.strongloop.com/display/public/LB/Executing+native+SQL).
How would I achieve the same result in 'Loopback-style'. I've tried with filters, include, hasManyTrough, ... -stuff but I'm not able to achieve the same result?
Extra question: the result from this are JSON objects, how can I convert those easily into Person objects?
EDIT
With the following code in my remote method, I'm getting close:
Person.find({
include: {
relation: 'classgroups',
scope: {
include: {
relation: 'people'
}
}
},
where: {
id: id
}
}, function(err, people){
cb(err, people);
});
I have as a result: my teacher, with his classgroups included, and with each classgroup, I have the connected persons (teachers and students). The problem that remains, is that the teacher is also included in the classrooms, so I should be able to filter the result by Role = 'student', and I don't see how to do that?