Rails model associations - has_one or single table inheritance? - ruby-on-rails

I'm having trouble deciding between Single Table Inheritance and a simple has_one relationship for my two models.
Background: I'm creating a betting website with a "Wager" model. Users may create a wager, at which point it is displayed to all users who may accept the wager if they choose. The wager model has an enum with three statuses: created, accepted, and finished.
Now, I want to add the feature of a "Favorite Wager". The point of this is to make it more convenient for users to create a wager, if they have ones they commonly create. One click instead of ten.
FavoriteWagers exist only as a saved blueprint. They are simply the details of a wager -- when the User wants to create a Wager, they may view FavoriteWagers and click "create", which will take all the fields of the FavoriteWager and create a Wager with them. So the difference is that FavoriteWagers acts as only as a storage for Wager, and also includes a name specified by the user.
I read up on STI, and it seems that a lot of examples have multiple subclassing - eg. Car, Motorcycle, Boat for a "Vehicle" class. Whereas I won't have multiple subclasses, just one (FavoriteWager < Wager). People have also said to defer STI until I can have more classes. I can't see myself subclassing the Wagers class again anytime soon, so that's why I'm hesitant to do STI.
On the other hand, has_one doesn't seem to capture the relationship correctly. Here is an example:
Class User < ApplicationRecord
has_many :favorite_wagers, dependent: :destroy
has_many :wagers, dependent: destroy
end
Class FavoriteWager < ApplicationRecord
has_one :wager
belongs_to: user, index: true, foreign_key: true
end
Class Wager < ApplicationRecord
belongs_to :favorite_wager, optional: true
belongs_to :user
end
I've also thought about just copying the fields directly, but that's not very DRY. Adding an enum with a "draft" option seems too little, because I might need to add more fields in the future (eg. time to auto-create), at which point it starts to evolve into something different. Thoughts on how to approach this?

