How should I model these relationships? - ruby-on-rails

I have a post model. Each post has a title and many snippets.
class Post < ActiveRecord::Base
has_many :snippets
end
class Snippet < ActiveRecord::Base
belongs_to :post
attr_accessible :position, :post_id
end
I want to have 4 different types of snippet i.e.:
body (text)
image (string)
code (text)
video (string)
Q1
Should I create four new models (called text, code, video and image) which extend the snippet model like so?:
class Text < Snipppet
attr_accessible :body
end
class Image < Snippet
attr_accessible :image
end
class Video < Snippet
attr_accessible :title
end
class Code < Snippet
attr_accessible code
end
Q2
How can I refer to the content of each snippet in my view when each snippet can be one of 4 different things?
In my view I'd like to put something like this:
- for snippet in #post.snippets
= place the content of the snippet here
Q3
I don't think it sounds like a good idea to have a "type" field on the snippet model as this would possibly lead to strong coupling of the database and the code. Is there some kind of rails magic that will help me out in this situation?

I kinda like the type field, actually. You could use the magic of single table inheritance, but it's well-known to be a fragile system that, surprise, includes a type field, anyway.
And then you could use the polymorphism solution, but seems a bit silly to have four different tables to represent almost exactly the same thing.
Honestly, using a simple type field and changing the interpretation based on that will probably give you the best results and fewest headaches.
As far as what you'd do in your template goes, that you can probably play by ear. One clean solution might be a simple helper to call like snippet_content which, based on the type field, will call the helper snippet_content_text or snippet_content_image or snippet_content_code or snippet_content_video. Or you can just do the if-then branching in the template, or refer to any number of partial templates (though be careful with that, since those can get slow when used unnecessarily).

I like this approach, but I always run into some subtle complications later on.
Create the appropriate views (texts/_text.html.haml, images/_image.html.haml, etc), so you can let Rails handle it for you:
= render #post.snippets
Like it or not: the "type"-field is the way to have Rails magic help you create the proper instances.
Update: polymorphic relations work too, let every snippet have their own tables. Is it a is a relation or a behaves like type of relation? You could say that Images and Texts both behave like snippets. In that case, go for a module named Snippet and mix it in.
class Post < ActiveRecord::Base
has_many :snippets, :polymorphic => true
end
class Text < ActiveRecord::Base
include Snippet
belongs_to :post, :as => :snippet
end
module Snippet
# shared behavior here
end

Many thanks to everyone who answered, but I think I've managed to fix this by using jystewart's rails 3 version of the acts_as_polymorphs gem
Here's what I did:
So let's recall, we have Posts and we have four different types of snippet. So that's 6 different models (post.rb, snippet.rb, code.rb. text.rb, image.rb and video.rb)
Posts have many snippets.
Snippets belong to posts.
Code, text, video and image are all types of snippet
And the big problem is that we don't know what type of object a snippet is, because it can be one of 4 different things.
At first, I tried to do this with Single Table Inheritance, but the objects (code, text, video and image) are, in my opinion, too different from one another for this to work well and I didn't like the fact that it would likely result in lots of empty database cells so I went along the Polymorphic route.
I couldn't get standard polymorphic associations to work here because this is not your usual polymorphic situation. Normally, when dealing with polymorphism, we're talking about something like a comments model, which can attach to multiple other models. The polymorphic entity is always the same thing. But in this case, we're talking about snippets model which can be one of 4 different things. This is not a simple belongs_to situation. Polymorphism was not happening.
Then I stumbled upon this article by m.onkey.org - which is a few years old but he basically explained that this sort of thing needs the acts_as_polymorphs gem.
So the solution was to do the following:
I create 6 models, all extending ActiveRecord::Base
add has_many_polymorphs to the post model
create a polymorphic association called "snippetable" in the snippet model
add some new fields to the snippets table through my migration file
Here's my code:
class Post < ActiveRecord::Base
has_many_polymorphs :snippets, :from => [:codes, :texts, :videos, :images], :through => :snippets
end
class Snippet < ActiveRecord::Base
belongs_to :snippetable, :polymorphic => true
end
class Code < ActiveRecord::Base
# don't have to put anything in here
end
class Text < ActiveRecord::Base
# don't have to put anything in here
end
class Video < ActiveRecord::Base
# don't have to put anything in here
end
class Image < ActiveRecord::Base
# don't have to put anything in here
end
The only other thing we need, is to stick a few new fields in the CreateSnippets migration:
class CreateSnippets < ActiveRecord::Migration
def self.up
create_table :snippets do |t|
t.references :post
t.column :snippetable_id, :integer, :null => false
t.column :snippetable_type, :string, :null => false
t.timestamps
end
end
end
And that's it! Unbelievably I can now go to rails console and do the following:
p = Post.first
p.codes << Code.create(:code=>"This is a code snippet")
p.images << Image.create(:image=>"xyz.png")
p.images << Image.create(:image=>"123.png")
p.codes.count # returns 1
p.images.count # returns 2
p.snippets.count # returns 3 !!!
Yaldi!
Anyway, it's taken me 11 days to fix this, and it REALLY depressed me that I couldn't do it. I hope this helps someone.
Here are some decent reading materials for acts_as_polymorph:
jstewart's rails 3 gem
Pratik Nait (of 37 signals) Blog post on it
What a carry on

