Generic model that has many relationship depending on value of enum - ruby-on-rails

I have the following class:
class Blog < ApplicationRecord
enum platform: {
wordpress: 'wordpress',
drupal: 'drupal',
}
has_many :wordpress_posts
has_many :drupal_posts
end
Besides platform, it also holds things like url and category. It also has a relationship of has many with WordPressPost and DrupalPost:
class WordPressPost < ApplicationRecord
belong_to :blog
end
class DrupalPost < ApplicationRecord
belongs_to :blog
end
I would like to know, if it's possible to infere what has_many relationship should be the valid, depending on the platform value: if the platform value is wordpress, the blog should only contain relationships with wordpress post entities. I'm not sure if there is a Rails way to solve it. I would love if someone can help me and show me the proper Rails way of implement such data model.
Thanks

Your idea of using a enum won't work here since assocations are class level and the value of the enum is only known on the instance level.
If you really wanted to use an enum you could hack something together with an instance method but it won't really behave like an assocation when it comes to stuff like eager loading:
class Blog < ApplicationRecord
# ...
def posts
send("#{platform}_posts")
end
end
What you can do is use Single Table Inheritance to setup classes that share a table yet have different behavior.
First add a type column to the table:
class AddDetailsToBlogs < ActiveRecord::Migration[6.0]
def change
change_table :blogs do |t|
t.remove :platform
t.string :type, index: true, null: false
end
end
end
If you have existing data you should go through it and set the type column based on the value of platform before you drop platform and make type non-nullable.
Then setup the subclasses:
class Blog < ApplicationRecord
# shared behavior
end
class WordPressBlog < Blog
has_many :posts,
class_name: 'WordPressPost',
foreign_key: :blog_id,
inverse_of: :blog
end
class DrupalBlog < Blog
has_many :posts,
class_name: 'DrupalPost',
foreign_key: :blog_id,
inverse_of: :blog
end
The main advantage of STI is that it lets you query as a single table and thus treat it as a homogenous collection, the drawbacks are that you are potentially wasting database space with columns containing largely nulls and it can become quite unweildy if the types differ to much from each other.

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.

How to deal with different type of users with rails?

I need to know, if there is a way to deal with different type of users/clients in Rails using a single model.
What I really need to do?
- I need to save a different type of clients in my database. So, I have this migration:
class CreateClients < ActiveRecord::Migration
def change
create_table :clients do |t|
t.string :name # Person name if "Personal", Company name if type "Company"
t.string :nif # fillable only if type is Company
t.string :identity_card_number # fillable only if type is Personal
t.integer :client_type # Type can be Personal or Company
t.references :company, index: true # if type is personal, it can belong to a company
t.timestamps null: false
end
end
end
Then I create this model
class Client < ActiveRecord::Base
has_many :employees, class_name: 'Client', foreign_key: 'company_id'
belongs_to :company, class_name: 'Client'
end
Note: A personal account can belong to a company or not.
Based on your experience, am I doing this in the right way? There are another way to do that?
EDIT:
Hi #manfergo25,
Whit this I have another question. "Company" and "Personal" are both "Clients Account", in that way, both must be able to buy services.
If I need to associent the client with the service, can I do this?
class Personal < Account
has_many :services
end
and
class Service < ...
belongs_to :account
end
??
The right way is Single Table Inheritance (STI) as Sontya say.
class Account < ActiveRecord::Base
end
Then,
class Client < Account
end
class Provider < Account
end
You only have to add a type column in 'Account' to contain a string representing the type of the stored object.
For example in a controller you could do this:
account = Client.find(params[:autocomplete_client])
params[:service][:account_id] = account.id
#service = Service.new(params[:service])
You can use STI(Single Table Inheritance)
class Account < ActiveRecord::Base
end
class Company < Account
has_many :services, :dependent => :destroy
end
class Personal < Account
has_many :services, :dependent => :destroy
end
class Service < ActiveRecord::Base
belongs_to: personal
belongs_to: company
end
With the above definition, a personal and company should be able to buy services.
and you should be able to call
#company.services # it will return you the number of services of company
#personal.services # it will return you the number of services of personal
Personally, I think the way you have suggested in your question is the way I'd do it.
While Single Table Inheritance is intellectually better modelling -- I've found STI to be a bit hard to work with and sometimes unreliable in Rails. And probably won't improve the cleanliness or conciseness of your code all that much in the end anyway. STI is good to keep in mind as an option, if you find the OP approach is not working well as far as allowing you to write clear concise code and it seems STI could work out better.
But I'd start without STI, with just the one Client class, as you've outlined in your question. If you later add STI, you'd still have the Client class, you'd just have sub-classes for, say, PersonalClient and CompanyClient. It won't be that hard to switch to STI later if you want to, although it might require some minor db schema alterations.
But I don't think it'll get you enough benefit to justify the added complexity, in an area of Rails that has sometimes had some rough edges.
Here's some more info about STI and it's plusses and minuses: http://eewang.github.io/blog/2013/03/12/how-and-when-to-use-single-table-inheritance-in-rails/

