Rails Joining together multiple tables - sql

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

Related

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

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.

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.

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.

Ruby data modellng has_many relationship two way

I've got a question about a certain data model in Ruby. I've got a set of business requirements:
User can create many Workshops
User can attend many workshops
Workshop has one owner (user)
Workshop has many attendees (users)
The first part of this relationship is easy to setup:
#user.rb
class User < ActiveRecord::Base
has_many :workshops
end
#workshop.rb
class Workshop < ActiveRecord::Base
belongs_to :user
end
But how will i make the "other has_many" relationship from workshops to Users. Can i do something like a workshop belongs_to :user, :as :owner. and a workshop has_many :users, :as :attendees?
What are your thoughts about this? To make it worse a Workshop has a attendee limit so i need validations...
Thanks,
Daniel
You have a has_many to has_many relationship, so you'll need to create a new associative table to associate the relationships (lets call it attendances):
create a db migration:
rails g model attendance
Then in your migration do something like:
create_table :attendances do |t|
t.integer :attendee_id
t.integer :workshop_id
end
add_index :attendances, :attendee_id
add_index :attendances, :workshop_id
add_index :attendances, [:workshop_id, :attendee_id]
So now you have a table you where you can associate many attendees to many workshops.
Now in your user model:
has_many :attending, through: :attendances, foreign_key: 'attendee_id', class_name: 'Workshop', source: :workshop
In your workshop model:
has_many :attendees, through: :attendances, class_name: 'User', source: :attendee
So now 'some_user.attending' will return the ActiveRecord relation of all the workshops some_user is attending, and 'some_workshop.attendees' will give you all the users attending some_workshop.

referencing attributes in models with belongs_to relationships through a nested namespace

Ok, so I thought I understood how the relationship specifications work in rails but I've been struggling with this for a day now.
Some context, I have two models Cars & Model Names (e.g. Impala, Charger, etc), where Cars are instances of Model Names, and Model Names really is nothing more than a lookup table of Model Names and some other model level attributes. The Model Name controller is nested within the admin namespace as only admins can CRUD Model Names. End users can add instances of cars to the Cars model.
So, in routes.rb I have:
resources :cars
namespace :admin do resources :model_names end
The Model's are defined as:
class Admin::ModelName < ActiveRecord::Base
end
class Car < ActiveRecord::Base
belongs_to :admin_model_name
end
The Migrations are:
class CreateCars < ActiveRecord::Migration
def self.up
create_table :cars do |t|
t.string :chassis_number
t.string :description
t.references :admin_model_name
t.timestamps
end
end
class CreateAdminModelNames < ActiveRecord::Migration
def self.up
create_table :admin_model_names do |t|
t.string :model
t.integer :sort_index
#...additional attributes removed
t.timestamps
end
The admin CRUD of ModelName all work great. The problem is in the Car views. I think I should be referencing a particular cars model name like such:
<%= #car.admin_model_names.Model =>
But I get the error:
undefined method `admin_model_names' for #<Car:0x000001040e2478>
I've tried the attr_accessible on the ModelNames model but to no avail. The underlying data is referenced correctly. I have also have HABTMT relationship between Cars & Users and that all worked fine referencing each others attributes from the different entities views. But haven't been able to get this to work. Is it due to the nested resource & admin control inheritance?
The only referencing method I found that works is:
<%= Admin::ModelName.find(#car.admin_model_name_id).model %>
But this really seems to be too much code (and expense of a find) to get to that attribute. Is there a Rails way?
Thanks in advance.
Scott
Have you tried:
class Car < ActiveRecord::Base
belongs_to :admin_model_name, :class_name => "Admin::ModelName"
end
as stated in
http://guides.rubyonrails.org/association_basics.html
section 3.4?
you may also need to set the :foreign_key => "admin_model_name_id" attribute to specify the referencing model.
Hope it helps.
Did you try
class Car < ActiveRecord::Base
belongs_to :admin_model_name, :class_name => 'Admin::ModelName'
end
and if necessary add :foreign_key => '' and add this column to your cars table.