Related

Rails 5 nested attributes alternative way?

I'm creating an app that it needs to be customizable.
Ej:
I have:
Position scaffold
Category scaffold
Class scaffold
Type Scaffold
Now, the situation is, position table along other attributes, includes a category attr, a class attr and a type attr. The problem I have is that every category, class and type is created and owned by another controller and moedel.
Now, here is where my problem comes to life.
when I tried to create a position (even though I can actually see all categories, classes and types listed on my position view via form.select) I cannot save a position with a category, class nor type.
I tried nested_attributes and don't work quite good in this situation.
What is the appropriate method tho attack this from scratch base on my scaffolds?
Any help will be very much appreciated!
Thanks in advance!
It is a little tough to figure out what you need without seeing your code, but I'll try my best to help.
Keep in mind that Class and Type are reserved words in rails; Columns named _type are used for polymorphic relationships. And Class is used well for ruby classes. So as a general rule of thumb I would stay clear of those terms.
For you Position model:
class Position < ActiveRecord::Base
has_many :categories
has_many :types
has_many :classes
accepts_nested_attributes_for :categories, :types, :classes
end
Then for your other models:
class NameOfClass < ActiveRecord::Base
belongs_to :position
end
You also need to make sure that you add these nested attributes to the whitelist on you controllers. It should include any columns that you want to accept that will update other models. Keep in mind that this has to be the last thing listed.
In your Position Controller
PositionsController < ApplicationController
private
def position_params
params.require(:position).permit(:id, :position_column_a, :another_column,
categories_attributes: [:id, :position_id, :any_other_column_you_want],
types_attributes: [:id, :position_id, :again_any_other_column])
Again, before you do anything I would look into renaming your scaffolds. the best way is to delete them and then regenerate. This will do the trick:
rails d scaffold Type
If you just need a simple dropdown of a list of categories you can try enums. However, it is not good for options that need to be customizable for the user. I typically only used them for statuses.
https://bendyworks.com/blog/rails-enum-sharp-knife
You can also read the API docs: http://api.rubyonrails.org/classes/ActiveRecord/NestedAttributes/ClassMethods.html

Rails - Manipulating the returned objects in an ActiveRecord query

