Rails: How do I map out this has_many, has_many association? - sql

I'm currently working on a game for a student project. I'm spinning in circles engineering the associations. Any guidance from the community would be greatly appreciated.
There are 3 models/classes: Games, Characters and Quotes.
Game:
has_many Characters (two historical characters with a bank of quotes, you will have to choose who said the quote)
has_many Quotes (through games)
Character:
has_many Games (to keep track of games a character has appeared in)
has_many Quotes
Quote:
belongs_to Character
class Game < ActiveRecord::Base
has_many :characters
# Each game will have exactly 2 characters
# #game.characters will return the two characters
has_many :quotes, through: :characters
end
class Characters < ActiveRecord::Base
has_many :games
# #character.games will return all games the character appeared in
has_many :quotes
end
class Quote < ActiveRecord::Base
belongs_to :character
end
These are the migrations I've created:
class CreateGames < ActiveRecord::Migration[5.1]
def change
create_table :games do |t|
t.text :game_state
end
end
end
class CreateCharacters < ActiveRecord::Migration[5.1]
def change
create_table :characters do |t|
t.string :name
t.string :title
end
end
end
class CreateQuotes < ActiveRecord::Migration[5.1]
def change
create_table :quotes do |t|
t.string :content
t.belongs_to :character, index: true
end
end
end
Goals:
instantiate a new game with two characters: #game =
Game.new(#character1, #character2)
#game.characters should return
the two characters.
#character1.games should return all games
the character appeared in.
#game.quotes should return all the
quotes from the two characters.
My first instinct is that I need a join table for the has_many has_many relationship and to keep track of the games. For example:.
class GamesPlayed < ApplicationRecord
belongs_to :character1
belongs_to :character2
belongs_to :game
end
Thanks in advance if you can offer me any guidance or suggestions.

The way you've written your associations, you haven't really fleshed out the many to many association between the Game & Character models. Basically you'll need a join table for that.
In it's simplest form, it will look something like:
class Game < ActiveRecord::Base
has_many :game_character_joins
has_many :characters, through: :game_character_joins
end
class Characters < ActiveRecord::Base
has_many :game_character_joins
has_many :games, through: :game_character_joins
end
class GameCharacterJoin < ActiveRecord::Base
belongs_to :game
belongs_to :character
end
The migrations for the join table would be:
class CreateGameCharacterJoin < ActiveRecord::Migration[5.1]
def change
create_table :game_character_joins do |t|
t.integer :game_id
t.integer :character_id
end
end
end
That's really the easier part of what I think you're asking.
Then, the association between Quote and Game is kind of odd looking to me. Is this because you need to get all the Quotes for a single Game? There are a lot of ways to model that--is the idea of an association between Quote and Game a matter of convenience? It seems to me that a quote is
requires both a character and a game,
owned by a character
is about a game
How you approach this really becomes a matter of preference. Depending on the importance of the associations, you can have a many to many between character & quote and another between game & quote, or you can just rely on additional data in the quote table to specify which game it's about.

To model a many-to-many relationship you can use either a has_and_belongs_to_many which would rely on a join table (no model), or a model that represents the relationship between the two models.
I wonder if you don't really have a many-to-many relationship though. Consider this instead:
class Game
belongs_to :player_one, class_name: 'Player'
belongs_to :player_two, class_name: 'Player'
scope :for_player, ->(player) { where(player_one: player).or(where(player_two: player)) }
def players
Player.where(id: [player_one_id, player_two_id])
end
end
class Player
def games
Game.for_player(self)
end
end
# In use:
#game = Game.find(1)
#game.players
#player = Player.find(1)
#player.games
#player.games.where(created_at: 1.week.ago..Date.today)
Note that both game.players and player.games return an ActiveRecord_Relation that you can use in additional scopes, you just don't have a has_many on the Player model.

Related

Rails news feed for comments

I updated the question because I combined following locations and users into one polymorphic model.
So I am trying to make a news feed for comments in my app.
I have a user model, a model for following locations and users. And then I have a model for comments.
How do I grab the comments from users that a user follows and comments from a location that a user follows and put these together?
I need this to be paginated as well as sorted by the created_at time stamp ( showing newest first).
Heres what my migrations look like
create_table :comments do |t|
t.text :text
t.integer :user_id
t.integer :commentable_id
t.string :commentable_type
create_table :follows do |t|
t.integer :user_id
t.integer :followable_id
t.string :followable_type
And this is what my models look like
class User < ActiveRecord::Base
has_many :comments, as: :commentable
end
class Comment < ActiveRecord::Base
belongs_to :commentable, polymorphic: true
end
update:
I found something called a CASE statement in sql (like an if statement) How do I perform an IF...THEN in an SQL SELECT?
I am still unsure of how to write this query but I think the CASE statement may help.
If I'm gathering your question correctly you might see something like this for a model:
class User < ActiveRecord::Base
has_many :comments, as: :commentable, dependent: :nullify
has_many :follows, dependent: :destroy
has_many :followed_users, class_name: 'Follow', conditions: {followable_type: 'User'}
has_many :followed_locations, class_name: 'Follow', conditions: {followable_type: 'Location'}
has_many :followers, as: :followable, dependent: :destroy
end
And in your controller you might see something like this:
# See all comments made by those you follow (and yourself)
# See all comments made on locations you follow
# See all comments made on users you follow
#comments = Comment.where(
'user_id in (:followed_user_ids) or (commentable_type = 'Location' and commentable_id in (:followed_location_ids)) or (commentable_type = 'User' and commentable_id in (:followed_user_ids))',
{
followed_user_ids: current_user.followed_users.pluck(:user_id) | [current_user.id],
followed_location_ids: current_user.followed_locations.pluck(:location_id)
}
).paginate(page: params[:page]).order(created_at: :desc)
As this gets more complex, and it will get much more complex, look into scopes and merging. Probably pulling this method out into a search class somewhere.

