How to structure these models associations? - ruby-on-rails

I am basically trying to create an app which allows a user to track what they are working on. So each user will have many projects.
Now each project can have a certain number of types e.g. videos, websites, articles, books etc. with each of these types of projects having many objects (i.e. a videos project would list all the videos they were working on, books project may have a list of books they read).
Each of these objects would have different variables (books might have author and length and websites may have URL, rank etc.)
How would I set up these models in rails?
So basically would I separate my project model from my type model (so project has_one type and type belongs_to project) and then have 4 separate object models (i.e. objectsite) for each of the different types?
Or is there a better way to design this.
Modelling Many Rails Associations
I read this question and I think this may be similar to what I want to do but am not entirely sure as I don't full understand it.
UPDATE**
So far I have:
Projects
has_one type
Type
belongs_to projects
has_many objects
object
belongs_to type
But I am not to sure if this is the best way to go about it because like I said each object for each type will be different

From what I could bring myself to read, I'd recommend this:
Models
So each user will have many projects.
#app/models/project.rb
Class Project < ActiveRecord::Base
belongs_to :user
end
#app/models/user.rb
Class User < ActiveRecord::Base
has_many :projects
end
Vars
Each of these objects would have different variables (books might have
author and length and websites may have URL, rank etc.)
There are two ways to interpret this
The first is if you know what details your different objects will require, you could include them as attributes in their respective datatables. If you don't know, you'll have to use another table to populate them.
I'll detail both approaches for you:
--
Known Attributes
As Pavan mentioned in the comments, you'll likely benefit from an STI (Single Table Inheritance) for this:
#app/models/project.rb
Class Project < ActiveRecord::Base
belongs_to :user
has_many :books, class_name: "Project::Books"
has_many :folders, class_name: "Project::Folders"
end
#app/models/object.rb
Class Object < ActiveRecord::Base
#fields - id | type | project_id | field1 | field2 | field3 | field4 | created_at | updated_at
belongs_to :project
end
#app/models/project/book.rb
Class Book < Object
... stuff in here
end
This will allow you to call:
project = Project.find(params[:id])
project.books.each do |book|
book.field1
end
--
Unknown Attributes
#app/models/project.rb
Class Project < ActiveRecord::Base
has_many :objects
end
#app/models/object.rb
Class Object < ActiveRecord::Base
belongs_to :project
has_many :options
end
#app/models/option.rb
Class Option < ActiveRecord::Base
#fields - id | object_id | name | value | created_at | updated_at
belongs_to :object
end
This will allow you to call:
project = Project.find(params[:id])
project.objects.first.options.each do |option|
option.name #-> outputs "name" of attribute (EG "length")
option.value #-> outputs "value" of attribute (EG "144")
end
This means you can use option to populate the various attributes your objects may require.

Related

Rails Combining and Sorting ActiveRecord Relations when using polymorphic associations