So I have the following simplified models and associations:
class Barber < User
has_many :barber_styles, inverse_of: :barber
end
class Style < ActiveRecord::Base
end
class BarberStyle < ActiveRecord::Base
belongs_to :barber, inverse_of: :barber_styles
belongs_to :style
delegate :name, to: :style
end
If I wanted to make a query based on all BarberStyle's that belong to a specific Barber, is there a way I can include the name of the associated Style?
I want to use a line something like:
BarberStyle.joins(:barber).where(barber: 109).include(:style)
So I'd be able to access an associated style.name. But I don't think a line like this exists.
I know I could just use map to build an array of my own objects, but I have to use a similar query with many different models, and don't want to do a bunch of extra work if it's not necessary.
In one of my previous projects, I was able to render json with the following lines:
#colleges = College.includes(:sports).where(sports: { gender: "male" })
render json: #colleges, include: :sports
But I don't want to render json in this case.
Edit:
I don't really have any unincluded associations to show, but I'll try to elaborate further.
All I'm trying to do is have extra associated models/fields aggregated to each BarberStyle result within my ActiveRecord Relation query.
In an attempt to make this less confusing, here is an example of the type of data I want to pass into my JS view:
[
#<BarberStyle
id: 1,
barber_id: 116,
style_id: 91,
style: { name: "Cool Hairstyle" }
>,
#<BarberStyle
id: 2,
barber_id: 97,
style_id: 92,
style: { name: "Cooler Hairstyle" }
>,
etc...
]
The reason I want the data formatted this way is because I can't make any queries on associated models in my JS view (without an AJAX call to the server).
I had very similarly formatted data when I did render json: #colleges, include: :sports in the past. This gave me a collection of Colleges with associated Sport models aggregated to each College result. I don't want to build json in this fashion for my current situation, as it will complicate a few things. But I suppose that's a last resort.
I'm not sure if there's a way to structure an ActiveRecord query where it adds additional fields to the results, but I feel like it should, so here I am looking for it. Haven't found anything in the docs, but then again there is sooooo much left out of those.
Doug as far as I understand your code, BarberStyle must be a joining model between Barber and Style. You mentioned in your comment that you removed the has_many :styles, through: :barber_styles from your Barber model because you thought that it wasn't relevant. That's not true, it's exactly the point that would help you to achieve your goal. Add this relation again then you can do something like this:
barber = Barber.includes(:styles).where(barber: {id: 109})
barber.styles.each { |style| puts style.name }
Since barber.styles is a collection, I added a loop between all the possible styles you can have. But, from that, you can use your data as you feel like, looping through it or any other way you want.
Hope to have helped!
First off, in your case inverse_of does not accomplish anything, since you are setting the default values. Remove that.
Secondly, it seems you need to better understand the concept of HABTM relationships.
Using has many through is generally a good idea since you can add data and logic to the model in the middle.
It ideally suits your case, so you should set it up like this:
class Barber < User
has_many :barber_styles
has_many :styles, through: :barber_styles
end
class Style < ActiveRecord::Base
has_many :barber_styles
has_many :barbers, through: :barber_styles
end
class BarberStyle < ActiveRecord::Base
belongs_to :barber
belongs_to :style
end
Now it is an easy task to get the styles of a given barber. Just do this:
barber = Barber.find(1)
barber.styles
# => AR::Collection[<#Style name:'Cool Hairstyle'>,<#Style name:'Cooler Hairstyle'>]
Rails automatically uses the BarberStyle model in between to find the styles of a certain barber.
I assume this covers your need, if you have extra information stored only in BarberStyle, let me know.

Rails: specify foreign_key/parent_class at one place when using model namespaces

