Here is my STI Models:
class User < ActiveRecord::Base
end
class Athlete < User
has_many :sports, :through => :user_sports
has_many :user_sports
end
class Coach < User
end
The UserSports table has user_id and sport_id... but then you run this:
athlete = Athlete.all.last
athlete.sports
The SQL that is generated is trying to use the athlete_id instead of the user_id... not too sure what I am doing wrong here... any suggestions would be greatful!
I'm not sure why you have a UserSports table. You can just use a foreign key for either User or Sport, depending on their relation to each other.
The User model would need a specified relation to the Sport model, and vice versa.
More information on that is here: http://guides.rubyonrails.org/association_basics.html#the-has_many-association
It makes sense that it's trying to pull the athlete_id instead of the user_id, since you are calling on an Athlete object.
As a side note: There is no need to write Athlete.all.last - you only need to write Athlete.last.
Related
Alrigth, so I'm not sure if this isn't too specific... But I really don't have a clue how to construct such a query, neither in AR, nor in SQL. So here's the situation:
I have a User model. User has_many Projects. A Project, in turn, have a following associations:
Project has_one BasicEvent
Project had_many AdditionalEvents
BasicEvent and AdditionalEvent classes are built on inheritance from a AR model class, Event
Now, the goal is this: on my view, I need:
access to all Projects count per User
access to all Events count, where happened_at attribute in nil
have all the Users sorted, by the count of Events, where happened_at attribute in nil
I made a couple of attempts at it so far, but didn't really get too far... I will appreciate any help with this complicated (at least from my perspective) query.
You can start from code like:
# -*- frozen-string-literal: true -*-
class UserStatService
SELECT_CLAUSE = <<~SQL
(SELECT count(1) FROM projects WHERE user_id=users.id) AS projects_count,
(SELECT count(1) FROM events WHERE (happened_at IS NULL)
AND (project_id IN (SELECT id FROM projects WHERE user_id=users.id)
) AS events_count,
users.*
SQL
def call
User
.select( SELECT_CLAUSE )
.order( 'events_count' )
end
end
Each User instance in resultset will have projects_count and events_count attributes.
Also any kind of cache (counter_cache or handmade) is recommended. Because the query is not expected to be fast.
Projects count per User: Project.group(:user_id).count
Events count, where happened_at attribute in nil: Event.where(happened_at: nil).count
Users sorted, by the count of Events, where happened_at attribute in nil: This is the trickiest but it's also not particularly difficult:
User.
select("users.*, (SELECT COUNT(*) FROM events WHERE user_id = users.id AND happened_at IS NULL) AS unhappened_events_count").
order("unhappened_events_count DESC")
This is not a particularly efficient query but it should do its job well for several thousand records (easily) – especially if you set an index on user_id and happened_at in the events table.
Assuming basic_events and additional_events have essentially the same columns, a great solution to this problem would be to change your database/app to use single table inheritance.
Assuming you're using Rails 5.x your models would look like this:
class User < ApplicationRecord
has_many :projects
has_many :basic_events, through: :projects
has_many :additional_events, through: :projects
end
class Project < ApplicationRecord
belongs_to :user
has_one :basic_event
has_many :additional_events
end
class Event < ApplicationRecord
belongs_to :project
end
class BasicEvent < Event
belongs_to :project
end
class AdditionalEvent < Event
belongs_to :project
end
This way, there are only three tables involved users, projects and events. Your events table will have a :type column where you can specify if it's basic or additional.
For your queries:
access to all Projects count per User
User
.select("users.*, count(projects.id) AS projects_count")
.joins(:projects)
.group('users.id')
.order('projects_count DESC')
Using select like this give you access to a :projects_count method on each User object returned in this active record relation, so if you assigned this query to a variable called users_with_projects_count you could do users_with_projects_count.first.projects_count and it would return the number of projects associated with that user.
access to all Events count, where happened_at attribute in nil
User
.select("users.*, count(events.id) AS events_count")
.joins(:events)
.where('events.happened_at IS NULL')
.group('users.id')
You can access :events_count the same way you did :projects_count in the last example.
have all the Users sorted, by the count of Events, where happened_at attribute in nil
User
.select("users.*, count(events.id) AS events_count")
.joins(:events)
.where('events.happened_at IS NULL')
.group('users.id')
.order('events_count DESC')
You use the same query as the last example just add an :order.
Consider this setup. Please understand that our setup is much more detailed but this is a simple example.
competition which has name. This is an annual competition.
competition_instances which has location, starts_at.
Each competition has sports which has name.
Example:
competition.name: "Super Bowl" has different competition_instances every year but sport remains the same.
Conversely, competition.name: "Olympics" has different competition_instances and different sports in each competition_instance.
Would it be best to create competition_sports and competition_instance_sports with competition_instance_sports as a subclass of competition_sports?
GOAL: Use competition_instance_sports records if they exist, otherwise use the competition_sports record. In our real world app, each competition/competition_instance can have 20-50 sport records. How can we best achieve?
Based on what I understand from the question I cannot see where STI will be helpful in this situation. However a join table will get you where you want.
I suggest creating a new table sports, this model will have all the specific details of each sport. The competition_instance.rb will have one/many sport.rb. competiton.rb will have many sports through competition_instance.rb.
competition.rb
Class Competition < ActiveRecord::Base
has_many :competition_instances
has_many :sports, through: :competition_instances
end
competition_instance.rb
Class CompetitionInstance < ActiveRecord::Base
belongs_to :competition
belongs_to :sport
end
sport.rb
Class Sport < ActiveRecord::Base
has_many :competition_instances
end
By using this design you will be able to achieve the following:
1- You will have your predefined sports in your database along with their specific properties.
2- Each competition will have .sports which will give all the sports in this competition for the olympics case.
3- You will be able to set specific properties for each competition instance (example event_start_time and event_end_time) in the competition instance table.
I'm just thinking of the case where there are standard sports which are always in the Olympics and some which are added on, such as those proposed by the host country.
I would use Polymorphic Associations, in a "reverse manner".
class Competition < ActiveRecord::Base
has_many :competition_instances
has_many :competition_sports, as: :event
end
class CompetitionInstance < ActiveRecord::Base
belongs_to :competition
has_many :competition_sports, as: :event
def events_array # neater by sacrificing ActiveRecord methods
competition.competition_sports + competition_sports
end
def events # messier, returns ActiveRecord relationship
CompetitionSport.where( " ( event_id = ? AND event_type = 'Competition' ) OR
( event_id = ? AND event_type = 'CompetitionInstance')", competition_id, id )
end
end
class Sport < ActiveRecord::Base
has_many :events, as: :competition_sport
end
class CompetitionSport < ActiveRecord::Base
belongs_to :sport
belongs_to :event, polymorphic: true
end
This allows:
competition.competition_sports # standard sports
competition_instance.competition_sports # only those specific for this instance
competition_instance.events # includes sports from both
Aiming at the original question of "Is this a use case for STI", it will be difficult to say if this is or not without seeing the full complexity of your environment. But here's are some things to consider:
Abridged from How (and When) to Use Single Table Inheritance in Rails - eugenius blog:
STI should be considered when dealing with model classes that share much of the same functionality and data fields, but you want more granular control over extending or adding to each class individually. Rather than duplicate code or forego the individual functionalities, STI permits you to use keep your data in a single table while writing specialized functionality.
You've simplified your example, but it sounds like competition and competition_instance are NOT essentially the same object, with minor behavioral differences. With what you've described, I would probably rename these objects to event and competition, or whatever makes better sense to you in terms of illustrating what these objects are actually representing. I think of 'The Olympics' as a generic event, and '100m Dash' as a competition taking place at The Olympics. Or "The SuperBowl" only hosted one competition in 2014, "SuperBowl XLIX".
You've also tagged this question with database-normalization, which STI won't help. If the attributes differ slightly between your shared objects you'll end up with null fields everywhere.
I refer you to the other answers here to see how you should probably set those objects up to behave as desired.
This is not a case for single table inheritance, because competition_instance is not a substitute for competition and 1 competition can have many competition_instances. So you have 3 tables:
competitions
sports
competition_instances
competition_instances has a foreign key to competitions because 1 competition can have many competition_instances but each competition_instance has exactly one competition.
Whether you attach sports to competitions or competition_instances depends on the specific constraints of your use case. I don't exactly know what you mean by "each competition/competition_instance can have 20-50 sport records". I would expect each competition_instance to have exactly one sport, so you might leave it at that, or you might attach a collection of sports to a competition as well, so that you can retrieve new competitions by sport before there is a competition_instance. I'd need more details on your use case to give you further advice.
I have the following models:
Student has_many :subjects, :through => :classes
Subject has_many :students, :through => :classes
Class belongs_to :subject
belongs_to :student
The model class has an extra attribute (among the foreign keys to subject and students table) called level.
Basically I want to be able to have a form that will let the student to choose a subject and relate that subject to its record. So, I have this:
ClassesController < ApplicationController
def new
#list_of_subjects = Subject.all
# What should I do here?
end
My question is: How should I create the object for the form? From which model it should be, subject, student or class? I want to be able to create a record in the class table that would relate the student and the subject that the student has chosen, but I don't know if I am doing it wrong.
Thanks
I didn't think you could create a model called Class since it's a keyword, but that's neither here nor there...
First I think your controller and view should be using Student since it's the student that's selecting these things. Next, I think what you want to do is to add accepts_nested_attributes_for :class in your Student model which allows you to create an instance of the Class connector model from Student.
What you're trying to do sounds a little like something I tried to do. I have my full code there.
Using nested attributes to easily select associations in a form
I later refined it a bit in this question too to make the code less hideous:
Rails: How do I prepend or insert an association with build?
I know it's late, but I hope that helps.
I made a relationship with the three models using has_many :through:
class Curriculum class < ActiveRecord::Base
has_many :interests
has_many :vacancies,: through => :interests
end
class Vacancy class < ActiveRecord::Base
has_many :interests
has_many :resumes,: through => :interests
end
class Interest < ActiveRecord:: Base
belongs_to :vacancy
belongs_to :curriculum
end
And to create curriculum and vacancy, I create them by administrative, i need to know how can i create the interest to the id of the vacancy, and how it will be logged on the system I have to get the id of it and make the relationship in creating a new bank interest. I wonder how I can program it to do so, and I wonder how the controller will get the create action, and what better way to do this.
First, try to read the whole "Guide to Rails on Associations", especially the part about has_many :through. Then check your schema if your db is migrated and contains for the table interests the necessary foreign keys to curriculums and vacancies called curriculum_id and vacancy_id.
If that is all in place, the following code will create the relationship between two objects:
#curr = Curriculum.find(1)
#vac = Vacancy.find(1)
#curr.interests << #vac
#curr.save
The last two lines creates an interest between #curr and #vac and store that on the database. So you should not use IDs and handle them directly, but work with objects instead.
The second part now is to provide a UI to allow the definition (and removal) of interests between curricula and vacancies. The base flow here is:
You have one curriculum in focus.
You have a link to add / remove curricula.
The view that opens shows a list of possible vacancies, where every vacancy has a checkbox.
By selecting (or deselecting) the check boxes, the IDs of the vacancies will be held in the params of the request sent to the controller.
See the (older) podcast Railscast #52 how to do that in a similar context. Or see the example for has_many :through with checkboxes.
An alternative way would be to use JQuery autocomplete, and add so interests one-by-one. See the nice podcast Railscast #258 which uses JQuery Tokeninput for that.
I think this is what your looking for:
HABTM Checkboxes
That's the best way to use an Has and Belongs to many association.
For example, one user has joined many group; and one group has many user members. Now I get a user object and a group object, I want to know whether this user is a member of this group.
I can see some methods, but still wandering whether there is a better way?
So, if i understand your question, what you have is something like:
class User < ActiveRecord::Base
has_many :groups, :though => :user_members
class Group < ActiveRecord::Base
has_many :users, :though => :user_members
And you want to know if a user is a member of a specific group.
So, given something like:
u = User.first
g = Group.first
Just do:
u.groups.include? g
Thats all there is to it!
ian.
ipd's way is fine, another more Rails oriented way to do it is :
u.groups.exists?(g)