I am working on a small collection tracker where I feel like STI could really simplify this problem but it seems the general consensus is to avoid STI whenever possible so I have broken my models apart. Currently, they are all the same but I do have a few different bits of metadata that I can see myself attaching to them.
Anyways, the root is a Platform which has many Games, Systems, Peripherals, etc. and I am trying to show all of these relations on a view in a dynamic table that is filterable, sortable and searchable.
For example a query could be #platform.collectables.search(q).order(:name).
# Schema: platforms[ id, name ]
class Platform < ApplicationRecord
has_many :games
has_many :systems
has_many :peripherals
end
# Schema: games[ id, platform_id, name ]
class Game < ApplicationRecord
belongs_to :platform
end
# Schema: systems[ id, platform_id, name ]
class System < ApplicationRecord
belongs_to :platform
end
# Schema: peripherals[ id, platform_id, name ]
class Peripheral < ApplicationRecord
belongs_to :platform
end
In the above, the polymorphism comes into play when I add them to a Collection:
# Schema: collections[ id, user_id, collectable_type, collectable_id ]
class Collection < ApplicationRecord
belongs_to :user
belongs_to :collectable, polymorphic: true
end
Now, when I view a Platform, I expect to see all of its games, systems and peripherals which I refer to as collectables. How would I query all of these while being able to sort as a whole (ie: "name ASC"). Below works in theory but this changes the relation to an Array which stops me from further filtering, searching or reordering at the database level so I can't tag on another scope or order.
class Platform < ApplicationRecord
...
def collectables
games + systems + peripherals
end
end
I stumbled on Delegated Types which kind of sounds like the step in the direction that I am looking for but maybe I am missing something.
I'm tempted to try the STI route, I don't see these models diverging much and things that are different could be stored inside of a JSONB column cause it's mostly just metadata for populating a view with and not really searching against. Basically a model such as this but it seems so frowned upon, I feel like I must be missing something.
# Schema: collectables[ id, platform_id, type, name, data ]
class Collectable < ApplicationRecord
belongs_to :platform
end
class Platform < ApplicationRecord
has_many :collectables
def games
collectables.where(type: 'Game')
end
def systems
collectables.where(type: 'System')
end
...
end
One solution here would be Delegated Type (a relatively new Rails feature) which can basically be summarized as Multiple Table Inheritance through polymorphism. So you have a base table containing the shared attributes but each class also has its own table - thus avoiding some of the key problems of STI.
# app/models/concerns/collectable.rb
# This module defines shared behavior for the collectable "subtypes"
module Collectable
TYPES = %w{ Game System Peripheral }
extend ActiveSupport::Concern
included do
has_one :base_collectable, as: :collectable
accepts_nested_attributes_for :base_collectable
end
end
# This model contains the base attributes shared by all the collectable types
# rails g model base_collectable name collectable_type collectable_id:bigint
class BaseCollectable < ApplicationRecord
# this sets up a polymorhic association
delegated_type :collectable, types: Collectable::TYPES
end
class Game < ApplicationRecord
include Collectable
end
class Peripheral < ApplicationRecord
include Collectable
end
class System < ApplicationRecord
include Collectable
end
You can then setup a many to many assocation through a join model:
class Collection < ApplicationRecord
belongs_to :user
has_many :collection_items
has_many :base_collectables, through: :collection_items
has_many :games,
through: :base_collectables,
source_type: 'Game'
has_many :peripherals,
through: :base_collectables,
source_type: 'Peripheral'
has_many :systems,
through: :base_collectables,
source_type: 'Systems'
end
class CollectionItem < ApplicationRecord
belongs_to :collection
belongs_to :base_collectable
end
# This model contains the base attributes shared by all the collectable types
# rails g model base_collectable name collectable_type collectable_id:bigint
class BaseCollectable < ApplicationRecord
# this sets up a polymorhic association
delegated_type :collectable, types: %w{ Game System Peripheral }
has_many :collection_items
has_many :collections, through: :collection_items
end
This lets you treat it as a homogenius collection and order by columns on the base_collectables table.
The big but - the relational model still doesn't like cheaters
Polymorphic assocations are a dirty cheat around the object relational impedence missmatch by having one columns with a primary key reference and another storing the class name. Its not an actual foreign key since the assocation cannot resolved without first pulling the records out of the database.
This means you can't setup a has_many :collectables, through: :base_collectables association. And you can't eager load all the delegated types or order the entire collection by the columns on the games, peripherals or systems tables.
It does work for the specific types such as:
has_many :games,
through: :base_collectables,
source_type: 'Game'
Since the table can be known beforehand.
This is simply a tough nut to crack in relational databases which are table based and not object oriented and where relations in the form of foreign keys point to a single table.
VS STI + JSON
The key problem here is that all the data you're stuffing into the JSON column is essentially schemaless and you're restricted by the 6 types supported by JSON (none of which is a date or decent number type) and working with the data can be extremely difficult.
JSON's main feature is it simplicity which worked well enough as a transmission format. As a data storage format its not really that great.
It's a huge step up from the EAV table or storing YAML/JSON serialized into a varchar column but its still has huge caveats.
Other potential solutions
STI. Fixes one the polymorphism problem and introduces a bunch more.
A materized view that is populated with a union of the three tables can be treated as table and thus can simply have an ActiveRecord relation attached to it.
Union query. Basically the same idea as above but you just use the raw query results or feed them into a model representing a non-sensical table.
Non-relational database. Document based databases like MongoDB let you create flexible documents and non-homogenius collections by design while having better type support.

Database organization for separating two different types of the same model