Passing a child to container of parent type

I have a parent class Individual and child classes Student and Professor in my rails application.
Inheritance is handled with a gem called 'acts_as_relation' which simulates multiple table inheritance.
In addition, I have an action within which a student instance is appended to a list of individuals. Normally I would have expected this to go through without any problems but I get this error:
ActiveRecord::AssociationTypeMismatch: Individual(#70220161296060) expected, got Student(#70220161349360)
Here is a glance at my model:
class Individual < ActiveRecord::Base
acts_as_superclass
end
class Student < ActiveRecord::Base
acts_as :individual
end
class Professor < ActiveRecord::Base
acts_as :individual
end
I've not used this gem, but to give you some help, here's what I've found and this:
They both mention that you're calling an object through your relation, which will have confusion over polymorphism or similar. The two posts could not fix the issue, and I presume that is because they could find the correct object for their relationship
Looking at this further, I found this tutorial on the gem homepage:
acts_as_relation uses a polymorphic has_one association to simulate
multiple-table inheritance. For the e-commerce example you would
declare the product as a supermodel and all types of it as acts_as
:product (if you prefer you can use their aliases is_a and
is_a_superclass)
class Product < ActiveRecord::Base
acts_as_superclass
end
class Pen < ActiveRecord::Base
acts_as :product
end
class Book < ActiveRecord::Base
acts_as :product
end
To make this work, you need to declare both a foreign key column and a
type column in the model that declares superclass. To do this you can
set :as_relation_superclass option to true on products create_table
(or pass it name of the association):
create_table :products, :as_relation_superclass => true do |t|
# ...
end
Or declare them as you do on a polymorphic belongs_to association, it
this case you must pass name to acts_as in :as option:
change_table :products do |t|
t.integer :producible_id
t.string :producible_type
end
class Pen < ActiveRecord::Base
acts_as :product, :as => :producible
end
class Book < ActiveRecord::Base
acts_as :product, :as => :producible
end
Are you sure you've got your datatables set up correctly?
The way I've solve this in my projects, is using instance_ofsome_class.individuals << student_instance.individual.
The thing here is that is not a real MTI, so your collection of individuals would accept only individuals instances. If you call some_student_instance.individual or some_professor_instance.individual, you'll get an individual instance which is related with your specific instance.
Then working with that collection, if you want a Student or Professor all you need to do is call individual_in_collection.specific. For example:
p = Professor.create
a_model.individuals << p.individual
puts "#{a_model.individuals.first.class.name}"
=> Individual
puts "#{a_model.individuals.first.specific.class.name}"
=> Professor

Using Dropdown menu for One to One relationship

I plan on making a genotype calculator in the future. I intend for this calculator to eventually be able to compute the following from a pairing: probability of color listing all possibilities, genotype.
I am wanting to make a dropdown menu/text field combination on a very simple webpage to learn how it works so that I can continue my project and hopefully meet this goal. I have searched and tried to figure this out, but I am pretty lost. Currently in my database I have a table called "colors" with the following schema:
id
angora_color
genotype
created_at
updated_at
I do not intend for users to be able to add data to this form. I want them to be able to select a color from the dropdown box, and get the genotype in a text field below it.
My code so far is as follows:
class Color < ActiveRecord::Base
has_one :genotype
end
class Genotype < ActiveRecord::Base
has_one :color
end
index.html.erb:
<h2>Placeholder for Genotype List..</h2>
class PagesController < ApplicationController
def index
end
end
I appreciate any help.
Are you sure you only want a has_one relationship? Wouldn't a Genotype have many colors? and Colors can be part of many Genotypes?
You also can't have both models declare has_one. One model has to belong to the other. And the one that belongs_to should have the foreign key as <model_name>_id e.g. genotype_id. In your table you only put genotype. Rails looks for that _id.
What may be better here is to use has_many through. Create a join model such as genotypes_colors:
rails g model GenotypesColor genotype_id:integer color_id:integer
Then change your code to look like:
class Genotype < ActiveRecord::Base
has_many :genotypes_colors
has_many :colors, through: :genotypes_colors
end
class GenotypesColor < ActiveRecord::Base
belongs_to :genotype
belongs_to :color
end
class Color < ActiveRecord::Base
has_many :genotypes_colors
has_many :genotypes, through: :genotypes_colors
end
Now you can correctly relate a Genotype to its Colors. You can use fields_for in either model's forms to create the genotypes_color association that will relate a Genotype to any Color or vice versa. If this sounds about right let me know and I can further help on how to do the forms.
Right now my migration reads as follows:
class CreateColors < ActiveRecord::Migration
def change
create_table :colors do |t|
t.string :angora_color
t.string :genotype
t.timestamps
end
end
end

Rails: polymorphic assoziation, has_many :through

Im not sure i understand rails polymorphic.
In Java you can create Objects from the same Objecttype:
http://www.fh-kl.de/~guenter.biehl/lehrgebiete/java2/j2-08-Dateien/abb.8.10.jpg
Person trainer = new Trainer()
Person sportler = new Trainer()
In Rails http://guides.rubyonrails.org/association_basics.html#polymorphic-associations:
In this example: picture can be from an employee or from a product, sounds strange because this is not realy the same type.
Do i understand the real purpose: to save objects in the same container an array of person or image?
In my rails project: I have several person: sportsmen, trainer and guest. They are sons of person (inheritance).
I think i meet the inheritance reason.
There is another class named exercise.
Sportsmen and trainer can create exercises.
So i want to use polymorphic. Exercises can be from trainer or sportsmen. Like in the example of the rails page, images can be from employee or a product.
Do i meet the best practise?
How do i implement a has_many :through with polymorphy?
It is not possible to use a habtm assoziation with polymorphic.
You have to define a additional class, but how exactly?
I think you want single table inheritance (STI) models, not a polymorphic relationship.
See this article http://www.alexreisner.com/code/single-table-inheritance-in-rails and these stackoverflow answers Rails - Single Table Inheritance or not for Applicant/Employee relationship Alternative to Rails Single Table Inheritance (STI)?
Just to make it clear, you should use polymorphic associations when you have a model that may belong to many different models on a single association.
Suppose, you want to be able to write comments for users and stories. You want both models to be commendable. Here's how this could be declared:
class Comment < ApplicationRecord
belongs_to :commentable, polymorphic: true
end
class Employee < ApplicationRecord
has_many :comment, as: :commentable
end
class Product < ApplicationRecord
has_many :comment, as: :commentable
end
To declare the polymorphic interface (commendable) you need to declare both a foreign key column and a type column in the model.
class CreateComments < ActiveRecord::Migration
def change
create_table :comments do |t|
t.text :body
t.integer :commentable_id
t.string :commentable_type
t.timestamps
end
add_index :comments, :commentable_id
end
end
You can check more details about associations here.

Resources