I decided to move all user logic into separate folder app/models/user/*.
Let's say I have there file main.rb, which defines constant User::Main.
When I ran rspec test, I found that pretty much every association test fails.
Problem is that either Rails does not know that
belongs_to :user
means since there is not User constant anymore.
Either Rails attempts to find wrong foreign key:
User::Main.last.build_api_token
ActiveRecord::UnknownAttributeError: unknown attribute 'main_id' for ApiToken.
So there is a solution: I need to change every belongs_tolike this:
belongs_to :user, class_name: 'User::Main'
(here at least I can use concerns)
and I need to specify every foreign_key
has_many :api_tokens, foreign_key: "user_id"
But making necessary to do this much of additional edits in many different files/lines confuses me.
For first issue I can use concerns module, but what about second one?
How can I set foreign key, by which Rails will work with association, once for all children.
module User
class Main < ActiveRecord::Base
self.accessible_by_key "user_id" #pseudocode to illustrate what I think of
end
end
P.S. I found add_foreign_key docs, but not sure if it is what I look for.
If I get it right, and what you are trying to accomplish is to modularize your User model, You can use the following approach:
Keep user.rb (the model) in the usual place, inside app/models folder.
Create a module inside app/models/concerns; for example user_common.rb.
Inside user_common.rb you can have something this:
#app/models/concerns/user_common.rb
module UserCommon
extend ActiveSupport::Concern
included do
validates :email, format: { with: /\A([^#\s]+)#((?:[-a-z0-9]+\.)+[a-z]{2,})\Z/i }
end
def custom_method
end
...
end
In your User model, just call the module:
#app/models/user.rb
class User < ActiveRecord::Base
include UserCommon
...
end
I hope that helps.

how to avoid repetition with identical models in rails

In my project I have two models which have identical attributes, methods and everything the same.
at the moment they are in app/models/ in separate rb files, with quite some repetition of code.
I would like to export all that code to a separate file and have the two files referring to it, and have mode DRY code.
I've tried the following, but it didn't work:
# app/models/order.rb
class Order < ActiveRecord::Base
before_save { self.version += 1 }
attr_accessible :order
attr_accessible :filled_date
validates :order, :presence => true
end
and one of the referring orders is:
# app/models/real_order.rb
class RealOrder < Order
belongs_to :User, inverse_of: :real_orders
end
but this doesn't work, and I get a Could not find table 'orders' when I try to use the models.
Also I'm thinking that Orders is not a real model, so probably the app/models is not really the right place for that file, though I'm not sure in what directory it should be.
thanks,
UPD1:
The structure I would like to achieve in the end is a situation where I have two identical database tables, with two separate models that are based on the same code. I would like to write such code only once in a separate superclass file. So I'm looking for DRY code, not DRY database.
There are a few different ways to share code between models. If it makes sense (for your problem domain) to use inheritance (as in your example above), then you need the following in your Order class:
self.abstract_class = true
You can also use mixins.
Here's a good question about this: ruby inheritance vs mixins

How many classes is too many? Rails STI

I am working on a very large Rails application. We initially did not use much inheritance, but we have had some eye opening experiences from a consultant and are looking to refactor some of our models.
We have the following pattern a lot in our application:
class Project < ActiveRecord::Base
has_many :graph_settings
end
class GraphType < ActiveRecord::Base
has_many :graph_settings
#graph type specific settings (units, labels, etc) stored in DB and very infrequently updated.
end
class GraphSetting < ActiveRecord::Base
belongs_to :graph_type
belongs_to :project
# Project implementation of graph type specific settings (y_min, y_max) also stored in db.
end
This also results in a ton of conditionals in views, helpers and in the GraphSetting model itself. None of this is good.
A simple refactor where we get rid of GraphType in favor of using a structure more like this:
class Graph < ActiveRecord::Base
belongs_to :project
# Generic methods and settings
end
class SpecificGraph < Graph
# Default methods and settings hard coded
# Project implementation specific details stored in db.
end
Now this makes perfect sense to me, eases testing, removes conditionals, and makes later internationalization easier. However we only have 15 to 30 graphs.
We have a very similar model (to complicated to use as an example) with close to probably 100 different 'types', and could potentially double that. They would all have relationships and methods they inheritated, some would need to override more methods then others. It seems like the perfect use, but that many just seems like a lot.
Is 200 STI classes to many? Is there another pattern we should look at?
Thanks for any wisdom and I will answer any questions.
If the differences are just in the behavior of the class, then I assume it shouldn't be a problem, and this is a good candidate for STI. (Mind you, I've never tried this with so many subclasses.)
But, if your 200 STI classes each have some unique attributes, you would need a lot of extra database columns in the master table which would be NULL, 99.5% of the time. This could be very inefficient.
To create something like "multiple table inheritance", what I've done before with success was to use a little metaprogramming to associate other tables for the details unique to each class:
class SpecificGraph < Graph
include SpecificGraphDetail::MTI
end
class SpecificGraphDetail < ActiveRecord::Base
module MTI
def self.included(base)
base.class_eval do
has_one :specific_graph_detail, :foreign_key => 'graph_id', :dependent => :destroy
delegate :extra_column, :extra_column=, :to => :specific_graph_detail
end
end
end
end
The delegation means you can access the associated detail fields as if they were directly on the model instead of going through the specific_graph_detail association, and for all intents and purposes it "looks" like these are just extra columns.
You have to trade off the situations where you need to join these extra detail tables against just having the extra columns in the master table. That will decide whether to use STI or a solution using associated tables, such as my solution above.

Resources