Fastest way to return a list of records depending on many many-to-many relationships - sql

I'm developing an API in Rails in which exists users and messages tables. Also users have a gender (gender_id), belong to a country (country_id) and also have a civil status (civil_status_id) and the messages are created by admins.
So up to here I have this model.
Now I have to develop the following requirement
An admin should be able to create a message targeted to users depending on its attributes (country, gender or civil status). Also, the admin should be able to declare a message as a global message, in this case "all" users should receive it, but exceptions should also be allowed. For example, in the case where an admin want to send a message to users from all countries, except people from Russia and China.
The thing is I'm no Rails/SQL expert, but I want to make this efficiently so that if tomorrow the app has ten thousand or a hundred thousand users the server responds quickly.
So I was thinking the following
First create 3 many-to-many relationships (countries_messages, genders_messages and civil_statuses_messages). The record of these tables represent the relations between the messages and the countries, civil_statuses and genders.
Then create a form where an admin can create a message, where by means of several select boxes, he should be able to choose the attributes of the users to whom he wants to reach. The form for creating a message should also have a checkbox to determine if the message is global, if its marked then I would consider that the selected countries, genders and civil statuses would be the categories that the administrator wants to exclude, i.e. if an admin want to send a message to all the people in the system except for people who are from Canada he should mark the global option and select the country Canada in the select box (obviously this would be stated in the view).
Now up to here I have this model.
In what I do have doubts is which way is more efficient to return the messages that corresponds to a user.
Method 1
When an admin specifies that a message is global, except for those from country with id 3 then I could add to countries_messages records like (country_id: 1, message_id: 2), (country_id: 2, message_id: 2), (country_id: 4, message_id: 2), ..., etc. i.e. forming a relation with every country except the country with id 2.
Then retriveng the messages that the current user should read like the following:
global_messages = Message.where(global: true).ids
country_messages = current_user.country.messages.ids
gender_messages = current_user.gender.messages.ids
civil_status_messages = current_user.current_status.messages.ids
#messages = Message.find(global_messages + country_messages + gender_messages + civil_status_messages)
Method 2
Other way could be forming a relation of that message with the excluded country, i.e. if I make a message exclusively for people from country with id 2 then I should add the record (country_id: 2, message_id: 2) in countries_messages, but in the contrary case if I made a message to every country except the country with id 2 then I should also add the record (country_id: 2, message_id: 2) to countries_messages.
In this case I can know if a message is excluded for males and people from Argentina, for example, if the message is global AND it's associated with the country and gender record that represents Argentina and males.
Then the retriveng of the messages that the current user should read would be like this:
global_messages = Message.where(global: true).ids
country_messages = current_user.country.messages
gender_messages = current_user.gender.messages
civil_status_messages = current_user.current_status.messages
excluded_country_messages_ids = country_messages.where(global: true).ids
excluded_gender_messages_ids = gender_messages.where(global: true).ids
excluded_civil_status_messages_ids = civil_status_messages.where(global: true).ids
#messages = Message.find(global_messages + country_messages + gender_messages + civil_status_messages - excluded_country_messages_ids - excluded_gender_messages_ids - excluded_civil_status_messages_ids)
There could be more ways to do the same, so I want to receive recommendations or if you see that I could make improvements to do the same then tell me. If there is something you do not understand ask.

Depending on your database choice, you may want to consider storing the message "attributes" (country, gender, ...) in a jsonb column on your messages table. It could also include a user_ids attribute to simplify things. The queries might look a little funny, but you can move a lot into scopes to clear things up in your code and even add indexes to speed things up.
Here is a great article on using jsonb with indexes in Rails with postgresql.
Another alternative you could do is make a UserMessage join table with a polymorphic reference that could associate the different attributes to a message and user:
class UserMessage < ApplicationRecord
belongs_to :user
belongs_to :message
belongs_to :messageable_type # e.g. "Country"
belongs_to :messageable_id # e.g. some Country id
end

Related

Django: Annotate table with field across a M2M mapping to another table