Rails Joining together multiple tables

So I am trying to create a news feed of sorts but I am unsure as to how to make the queries.
I have a user model, a model for followed locations and a model for followed users. And then I have a model for comments. I need to grab all the comments from users that a user follows and all comments from a location that a user follows and I have to put these together.
I'm not that familiar with how to do this in sql or rails. Can anyone link me to an article or the docs where I might find how to do this?
If you need more information just comment what else I should include because I was unsure what to include in the post.
The comments model looks like this and it is polymorphic and can be posted to locations and events
create_table :comments do |t|
t.text :text
t.integer :user_id
t.integer :commentable_id
t.string :commentable_type
And then there is two separate tables for following users and following locations
create_table :followed_locations do |t|
t.integer :user_id
t.integer :location_id
create_table :followed_users do |t|
t.integer :user_id
t.integer :followed_id
Here's how the model associations would look:
class User < ActiveRecord::Base
has_many :comments, as: :commentable
has_many :followed_locations
has_many :followed_users
def followed_items
followed_locations.map(&:location).flatten + followed_users.map(&:followed).flatten
end
end
class Location < ActiveRecord::Base
has_many :comments, as: :commentable
end
class FollowedUser < ActiveRecord::Base
belongs_to :user
belongs_to :followed, class_name: 'User'
end
class FollowedLocation < ActiveRecord::Base
belongs_to :user
belongs_to :location
end
class Comment < ActiveRecord::Base
belongs_to :commentable, polymorphic: true
end
The code above defines the relationships among all the models, and adds one User instance method to collect all the items (locations or users) that a given user follows. Now you can gather all the comments for users/locations that a single user is following, like so:
user.followed_items.map(&:comments).flatten
This will gather all the followed items for a user (both locations and other users), get a list of all their comments, then flatten them into a simple array. If you want to sort them, my creation for example, tack that onto the end:
user.followed_items.map(&:comments).flatten.sort_by(&:created_at)
There are ways to optimize this, but at this point you probably just want to focus on getting the concepts down.
UPDATE:
I've created a simple Rails 4 app that implements this solution, and published it on github:
https://github.com/rubycuts/so26169791

How can I query a has_many through relationship which contains an additional attribute in the join table?