So I want my User model to have_many Skills. I want there to be two different categories for the skills: a wanted skill, and a possessed skill.
For example, a user can add a skill to their profile that they possess, such as HTML. They can also add a skill to their profile that they wish to learn, such as Ruby on Rails. On their profile it'll list their current skills and wanted ones, separately.
From a high level, what's the best way to architect this? I just want there to be 1 Skill model with no duplicates, but I want there to be a way for users to have the 2 separate groups of skills in the database.
You can use single table inheritance
class Skill < ActiveRecord::Base
end
class WantedSkill < Skill
belongs_to :user
end
class PossessesSkill < Skill
belongs_to :user
end
Your skills table should have a column called type where the type of the skill will be stored.
WantedSkill.create(:name => "html")
Above will save the record in the skills table with type 'WantedSkill'. You can retrieve it by
WantedSkill.where(:name => "html").first
your user associations can be like
class User < ActiveRecord::Base
has_many :wanted_skills
has_many :possessed_skills
end
You can read documentation here
A way to achieve this, you need two skill fields like: wanted_skill and possessed_skill
So, in Ruby on Rails you can have many references (with different names) to the same model, you only need to declare which class corresponds using class_name in the references, e.g.:
class User < ActiveRecord::Base
belongs_to :wanted_skill, class_name: 'Skill'
belongs_to :possessed_skill, class_name: 'Skill'
end

3 or more model association confusion at Rails

It has been almost a week since I'm trying to find a solution to my confusion... Here it is:
I have a Program model.
I have a ProgramCategory model.
I have a ProgramSubcategory model.
Let's make it more clear:
ProgramCategory ======> Shows, Movies,
ProgramSubcategory ===> Featured Shows, Action Movies
Program ==============> Lost, Dexter, Game of Thrones etc...
I want to able to associate each of these models with eachother. I've got what I want to do particularly with many-to-many association. I have a categories_navigation JOIN model/table and all of my other tables are connected to it. By this way, I can access all fields of all of these models.
BUT...
As you know, has_many :through style associations are always plural. There is nothing such as has_one :through or belongs_to through. BUT I want to play with SINGULAR objects, NOT arrays. A Program has ONLY ONE Subcategory and ONLY ONE Category. I'm just using a join table to only make connection between those 3. For example, at the moment I can access program.program_categories[0].title but I want to access it such like program.program_category for example.
How can I have 'has_many :through's abilities but has_one's singular usage convention all together? :|
P.S: My previous question was about this situation too, but I decided to start from scratch and learn about philosophy of associations. If you want so you may check my previous post here: How to access associated model through another model in Rails?
Why a join table where you have a direct relationship? In the end, a program belongs to a subcategory, which in turn belongs to one category. So no join table needed.
class Program < ActiveRecord::Base
belongs_to :subcategory # references the "subcategory_id" in the table
# belongs_to :category, :through => :subcategory
delegate :category, :to => :subcategory
end
class Subcategory < ActiveRecord::Base
has_many :programs
belongs_to :category # references the "category_id" in the table
end
class Category < ActiveRecord::Base
has_many :subcategories
has_many :programs, :through => :subcategories
end
Another point of view is to make categories a tree, so you don't need an additional model for "level-2" categories, you can add as many levels you want. If you use a tree implementation like "closure_tree" you can also get all subcategories (at any level), all supercategories, etc
In that case you skip the Subcategory model, as it is just a category with depth=2
class Program < ActiveRecord::Base
belongs_to :category # references the "category_id" in the table
scope :in_categories, lambda do |cats|
where(:category_id => cats) # accepts one or an array of either integers or Categories
end
end
class Category < ActiveRecord::Base
acts_as_tree
has_many :programs
end
Just an example on how to use a tree to filter by category. Suppose you have a select box, and you select a category from it. You want to retrieve all the object which correspond to any subcategory thereof, not only the category.
class ProgramsController < ApplicationController
def index
#programs = Program.scoped
if params[:category].present?
category = Category.find(params[:category])
#programs = #programs.in_categories(category.descendant_ids + [category.id])
end
end
end
Tree-win!

ActiveRecord, double belongs_to