I want to get competition.name from a list of submissions.
In my setup, competitions and teams share a M2M relationship (with an associated competition-team object. Each competition-team pair can submit any number of submissions. I now have a dashboard page which I am trying to create a table of all submissions by the team accompanied by the respective competition's name. The output should look like:
| Submission Name | Submission Date etc. | Competition Name |
| Sub01 | 2020-12-30 2000 | Competition01 |
I have trouble retrieving the competition name from the submissions. Here are my models:
class Competition(models.Model):
id = models.AutoField(primary_key=True)
name = models.CharField(max_length=30)
class CompetitionTeam(models.Model):
competition_id = models.ForeignKey('Competition', on_delete=models.CASCADE, to_field='id', db_column='competition_id')
team_id = models.ForeignKey('Team', on_delete=models.CASCADE, to_field='id', null=True, db_column='team_id')
class CompetitionSubmission(models.Model):
competitionteam_id = models.ForeignKey(CompetitionTeam, on_delete=models.CASCADE, db_column='competitionteam_id')
I wish to annotate a set of submissions with their respective competition names. I tried with:
submissions.annotate(competition_name=Subquery(Competition.objects.filter(id=Subquery(CompetitionTeam.objects.get(id=OuterRef('competitionteam_id')).competition_id)).values('name')))
"ValueError: This queryset contains a reference to an outer query and may only be used in a subquery."
I also tested with the following command:
CompetitionSubmission.objects.prefetch_related('competitionteam_id__competition_id')
It runs but the command seems to do nothing. I will update this post with other methods I try.
Thank you.
EDIT
submissions.annotate(competition_name=Subquery(Competition.objects.filter(id=Subquery(CompetitionTeam.objects.filter(id=OuterRef(OuterRef('competitionteam_id_id'))).values('competition_id'))).values('name')))
Seems to work correctly.
You can traverse ForeignKeys directly using double underscores.
CompetitionSubmission.objects.values(
'competitionteam_id',
'competitionteam_id__competition_id',
'competitionteam_id__competition_id__name',
'competitionteam_id__team_id',
'competitionteam_id__team_id__name',
)
This will only produce a single database query. Django ORM takes care of everything.
P.S. I would avoid using '_id' in field names as Django model fields are supposed to be referring to related objects themselves. Django automatically adds extra attributes with '_id' that contains the related object's id. Please see https://docs.djangoproject.com/en/3.1/ref/models/fields/#database-representation

Rails: Class method scoping on the properties of an associated model

This is a somewhat more complicated version of the question I asked previously.
Background:
So what I need is to display a list of articles. An article belongs to a media outlet. A media is located in a particular country and publishes articles in a particular language. So the data structure is as follows:
Article belongs to Media; Media has many Articles
Media belongs to a Country; Country has many Media
Media belongs to a Language; Language has many Media
Now, if I wanted to filter articles by media, I could use the following class method (I prefer class methods over scopes, because I am passing a parameter and am using a conditional statement inside the method):
def self.filter_by_media(parameter)
if parameter == "all"
all
else
where(media_id: parameter)
end
end
Question:
How to write a class method that would filter Articles based by properties of its associated model, the Media? For example, I want to get a list of articles published by media located a certain counrty or in several countries (there is also a default country when the user does not make any choice). Here’s what I tried:
# parameter can be either string 'default' or an array of id’s
def self.filter_by_country(parameter)
if parameter == "default"
joins(:media).where(media: [country_id: 1])
else
joins(:media).where(media: [country_id: parameter])
end
end
But that doesn’t work, and I am not conversant enough with SQL to figure out how to make this work. Could you please help?
Update:
I’m trying out #carlosramireziii's suggestion. I changed arrays into hashes (don't know what possessed me to use arrays in the first place), but I’m getting the following error in the Rails console (to avoid confusion, in my database, media is called agency):
def self.filter_by_country(parameter)
if parameter == "default"
joins(:agency).where(agency: {country_id: 1})
else
joins(:agency).where(agency: {country_id: parameter})
end
end
in Rails console:
> Article.filter_by_country('default')
=> Article Load (1.9ms) SELECT "articles".* FROM "articles" INNER JOIN "agencies" ON "agencies"."id" = "articles"."agency_id" WHERE "agency"."country_id" = 1
PG::UndefinedTable: ERROR: missing FROM-clause entry for table "agency"
LINE 1: ...ON "agencies"."id" = "articles"."agency_id" WHERE "agency"."...
^
: SELECT "articles".* FROM "articles" INNER JOIN "agencies" ON "agencies"."id" = "articles"."agency_id" WHERE "agency"."country_id" = 1
Update 2
My mistake in the Update section above is that I did not pluralize agency in the where clause. The part where(agency: {country_id: 1}) should have read where(agencies: {country_id: 1}). The pluralized word agencies here refers to the name of the table that is being joined.
You are very close, you just need to use a nested hash instead of an array.
Try this
def self.filter_by_country(parameter)
if parameter == "default"
joins(:media).where(media: { country_id: 1 })
else
joins(:media).where(media: { country_id: parameter })
end
end

Designing a rest url - filter with multiple params over entities (not last entity)

Let's say I have the following entities in my libraries app - Library Room, shelf, Book.
Where Room has N shelves, and shelves have N Books.
Now the following url brings me a list of books whose
library is 3, room no. is 5 and shelf no. is 43.
.../library/3/room/5/shelf/43/books
Assuming shelf 43 is unique per room only
(There is shelf 43 also in other rooms)
and Rooms are not unique (There's a few room no. 5 ) in the library.
Here is my questions:
I want to filter with more fields on the entities, here is what i want to do
(representation not in rest):
.../library/id=3&type=3/room/decade=21&topic=horror/shelf/location=east&/books
This is not rest.
How do I represent it in rest?
Notes:
I don't want to do this way
.../books&param1=X&param2=X&param3=X&param4=X
because not all params are related to books.
Couple of things that you need to look into while designing your apis.
1) are type, decade, topic etc required fields? if so, I will probably make them a part of the path itself, such as:
../libraries/{libraryId}/type/{typeId}/rooms/{roomId}/decades/{decadeId}/topics/{topicName}/shelves/{shelfId}/locations/{shelfLocation}/books
Here I am assuming that each library can have rooms which have unique room ids per library, each room can have shelves which has unique ids/locations per room (and so on and so forth). Yes, the url is pretty long, but that's kind of expected
2) if these fields are not required, you could use a different approach which is a bit less verbose but a bit more confusing for client developers who have never used such approach here. Here's a straight up example Restful Java with JAX-RS by Bill Burke
#Path("{first}-{last}")
#GET
#Produces("application/xml")
public StreamingOutput getCustomer(#PathParam("first") String firstName,
#PathParam("last") String lastName) {
...
}
Here, we have the URI path parameters {first} and {last}. If our HTTP request is
GET /customers/bill-burke, bill will be injected into the firstName parameter and
burke will be injected into the lastName parameter.
If we follow this somewhat academic approach (I have not seen this implemented on many platforms. Most platforms normally go with approach # 1, a more verbose but clear approach), your URL would look somewhat like this:
../libraries/{libraryId}-{typeId}/rooms/{roomId}-{decadeId}-{topicName}/shelves/{shelfId}-{shelfLocation}/books
This way, if the client developer doesn't pass in the non-required fields, you can handle it at the business logic level and assign these variables a default value, for example:
../libraries/3-/rooms/2-1-horror/shelves/1-/books
With this url, libraryId = 3, typeId = null (thus can be defaulted to it's default value) and so on and so forth. Remember that if libraryId is required field, then you might want to actually make it a part of the pathparam itself
Hope this helps!

Database Design + Rails associations with :through using metadata

Here is the scenario: I am building a system that will let users search for each other based on their skill sets.
Users have skills. Skills are universal objects and are shared amongst users.
Users have the following metadata associated with skills: level and interest
Each user can create their own categories under which they can organize their skills (e.g. one user might have the skill "Saas" under "Front-End Development" and another under "Web Development")
I have 4 tables: Users, Skills, Categories & Skillsets
Assuming that John Doe (username: "johndoe") has the following categories and skill set:
Category: Front-End
Skills: HAML, SASS/SCSS, CoffeeScript, Javascript, jQuery
Category: Back-End
Skills: Ruby, Ruby on Rails, node.js
I'd like to be able to perform the following operations:
user = User.where(:username => "johndoe").first
user.categories
# returns a list of the user's categories
user.skills
# returns a list of the user's skills
user.category.where(name => "Front-End").first.skills
# => returns list of skills in the "Front-End" category
user.skills.where(:name => "HAML").first.category
# returns "Front-End" category
# adds a skill without assigning it to a category
user.skills << skill_object
user.skills.last.level = 9
user.skills.last.interest = 6
user.skills.last.save
# adds a skill while assigning it to a category
user.category.skills << skill_object
user.category.skills.last.level = 9
user.category.skills.last.interest = 6
user.category.skills.last.save
And in order to find users by skill:
skill = Skill.where(:name => "Javascript").first
skill.users
# returns users possessing skill
I've been playing around with my models for a while but not quite getting to behave as I'd like them to. I need a fresh perspective - Any pointers / suggestions?
I know nothing about Ruby, but from a relational db perspective, I'd probably just create a special, hardcoded, category field for each user: Uncategorized.
Then, when you add skills, they either go in a specified cateogory, or the default uncategorized one for that user. Outside of that one "special" record for each user, everything else would work perfectly no matter which skill or category you're looking at, and you can leave all your constraints in place without having to worry about null values.
UserID, User
1, me
CategoryID, UserID, Category
1,1,Uncategorized
2,1,Web GUI
SkillID, Skill
1,Javascript
2,Knitting
SkillID, CategoryID, Level, Interest
1,2,10,75
2,1,50,1
Depending on how you use Level and Interest, I'd also consider making them lookup tables to give meaning to the values from a database perspective (and help populate drop down lists).
etc.

Creating a good search solution

I have an app where users have a role,a username,faculty and so on.When I'm looking for a list of users by their role or faculty or anything they have in common I can call (among others possible)
#users = User.find_by_role(params[:role]) #or
#users = User.find_by_shift(params[:shift])
So it keeps the system
Class.find_by_property
So the question is: What if at different points users lists should be generated based on different properties.I mean: I'm passing from different links
params[:role] or
params[:faculty] or
params[:department]
to my list action in my users controller.As I see it all has to be in that action,but which parameter should the search be made by?
Try https://github.com/ernie/meta_search if you're on Rails 3