Situation
In my application a user can create a plan. Once the plan is created, the user can define the stakeholders/team members of the plan. Each team member becomes a responsibility assigned. There are many plans and users can be stakeholders of multiple plans and in each plan they have a different responsibility.
Example
Admin creates a plan and assigns 10 users as stakeholders. 1 is accountable, 2 are responsible, 7 just need to be informed
What I did so far
I set up a has_many through relationship between two models:
class User < ActiveRecord::Base
has_many :assignments
has_many :plans, through: :assignments
end
class Plan < ActiveRecord::Base
has_many :assignments
has_many :users, through: :assignments
end
The assignment table looks like this:
create_table :assignments do |t|
t.belongs_to :user
t.belongs_to :plan
t.string :responsibility
end
add_index :assignments, [:user_id, :plan_id]
the column responsibility contains one of 4 different values (responsible, accountable, informed, consulted.)
What I am looking for
I know how I can query all users that have been assigned to the plan (#plan.users.to_a) but I do not know how I can additionally supplement the user information with the responsibility they have in this plan.
The query I need is something along the lines of:
Select users which belong to plan X by looking at the assignment table. Do not just use the assignment table to identify the user, but also take the value from the responsibility column in the assignment table and return an array which contains:
user.first_name
user.last_name
user.responsibility (for this specific plan)
We had this exact requirement, and solved it in 2 ways:
Use an SQL Alias Column
The first way is to use an SQL Alias Column & append it to your has_many association, like this:
Class User < ActiveRecord::Base
has_many :assignments
has_many :plans, -> { select("#{User.table_name}.*, #{Plan.table_name}.responsibility AS responsibility") }, through: :assignments, dependent: :destroy
end
This will allow you to call #user.plans.first.responsibility, and will fail gracefully if no record exists
Use ActiveRecord Association Extensions
This is the best, but more complicated, way, as it uses the proxy_association object in memory (instead of performing another DB request). This script took us 2 weeks to create, so we're very proud of it!! Not tested with Rails 4.1:
#app/models/user.rb
Class User < ActiveRecord::Base
has_many :assignments
has_many :plans, through: :assignments, extend: Responsibility
end
#app/models/plan.rb
Class Plan < ActiveRecord::Base
attr_accessor :responsibility
end
#app/models/concerns/Responsibility.rb
module Responsibility
#Load
def load
captions.each do |caption|
proxy_association.target << responsibility
end
end
#Private
private
#Captions
def captions
return_array = []
through_collection.each_with_index do |through,i|
associate = through.send(reflection_name)
associate.assign_attributes({responsibility: items[i]}) if items[i].present?
return_array.concat Array.new(1).fill( associate )
end
return_array
end
#######################
# Variables #
#######################
#Association
def reflection_name
proxy_association.source_reflection.name
end
#Foreign Key
def through_source_key
proxy_association.reflection.source_reflection.foreign_key
end
#Primary Key
def through_primary_key
proxy_association.reflection.through_reflection.active_record_primary_key
end
#Through Name
def through_name
proxy_association.reflection.through_reflection.name
end
#Through
def through_collection
proxy_association.owner.send through_name
end
#Responsibilities
def items
through_collection.map(&:responsibility)
end
#Target
def target_collection
#load_target
proxy_association.target
end
end
Query the appointement table directly fitering for all the users in the current plan:
Appointement.select(:id).where(user_id: Plan.find(params[:id]).users.pluck(:user_id), plan_id: params[:id]).group(:id).having('count(*) = ?', Plan.find(params[:id]).users.count)

Handling relationship with ActiveRecord

I have two models to make a relationship between them, where I need to access stores of a radar and the radars of a store. A radar could monitoring zero or many stores. A store could belong to zero, one or many radars.
I would like to have something like this:
store = Store.first
store.radars #all radars of the store location
And the opposite too:
radar = Radar.first
radar.stores #all stores of the radar location
My classes:
class Store < ActiveRecord::Base
attr_accessible :title, :description, :user, :store_group, :city,
:neighborhood, :sublocality, :post_code, :route,
:street_number, :latitude, :longitude
end
class Radar < ActiveRecord::Base
attr_accessible :name, :radius, :latitude, :longitude, :user
end
How can I create a migration to handle this?
What you are looking for is a has_and_belongs_to_many association between radars and stores. The question you need to ask your self is will there ever be any attributes on the the joining between the two models? If so you might considering using an explicit join model, that will hold those attributes. In that case you would be looking at a has_many :through association.
see http://guides.rubyonrails.org/association_basics.html#the-has-and-belongs-to-many-association for information on the HABTM association.
your migration for a HABTM would be something like this.
class CreateRadarStores < ActiveRecord::Migration
create_table :radars_stores, :id => false do |t|
t.belongs_to :radar
t.belongs_to :store
end
end
The order of the table name is important, since by default rails creates it in alphabetical order of the models.
Your models would need to be updated to include the HABTM
class Store < ActiveRecord::Base
has_and_belongs_to_many :radars
....
end
class Radar < ActiveRecord::Base
has_and_belongs_to_many :stores
....
end
or if using a has many :through look here http://guides.rubyonrails.org/association_basics.html#the-has-many-through-association
Building that join model would be up to you depending upon attributes reuqired.

Rails basic association

I'm trying to do a basic model association in rails.
Basically I have a List table which stores item_id and user_id.
One user can create multiple "list-items."
Is this the correct way to do it?
Thanks.
class Item < ActiveRecord::Base
has_many :users, :through => :lists
end
class User < ActiveRecord::Base
has_many :items, :through => :lists
end
class List < ActiveRecord::Base
belongs_to :user
belongs_to :item
end
Depending on what you want to reach, your solution is the right one (or not). I see the following cases:
You want to create an n:m association between items and users. So each item could be referenced by many users, and each user references many items. If this is the right context, then your solution is the right one. See the Rails Guides: Associations for more information on that.
An alternative for that situation could be to use the has_and_belongs_to_many Association. The situation is the same, but it does not make sense to talk about lists, there will be no model object for it.
If each users may have many lists, and each list may have many items, your solution would be wrong. This would be no n:m association with list as the join table in between, but two 1:n relations.
The code for the third example would look like that:
class User < ActiveRecord::Base
has_many :items, :through => :lists
has_many :lists
end
class List < ActiveRecord::Base
has_many :items
belongs_to :user
end
class Item < ActiveRecord::Base
belongs_to :list
end
In the first solution, you should add the relations for users to lists and items to list:
class Item < ActiveRecord::Base
has_many :lists
has_many :users, :through => :lists
end
class User < ActiveRecord::Base
has_many :lists
has_many :items, :through => :lists
end
If the "list" entity truly is a pure association/join, that is, it has no inherent attributes of its own, then you can simplify a bit and use has_and_belongs_to_many. Then you don't need a "List" class.
class Item < ActiveRecord::Base
has_and_belongs_to_many :users
end
class User < ActiveRecord::Base
has_and_belongs_to_many :items
end
Rails will look for the references in a "items_users" table, so in your migration, you need to create it a la:
create_table :items_users, :id => false do |t|
t.references :users, :items
end
Many people will tell you to always use has_many :through, but others (like me) will disagree - use the right tool for job.