I have 2 models: Link and User such as:
class Link < ActiveRecord::Base
belongs_to :src_user
belongs_to :dst_user
end
class User < ActiveRecord::Base
has_many :links
end
A schema could looking like:
+----------+ +------+
| Link | | User |
+----------+ |------+
| src_user |---->| |
| dst_user |---->| |
+----------+ +------+
My question is: how can I edit User model do in order to do this
#user.links # => [list of links]
(...which should query #user.src_users + #users.dst_users, with unicity if possible.)
Can we do this only using SQL inside ActiveRecord?
Many thanks.
(note: I'm on Rails 3.1.1)
You have to specify multiple relations inside the user model so it knows which specific association it will attach to.
class Link < ActiveRecord::Base
belongs_to :src_user, class_name: 'User'
belongs_to :dst_user, class_name: 'User'
end
class User < ActiveRecord::Base
has_many :src_links, class_name: 'Link', inverse_of: :src_user
has_many :dst_links, class_name: 'Link', inverse_of: :dst_user
end
The :class_name option must be specified since the association name is not simply :links. You may also need to specify the :inverse_of option in the Link model, but I can't be sure of that. It wouldn't hurt if you did, though.
In order to do your #user.links call, you'll have to do something like this:
class User < ActiveRecord::Base
def links
Link.where(['src_user = ? OR dst_user = ?', self.id, self.id])
end
end
… since ActiveRecord doesn't provide a way to merge two associations on the same model into one.
This sounds very similar to the child/mother/father problem, which has a solution. That is, a problem where an object of one type belongs_to more than one object of the same type (not necessarily the same type as the child, but the objects it belongs to are themselves the same type).
This solution is quite old, and may not be up to date with the latest style schemes in Rails3, but it should work fine, or with very little modification.
http://railsforum.com/viewtopic.php?id=1248

What is the best-practice, don't-work-yourself-into-a-corner way to add comments to many models?

Hi Stack Overflowers: I'm building a Ruby on Rails application that has several different models (e.g. Movie, Song, Photo) that I am storing movie clips, mp3s and photos. I'd like for users to be able to comment on any of those Models and have control over which comments are published.
Is the best practice to create a Comment model with:
belongs_to :movie
belongs_to :song
belongs_to :photo
And then tie each Model with:
has_many :comments
Then, I'm guessing in the Comment table, I'll need a foreign key for each Model:
comment, movie_id, song_id, photo_id
Is this the correct way to build something like this, or is there a better way? Thanks in advance for your help.
Use acts_as_commentable. It creates a comment table with a commentable_type (model name of the commented-upon item) and commentable_id (the model's ID). Then all you need to do in your models:
class Photo < ActiveRecord::Base
acts_as_commentable
end
Create a table to hold the relationships for each type of comment:
movie_comments, song_comments, photo_comments
and then use:
class Movie < ActiveRecord::Base
has_many :movie_comments
has_many :comments, :through => :movie_comments
end
class MovieComment < ActiveRecord::Base
include CommentRelationship
belongs_to :comment
belongs_to :movie
end
You can use a module (CommentRelationship) to hold all of the common functionality between your relationship tables (movie_comments)
This approach allows for the flexibility to be able to treat your comments differently depending on the type, while allowing for similar functionality between each. Also, you don't end up with tons of NULL entries in each column:
comment | movie_id | photo_id | song_id
----------------------------------------------------
Some comment 10 null null
Some other comment null 23 null
Those nulls are definitely a sign you should structure your database differently.
Personally I would model it this way:
Media table (media_id, type_id, content, ...)
.
MediaType table (type_id, description, ... )
.
MediaComments table ( comment_id, media_id, comment_text, ...)
After all, there is no difference to the database between a Song, Movie, or Photo. It's all just binary data. With this model you can add new "media types" without having to re-code. Add a new "MediaType" record, toss the data in the Media table.
Much more flexible that way.
I have no RoR Experience, but in this case you'd probably better off using inheritance on the database level, assuming your dbms supports this:
CREATE TABLE item (int id, ...);
CREATE TABLE movie (...) INHERITS (item);
CREATE TABLE song (...) INHERITS (item);
[...]
CREATE TABLE comments (int id, int item_id REFERENCES item(id));
Another approach could be a single table with a type column:
CREATE TABLE item (int id, int type...);
CREATE TABLE comments (int id, int item_id REFERENCES item(id));
As expressed before, I can't tell you how to exactly implement this using RoR.
The best idea is probably to do what Sarah suggests and use one of the existing plugins that handle commenting.
If you wish to roll your own, or just understand what happens under the covers, you need to read about Single Table Inheritance, the way Rails handles inheritance. Basically, you need a single comments table ala:
# db/migrate/xxx_create_comments
create_table :comments do |t|
t.string :type, :null => false
t.references :movies, :songs, :photos
end
Now you can define your comment types as
class Comment < ActiveRecord::Base
validates_presence_of :body, :author
# shared validations go here
end
class SongComment < Comment
belongs_to :song
end
class MovieComment < Comment
belongs_to :movie
end
class PhotoComment < Comment
belongs_to :photo
end
All your comments will be stored in a single table, comments, but PhotoComment.all only returns comments for which type == "Photo".

Resources