ActiveRecord: searchable concern - ruby-on-rails

Problem
Suppose I have two models, both of which have a name field:
class Student < ActiveRecord::Base
end
class Teacher < ActiveRecord::Base
end
Now I want to find all the students and teachers who has the name 'Joe'. What I think to do:
name = 'Joe'
Student.where(name: name) + Teacher.where(name: name)
But things get ugly when I add other type of models which has also a name field and I want to search them:
name = 'Joe'
Student.where(name: name) + Teacher.where(name: name) + Manager.where(name: name) + ...
Question
Is there a better way to do this? Ideally what I would like to do, define a module Person and then just:
Person.where(name: name)
Note: Changing the schema is not possible for other reasons.

The solution may be to define your data models in a way that will lend themselves to this type of query. If you truly need to work on all "people" by name perhaps you should have a Person data model with roles or an attribute to define what "type" of person they are.

You're on the right track when you talk about defining a Person module. Look into database normalization to get an idea of how best to define your models.
Since teachers and students are both people, but have different roles, you should store both of them in the same model, but add a field which allows you to select just the teachers or just the students. Something like this (in pseudo-code):
Person
:name (string)
:role (string)
With this, you can write queries like "find all people named John who have the role of teacher", or "find all people named Sue with any role".
With this kind of model, you'd need to enforce values on that role field so that people couldn't type anything they wanted - otherwise, you'd end up with some records that say 'student' and others that say 'classmate', for example. (You could take that a step further and store roles within a separate table, but that's another discussion!)

You have to use inheritance with rails, you do it like this:
class Person < ActiveRecord::Base
# shared person methods
end
class Student < Person
end
class Teacher < Person
end
You have to also have a table named persons with a column named type(string).
After this your students and teachers are going to be in persons table (you can delete original tables).
And you can query all students and persons with Person.where(name: name) and will include all students and teachers. And you can do Student.all to get only students, like a regular model.

Related

For a Rails and GraphQL project, how do you define multiple queries with the same name?

Here's some context: I'm using Rails 5.1.4, Ruby 2.6.1, and GraphQL 1.9. I have my Rails project currently configured as a REST api only project. However, I'm starting to convert it to using GraphQL. After running the graphql related generators, I currently have a graphql_controller.rb file and a query_type.rb file.
For my database model, I have a SchoolCourse model which describes a particular course taught in college. For this model, I've defined a school_course_type.rb. In my query_type.rb file, I've defined a field for the schoolCourse query.
However, there are 2 types of users who can log into my application - teachers and students. If the user is a teacher, then accessing the application and querying for schoolCourse will return all the school courses that this particular instructor teachers. If the user is a student, then accessing the application and querying for schoolCourse will return all the school courses that this particular student attends. In both cases, the schoolCourse data is the same type (school_course_type).
I currently have the schoolCourse query defined in query_type.rb to work for teachers. However, if I want to extend the schoolCourse query to work for students as well, it seems like I'll have to update the schoolCourse query to include if / else logic to look something like this:
# This is how it currently looks like:
def schoolCourses
teacher_user = context[:current_user]
teacher_user.school_courses
end
# This is how I think it would change:
def schoolCourses
user = context[:current_user]
if user.type == 'teacher'
user.school_courses
else
# different way for students to retrieve school courses
end
end
While this may work, this isn't ideal. The main problem I want to solve is to have multiple schoolCourse queries defined and have it so that teachers use one version and students use the other version.
In my current application, I have 2 schoolCourse controllers - one for teachers and one for students, and by splitting it into two files, my API is much cleaner. Is there any way to do something similar with the query_type.rb file where I have a query_type for teachers and another for students? How have you dealt with this problem in your projects?
Adding more context - 9/20/2019 2:10pm PST
How are school courses differentiated between student and teacher? So school course objects are identical for both students and teachers. Currently in my RESTful backend, I have 2 separate controller files for getting student courses. One is for teachers and the other is for students. When the teacher's school course controller gets called, it's only returning the school courses for the logged in teacher. Similarly, when the student's school course controller gets called, it's only returning the school courses for the logged in student.
Can you elaborate on why you want to have multiple school course queries? The reason why I'm thinking I want multiple school course queries is because I don't want to have to add if / else logic inside the single school course query to do something for teachers and something else for students. I know it'll work but I probably have 50 other models and I don't have to put the same type of if / else logic inside each query.
Need more information on how school courses are differentiated between student and teacher. Can you elaborate on why you want to have multiple school course queries?
You can define using Interface:
Types will become something like following:
module Types
module Output
StudentCourse = GraphQL::ObjectType.define do
name "StudentCourse"
interfaces [CourseInterface]
field :student_specific_field, Types::Output::SpecificField
end
end
end
module Types
module Output
TeacherCourse = GraphQL::ObjectType.define do
name "TeacherCourse"
interfaces [CourseInterface]
field :teacher_specific_field, Types::Output::SpecificField
end
end
end
module Types
module Output
CourseInterface = GraphQL::InterfaceType.define do
name "Course"
resolve_type lambda { |context, obj, _|
case context[:user].type?
when student
Types::Output::StudentCourse
when teacher
Types::Output::TeacherCourse
else
raise Types::CantResolveType, "can't resolve type: #{obj.class}"
end
}
field :id, !types.ID
end
end
end