Why not just do a join table like:
Class User < ApplicationRecord
has_many :favorite_wagers, dependent: :destroy
has_many :wagers, through: :favorite_wagers
end
Class FavoriteWager < ApplicationRecord
belongs_to :wager, index: true, foreign_key: true
belongs_to :user, index: true, foreign_key: true
end
Class Wager < ApplicationRecord
has_one :favorite_wager, dependent: destroy
has_one :user, through: :favorite_wager
end
Your FavoriteWager would have the following fields:
|user_id|wager_id|name|
That way you can access it like:
some_user.favorite_wagers
=> [#<FavoriteWager:0x00007f9adb0fa2f8...
some_user.favorite_wagers.first.name
=> 'some name'
some_user.wagers.first.amount
=> '$10'
some_user.wagers.first.favorite_wager.name
=> 'some name'
which returns an array of favorite wagers. If you only want to have ONE favorite wager per user you can tweak it to limit that. But this gives you the ability to have wagers and users tied together as favorites with a name attribute. I don't quite understand your use case of 'a live wager never has a favorite' but that doesn't matter, you can tweak this to suit your needs.

Related

Question about table/models logic before database generation

Im trying to make my first side project using rails to learn would you kindly help me understand this?
The basic idea is to have a betting game where one user generates a new bet that can only be accepted by another user (only 2 competitors assigned for each bet, the creator and the other player).
I'm thinking about 2 tables:
users
bets
Normally I would just have a one to many relationship for the user that created the bet. But I'm confused about the 'competitor' column where another user is also a user with a user_id. How can I express this relationship better than this:  
After thinking it through it doesn't look like a good setup because I'm renaming a column where I'm storing the user_id and having a many to many 'through' model doesn't make sense since it is a "only one competitor can participate in that bet".
I was thinking about a 'one to one through' creating a 'competitors' table like so:
Could you explain to me how to build it in a better way?
Many thanks!
just an idea, you can do this with 2 foreign_keys
so user can be as creator or competitors, you can also makesure that creator_id and competitor_id cannot be same value since user cannot bet with self
class Bet < ActiveRecord::Base
belongs_to :creator, foreign_key: "creator_id", class_name: "User"
belongs_to :competitor, foreign_key: "competitor_id", class_name: "User"
end
class User < ActiveRecord::Base
# as creator to create bet
has_many: creator_bets, foreign_key: :creator_id, class_name: "Bet"
# as competitor to create bet
has_many: competitor_bets, foreign_key: :competitor_id, class_name: "Bet"
end
#user = User.first
#user.creator_bets.build(...)
# this to create bet as creator
#user.competitor_bets.build(...)
# this to create bet as competitor
having a many to many 'through' model doesn't make sense since it is a
"only one competitor can participate in that bet".
Actually it does. Its is in many ways simpler than having muliple assocations pointing to the same table as you don't have to deal with the situation where a user could be in either column which requires something like:
# this gets much messier as you have to deal with more complex problems
Bet.where('bets.user_id = :id OR bets.competitor_id = :id', id: params[:user_id])
A many to many association also gives you the option of removing that limitation later with minimal redesign.
Given the following associations:
# This represents an event that users can bet on
# for example Elon Musk being the first man on mars.
class Event < ApplicationRecord
has_many :bets
has_many :users, through: :bets
end
class User < ApplicationRecord
has_many :bets
has_many :events, through: :bets
end
# This is the "join model" that joins User and Event
# columns:
# - user_id [bigint, fk]
# - event_id [bigint, fk]
# - amount [decimal]
class Bet < ApplicationRecord
belongs_to :user
belongs_to :event
end
You can simply create a bet by:
#event = Event.create!(description: "Elon Musk will be the first man on mars.")
#event.bets.new(user: User.first, amount: 500)
You can of course cap the number of users to two by adding a custom validation or in your controllers. If you only have two you can assume that event.users.first is the creator and event.users.last is the competitor.

Modeling a Subscription in Rails

So, after thinking on this for a while, I have no idea what the proper way to model this is.
I have a website focused on sharing images. In order to make the users happy, I want them to be able to subscribe to many different collections of images.
So far, there's two types of collections. One is a "creator" relationship, which defines people who worked on a specific image. That looks like this:
class Image < ActiveRecord::Base
has_many :creations
has_and_belongs_to_many :locations
has_many :creators, through: :creations
end
class Creator < ActiveRecord::Base
has_many :images, ->{uniq}, through: :creations
has_many :creations
belongs_to :user
end
class Creation < ActiveRecord::Base
belongs_to :image
belongs_to :creator
end
Users may also tag an image with a subjective tag, which is something not objectively present in the image. Typical subjective tags would include "funny" or "sad," that kind of stuff. That's implemented like this:
class SubjectiveTag < ActiveRecord::Base
# Has a "name" field. The reason why the names of tags are a first-class DB model
# is so we can display how many times a given image has been tagged with a specific tag
end
class SubjectiveCollection < ActiveRecord::Base
# Basically, "User X tagged image Y with tag Z"
belongs_to :subjective_tag
belongs_to :user
has_many :images, through: :subjective_collection_member
end
class SubjectiveCollectionMember < ActiveRecord::Base
belongs_to :subjective_collection
belongs_to :image
end
I want users to be able to subscribe to both Creators and SubjectiveTags, and to display all images in those collections, sequentially, on the home page when they log in.
What is the best way to do this? Should I have a bunch of different subscription types - for example, one called SubjectiveTagSubscription and one called CreatorSubscription? If I do go this route, what is the most efficient way to retrieve all images in each collection?
What you want to use is a Polymorphic Association.
In your case, it would look like this:
class Subscription < ActiveRecord::Base
belongs_to :user
belongs_to :subscribeable, polymorphic: true
end
The subscriptions table would need to include the following fields:
user_id (integer)
subscribeable_id (integer)
subscribeable_type (string)
This setup will allow a subscription to refer to an instance of any other model, as ActiveRecord will use the subscribeable_type field to record the class name of the thing being subscribed to.
To produce a list of images for the currently logged in user, you could do this:
Subscription.where(user_id: current_user.id).map do |subscription|
subscription.subscribeable.images.all
end.flatten
If the performance implications of the above approach are intolerable (one query per subscription), you could collapse your two types of subscribeables into a single table via STI (which doesn't seem like a good idea here, as the two tables aren't very similar) or you could go back to your initial suggestion of having two different types of subscription models/tables, querying each one separately for subscriptions.

What models/associations do I need to use to implement this feature efficiently

I'm wondering if you can help me determine the best associations and maybe new models to use for a feature I'm trying to add to an existing app. The app lets users ask questions that are answered by other users. When posting a question, a user indicates which Province it belongs to (because the questions are location specific) and the user also selects one or more categories for the question. Thus, after the questions created, I'm able to get category (choosing one category) and province information like this
def show
#question = Question.find(params[:id])
puts #question.categories.first.id #id = 2 for Tourism, for example
puts #question.province_id #id = 6 for Ontario, for example
end
What I'd like to do is let some users (who answer questions) become featured users whose profiles will be displayed down the side of the views/question/show page. So in the question show action, according to the above example, I'd to query for the featured users for Tourism category in the province of Ontario.
Considering that there will hopefully be thousands of users of the app, but only a small pool of featured users, I don't think it's best to store that information on the user model, but rather maybe create a featured_user model that connects to their main user profile, a province (for which they are an expert) and the category (for which they are an expert), but they'd only be pulled up when they match both 'province and category' at the same time, so a featured_user whose category is "Tourism" and province is "Ontario" will only be pulled up if the question is in both category "Tourism" and province "Ontario."
If I create a featured_user model, what columns and associations should I create with these other models to make it work efficiently, and/or what should I add to existing models? I'm not sure if I should be trying to use some has_and_belongs_to_many or has_many :through or simple foreign keys. My instinct is to make it more complicated than it probably should be.
Right now
Province.rb
has_many :questions
Question.rb
belongs_to :province
has_many :categorizations
has_many :categories, through: :categorizations
Category.rb
has_many :categorizations
has_many :questions, through: :categorizations
Categorization
belongs_to :question, touch: true
belongs_to :category, touch: true
User.rb
has_many :questions #i.e. questions they've asked
has_many :answers #i.e. questions they've answered
I think the featured user model is the best solution. This encapsulate all featured user logic in one place and you can implement it without adding attribute columns to other models that (as you say) only would be used for a very small number of users. Here is how i would implement this:
# CreateFeaturedUsers (migration)
create_table :featured_users do |t|
t.references :user
t.references :province
t.references :category
end
# FeaturedUser (model)
belongs_to :user
belongs_to :province
belongs_to :category
# User (model)
has_many :featured_users
# Question (model)
def featured_users
User.joins(:featured_users).where(featured_users: {province: this.province, category: this.category})
end
However i would properly name it something else then FeaturedUser as the model doesn't represent a user but only the relation to a user. Maybe Featuring would be a better name.

In has_many :through, what is the correct way to create object inheritance

I've hit something that I don't understand how to model with Rails associations and neither STI nor polymorphism seem to address it.
I want to be able to access attributes from a join table via the collection that's created by has_many :through.
In the code below, this means that I want to be able to access the name and description of a committee position via the objects in the .members collection but as far as I can see I can't do that. I have to go through the original join table.
e.g. modelling a club and it's committee members
class User < ActiveRecord::Base
attr_accessible :full_name,
:email
has_many: committee_positions
has_many: committees, :through => committee_positions
end
class Committee < ActiveRecord::Base
attr_accessible :name
has_many :committee_positions
has_many :members, :through => :committee_positions
end
class CommitteePosition < ActiveRecord::Base
attr_accessible :user_id,
:committee_id,
:member_description,
:role_title
belongs_to :committee
belongs_to :user
end
Assume that each committee position instance has a unique description
i.e. the description is particular to both the member and the committee and so has to be stored on the join table and not with either the user or the club.
e.g.
Committee member: Matt Wilkins
Role: "Rowing club president"
Description: "Beats the heart of the rowing club to his own particular drum"
Is there a way to access the data in the join table via the committee.members collection?
While active record gives us this great alias for going directly to the members, there doesn't seem to be any way to access the data on the join table that created the collection:
I cannot do the following:
rowing_committee.members.find_by_role_title('president').name
Each item in the .members collection is a user object and doesn't seem to have access to either the role or description that's stored in the CommitteePositions join table.
The only way to do this would be:
rowing_committee.committee_positions.find_by_role_title('president').user.name
This is perfectly do-able but is clunky and unhelpful. I feel like the use-case is sufficiently generic that I may well be missing something.
What I would like to access via objects in the committee.members collection
member
- full_name
- email
- role_title (referenced from join table attributes)
- member_description (referenced from join table attributes)
This is only a small thing but it feels ugly. Is there a clean way to instruct the "member" objects to inherit the information contained within the join table?
-------------- addendum
On working through this I realise that I can get half way to solving the problem by simply defining a new class for committee member and referencing that instead of user in the has_many :through relationship. It works a little bit better but is still pretty clunky
class Committee < ActiveRecord::Base
...
has_many :committee_positions
has_many :members,
:through => :committee_positions,
:class_name => 'CommitteeMember'
...
end
class CommitteeMember < User
def description( committee )
self.committees.find_by_committee_id( committee.id ).description
end
def role( committee )
self.committees.find_by_committee_id( committee.id ).description
end
end
Now this is getting closer but it still feels clunky in that the code to use it would be:
committee = Committee.first
president_description = committee.members.find_by_title('president').description( committee )
Is there any way to initialize these objects with the committee they are referencing?
I think you could use some delegation here. In your Committee_Position class:
class Committee_Position < ActiveRecord::Base
attr_accessible :user_id,
:committee_id,
:member_description,
:role_title
belongs_to :committee
belongs_to :user
delegate :name, :email, :to => :user
end
so you could do what you say you want:
rowing_club.committee_members.find_by_role_title('president').name

Ruby on rails with different user types

I'm trying to build a application that has different kinds of users, I'm using authlogic for user authentication.
So I have one user model that has the required field for authlogic to do its magic. I now want to add a couple of different models that would describe the extra fields for the different kinds of users.
Lets say that a user registers, he would then select his user type, when he is done registering he would be able to add information that is specific for his user model.
What would be the best way to do this? I am currently looking into polymorphic models but I'm not sure that's the best route to take. Any help would be greatly appreciated, thanks.
You can create different profile tables and just tie the profile to the user. So for each user type you can create a table and store the specific info there and have a user_id column to point back to users.
class User < ActiveRecord::Base
has_one :type_1
has_one :type_2
end
class Type1 < ActiveRecord::Base
belongs_to :user
end
class Type2 < ActiveRecord::Base
belongs_to :user
end
Now this isn't very DRY and could lead to problems if you are constantly adding user types. So you could look into polymorphism.
For polymorphism, the users table would define what type the user is (profileable_id and profileable_type). So something like this:
class User < ActiveRecord::Base
belongs_to :profileable, :polymorphic => true
end
class Type1 < ActiveRecord::Base
has_one :user, :as => :profileable
end
class Type2 < ActiveRecord::Base
has_one :user, :as => :profileable
end
Then there is a third option of STI (single table inheritance) for the user types. But that doesn't scale well if the user type fields differ dramatically.
The best approach I saw it here
http://astockwell.com/blog/2014/03/polymorphic-associations-in-rails-4-devise/

Resources