Search field in rails 5

I have an index page with users list where I want to implement a search field, and filtering records based on the text field of users
scope :filter_users ->(params[:search]) { where("name like %#{params[:search]}%)
where i also need to check the the associated object name. I.e user belongs to organization, I need to add condition at scope to check the organisation name.
Your question was missing this, but I'll suppose your model is something like:
class User
belongs_to :organization
end
class Organization
has_many :users
end
To setup a search on the relation, you'll first need to join to the organization table onto users and search on the result of the join, filtering both on the user name and the organization name. It would look something like:
class User
belongs_to :organization
# I took the liberty of fixing your simple search syntax
scope :simple_search, ->(query) { where('name like ?', "%#{query}%") }
# The complex search:
# 1. Inner joins users table to organizations table
# 2. Applies a where conditions to the result of the join (note: we need to
# specify the table name in the where because both models have a name field)
scope :complex_search, ->(query) { joins(:organization).where('users.name LIKE :q OR organizations.name LIKE :q', query:"%#{query}%")}
end
And used like this:
# returns only users whose name matches '%ben%'
User.simple_search('ben')
# returns users whose name matches '%ben%' and users belonging to
# companies whose name matches '%ben%'
User.complex_search('ben')
You can find an example of this (and interesting details about things I used in my answer, and lots of other things as well) in the official Active Record Query Interface guide, and for this particular matter, in the specifying conditions on the joined tables section.

Validate presence of associations in a many-to-many relation

In my Rails app I have a many-to-many relationship between 2 models Teacher and Course through a join table. I'd like to create some sort of validation where a course can't be created without being associated to at least one teacher (it is assumed that all teachers are in the database by the time we are adding a new course). This would be easy to do if this was a one-to-many relationship, but with a many-to-many relationship, we need to save the course before we can associate it with teachers.
My initial plan was to override Rails create method in the Course model to allow passing teacher_ids and validate presence of at least one teacher_id before saving the course, but I'm not sure this is a nice approach.
You should write custom validation, which is quite easy (please adapt to your code):
class Course < ActiveRecord::Base
has_and_belongs_to_many :teachers
validate :has_one_teacher_at_least
def has_one_teacher_at_least
if teachers.empty?
errors.add(:teachers, "need one teacher at least")
end
end
end
That way, you'll only be able to create courses if associated to one teacher like so:
teacher = Teacher.create()
course = Course.new()
course.teachers << teacher
course.save!

Adding a JOIN between two tables

Here is the classes I have:
Model Organization
has_many Students
Model Student
has_many Classes
belongs_to Organization
Model Class
a field named : price
belongs_to Student
scope :top_expensive_classes, joins(:students).order('price DESC')
Now I want to list the top 10 expensive classes
At least the first problem I have is that in the params I have the organization_id to filter based on that But I write my controller like this which does NOT work because it thinks it should find organization_id in the Class model but it is in the Student model.
#results = Class.top_expensive_classes.where(organization_id: params[:id]).limit(RESULT_SET_COUNT)
So I was wondering if there is a way to fix this? I think I should introduce a new join somewhere? but couldn't figure it out.
There's a typo in your scope: joins:(:programs) should be joins(:programs)
To fetch based on the organization id in Student you may be able to do this:
#results = Class.top_expensive_classes
.joins(student: :organization)
.where(organization: {id: params[:id]})

Structure movie lists

I'm trying to set up a proper database-design, but I'm stuck.
Here is what I'm trying to save.
Every user can define a vote history list from imdb looking like this.
Two users can define the same list.
First I want to be able to save each list as an imdb_vote_history_list - list.
class ImdbVoteHistoryList < ActiveRecord::Base
has_and_belongs_to_many :vote_history_list
has_and_belongs_to_many :movies
# Fields
# id (Integer) - defined by the user
end
Each list should be unique and is being defined by it's ID (given in the link).
Each list has and belongs to many movies, as in the code above.
Each user should be able to pick a name for every list.
So instead of saying
Each imdb_vote_history_list belongs_to user
I create a new relation called vote_history_list.
class VoteHistoryList < ActiveRecord::Base
has_and_belongs_to_many :imdb_vote_history_lists
belongs_to :user
# Fields
# name (String)
end
Here the user can pick any name for the list, without interference with other user's names.
Is this a good way to store the data?
From the theoretical database design view this is the right approach.
For example the entity relationship model describes it this way. You can have relationships between entities and attributes at those relationships. If you map those to a relational model (database tables) you get a table for the relationship containing references to both entites and all additional information.
This is what theory can tell us about it :)

Resources