Polymorphic belongs_to association with dynamic class (taken from other relationship) - ruby-on-rails

I've got a Rails app where Users are able to keep track of airing shows and episodes.
To simplify the process of keeping track of (not yet) watched shows, users are able to synchronize their account with other services. They can, in their user settings page, choose which service they want to synchronize with.
To synchronize, I load their profile from the other service, and then run it through an algorithm which detects changes from the last synchronization, and updates the DB accordingly. In order to store the last synchronization status, for each Show ID, I create a "UsersSyncIdStatus" object which stores the show ID, as well as the current status for that show in the synchronized service.
Note that the services do not use the same Show IDs as my website, which means that I have a table which I can use to "convert" from their show IDs to my show IDs. Since the information each service provides is different, they must be stored in different tables.
Right now, this is (a simplified version of) how the DB schema is set up:
create_table "service1_ids", force: :cascade do |t|
t.integer "service_id", null: false
t.integer "show_id", null: false
[...]
end
create_table "service2_ids", force: :cascade do |t|
t.integer "service_id", null: false
t.integer "show_id", null: false
[...]
end
create_table "users_sync_id_statuses", force: :cascade do |t|
t.integer "user_id"
t.integer "service_id", null: false
t.integer "sync_status", default: 0, null: false
t.datetime "sync_date", null: false
[...]
end
create_table "users", force: :cascade do |t|
[...]
t.datetime "synced_at"
t.boolean "sync_enabled", default: false, null: false
t.integer "sync_method", default: 0, null: false
[...]
end
In particular, users.sync_method is an enum which stores the service the user has selected for synchronization:
SYNC_METHODS = {
0 => {
symbol: :service1,
name: 'Service1',
model_name: 'Service1Id',
show_scope: :service1_ids
}
1 => {
symbol: :service2,
name: 'Service2',
model_name: 'Service2Id',
show_scope: :service2_ids
}
}
This means I can easily know the model name of the IDs of a specific user by just doing SyncHelper::SYNC_METHODS[current_user.sync_method][:model_name].
Now, the question is, how can I have a relationship between "users_sync_id_statuses" and "serviceN_ids"? In order to know which class the "service_id" column corresponds to, I have to 'ask' the user model.
I currently have it implemented as a method:
class User
def sync_method_hash
SyncHelper::SYNC_METHODS[self.sync_method]
end
def sync_method_model
self.sync_method_hash[:model_name].constantize
end
end
class UsersSyncIdStatus
def service_id_obj
self.user.sync_method_model.where(service_id: self.service_id).first
end
end
However, UsersSyncIdStatus.service_id_obj is a method, not a relationship, which means I cannot do all the fancy stuff a relationship allows. For example, I cannot easily grab the UsersSyncIdStatus for a specific user and show ID:
current_user.sync_id_statuses.where(service_id_obj: {show_id: 123}).first
I could turn it into a polymorphic relationship, but I really don't want to have to add a text column to contain the class name, when it is a "constant" from the point of view of each user (for a user to switch synchronization service, all UsersSyncIdStatuses for that user are destroyed, so a user never has more than 1 service type in their UsersSyncIdStatuses).
Any ideas? Thank you in advance!

I don't think vanilla Rails 5 supports what I want to do, someone please correct me if I'm wrong.
Still, after some research into how Rails implements polymorphic relationships, I was able to relatively easily monkey-patch Rails 5 to add this functionality:
config/initializers/belongs_to_polymorphic_type_send.rb:
# Modified from: rails/activerecord/lib/active_record/associations/belongs_to_polymorphic_association.rb
module ActiveRecord
# = Active Record Belongs To Polymorphic Association
module Associations
class BelongsToPolymorphicAssociation < BelongsToAssociation #:nodoc:
def klass
type = owner.send(reflection.foreign_type)
type.presence && (type.respond_to?(:constantize) ? type.constantize : type)
end
end
end
end
app/models/users_sync_id_status.rb:
class UsersSyncIdStatus
belongs_to :service_id_obj, polymorphic: true, foreign_key: :service_id, primary_key: :service_id
def service_id_obj_type
self.user.sync_method_model
end
end
With this monkey-patch, belongs_to polymorphic associations do not assume that the type field is a varchar column, but instead call it as a method on the object. This means you can very easily add your own dynamic behavior, without breaking any old behavior (AFAIK, didn't do intensive testing on that).
For my specific use-case, I have the sync_id_obj_type method query the user object for the class that should be used in the polymorphic association.

Related

How to make a trigger with Rails 7 and PostgreSQL

I'm using PostgreSQL with Rails in API mode and I have a row called expired with data type Date and I want to make a trigger that identifies if the expired date is equal to the current date and change my is_expired column from false to true, but I don't know where to start.
I've read a bit of Rails documentation or some libraries like hairtrigger and it seems a bit confusing.
this is my table:
class CreateRifs < ActiveRecord::Migration[7.0]
def change
create_table :rifs do |t|
t.string :name
t.datetime :rif_date
t.string :award_with_sign
t.string :award_no_sign
t.string :plate
t.integer :year
t.float :price
t.integer :numbers
t.integer :with_signs
t.date :expired
t.boolean :is_expired, default: false
t.timestamps
end
end
end
Do you have a specific reason to use a database column for this? Because you could easily write this method on the model:
def expired? # This is actually an override, see next paragraph
expired <= Date.today
end
Or alternatively, if the expired column only gets populated with a past date after it actually has expired (and doesn't, e.g., represent a future expiry), don't write a method at all. Rails automatically provides you with a predicate for every column: a column? method that returns true if column is populated.
You don't need a trigger for this, maybe a scope to only get expired vs not expired records.
class Rif < ApplicationRecord
scope :expired, -> { where('expired < NOW()') }
end
You can then use the scope later on
expired_rifs = Rif.expired

Create correct inheritance

I have problem with creating proper inheritance between classes in Ruby On Rails.
Idea:
There are 2 classes: Person and Client. Person is a abstract class and Client inherits Person attribute.
Problem:
My solution doesn't work. I don't know why. How can I correctly implement (prefer CTI) inheritance.
Migrations:
create_persons.rb
class CreatePersons < ActiveRecord::Migration
def self.up
create_table :persons do |t|
t.string :pesel, null: false
t.string :first_name, null: false
t.string :last_name, null: false
t.string :email, null: false
t.date :data_of_birth, null: false
t.string :password_digest, null: false
# required for STI
t.string :type
t.timestamps null: false
end
end
def self.down
drop_table :persons
end
end
create_clients.rb
class CreateClients < ActiveRecord::Migration
def change
create_table :clients do |t|
add_foreign_key :persons
t.timestamps null: false
end
end
end
Model Person
class Person < ActiveRecord::Base
self.abstract_class = true
end
Model Client
class Client < Person
end
After db:migrate, when I try Client.create(pesel: "1232",....) there is error:
unknown attribute 'pesel' for Client.
You're getting an error because you've created a clients table in addition to your persons table. If you have a separate table for each class then the only thing that is inherited is the code and not the contents of the database.
Single Table Inheritance (STI) allows you to add a type column and then instances of the parent class and subclasses will be stored in that single table provided the expected table for the subclass isn't present. You've added that type column but you've also created a clients table. This means that ActiveRecord is expecting you to store Client instances in that table instead of the persons - and when it tries to store a Client there it cannot use the field pesel causing your error.
So, if you want to use STI, then you need to remove your clients table and add an client-specific fields to your persons table.
Alternatively, you can keep your clients table and add the fields from persons that you want to also use for clients to the clients table. This wouldn't then be STI but your Client objects would inherit the methods from Person.
I suspect from your inclusion of the type field that you want to get rid of the clients table.
Single Table Inheritance is based upon having a single table to store all the records.
Your problem is that you create a table 'clients' which Rails uses by default for the Client class.
Just rake db:rollback on your last migration and it should look for the superclass table 'people' and work fine.
Edit : Oops, didn't see you mention CTI, this solution only works for STI.

Domain Driven Design for Rails App: Implementing a service in a basic example

Two Models: An Owner and a Dog:
owner.rb
class Owner < ActiveRecord::Base
has_one :dog
end
dog.rb
class Dog < ActiveRecord::Base
belongs_to :owner
end
And here is the schema:
schema.rb
ActiveRecord::Schema.define(version: 123) do
create_table "dogs", force: true do |t|
t.string "name"
t.integer "energy"
t.integer "owner_id"
t.datetime "created_at"
t.datetime "updated_at"
end
add_index "dogs", ["owner_id"], name: "index_dogs_on_owner_id"
create_table "owners", force: true do |t|
t.string "name"
t.string "energy"
t.datetime "created_at"
t.datetime "updated_at"
end
end
Pretty simple setup.
I want an owner to take his dog for a walk. When the walk ends, the owner's energy will drop by 5, AND the dog's energy will drop by 20.
Clearly this walk_the_dog action/method, wherever it is going to live, is effecting two objects: an owner object and a dog object (and of course this dog object happens to be associated to this owner).
I don't know where to put this code. I know I could simply create an action within the owners_controller.rb, but that seems like a bad idea. It would look something like this:
owners_controller.rb
class OwnersController < ApplicationController
def walk_the_dog
#owner = Owner.find(params[:id])
#owner.energy -= 5
#owner.dog.energy -= 20 # this line in particular seems like bad OO design
#owner.save
#owner.dog.save
end
...
end
As I understand it, objects should only change state for themselves and shouldn't change the state of other objects. So this appears to be a bad idea because within the owner controller we are changing the state of not just the owner object, but the associated dog object as well.
I have read about services. It seems like walk_the_dog is an excellent case for a service, because services, as I understand it, allow interactions between objects and state changes for multiple objects. I just don't know how to do it/implement it.
Should there be a service object called walk_the_dog? Should their just be a file within a services directory with a bunch of service methods -- one of which is called walk_the_dog and the owners_controller.rb simply utilizes this method in it's controller? I don't know what the next step is.
Note: I can see someone saying "who cares if this breaks OO design. Just do it and if it works, it works." Unfortunately this is not an option. The application I am working on now followed that mindset. The application grew very large, and now maintaining it is very difficult. I want to get this situation down for the major redesign of the app.
Here are the few things that I would do if I were to refactor this code:
Writing numbers in your code is a bad thing, either you have defined them as constants like ENERGY_PER_WALK_FOR_DOG = 20 or a better way is to define a field in the table of Dog model. This way, it will be much better to manage and assign those values.
add_column :dogs, energy_per_walk, :integer, default: 20
add_column :owners, energy_per_walk, :integer, default: 5
I'd create a method in ApplicationController class:
def walk(resources = [])
resources.each do |resource|
resource.lose_walk_energy # you can refine it more!
end
end
In the folder, app/models/concerns, I would write the following module:
module Walkable
extend ActiveSupport::Concern
# subtract energy_per_walk form the energy saved in db
def lose_walk_energy
self.energy -= self.energy_per_walk
save
end
end
And now, your method reduces to the following method:
def walk_the_dog
#owner = Owner.find(params[:id])
walk([#owner, #owner.dog])
end
I would say that this should be a method in the Owner model. You also need to do both operations in one transaction, to ensure, that both models have been updated.
class Owner
has_one :dog
def walk_the_dog
return false if dog.nil?
transaction do
decrement!(:energy, 5)
dog.decrement!(:energy, 20)
end
end
end

Rails: RSPEC test for updating active record entry is failing

I have two models: Draft and Pick. I want the Draft's ActiveRecord column current_pick to increase by 1 after a Pick is created.
Inside Pick, I have a method that increase draft.current_pick by 1:
class Pick < ActiveRecord::Base
belongs_to :draft
after_save :advance_draft
def advance_draft
draft.updraft
end
Inside draft, updraft is:
def updraft
self.current_pick += 1
end
My test to ensure the current_pick is being increased by one is:
it 'should run the advance_draft method after creating' do
team1 = FactoryGirl.create(:team)
team2 = FactoryGirl.create(:team_two)
cam = FactoryGirl.create(:player)
draft = FactoryGirl.create(:two_team_draft)
pick = Pick.create(:player_id => cam.id, :team_id => draft.team_at(draft.current_pick).id, :draft_id => draft.id, :draft_position => draft.current_pick)
draft.draft_position.should eq 2
end
The pick and draft are being created in the test but the updraft method is not being called on the correct draft because the pick.draft.draft_position remains at 1 after the pick has been created. (it should increase to 2).
Here is my schema:
create_table "drafts", force: true do |t|
t.integer "draft_position"
t.integer "number_of_teams"
t.integer "PPTD"
t.integer "PPR"
t.integer "current_pick", default: 1
end
create_table "picks", force: true do |t|
t.integer "player_id"
t.integer "team_id"
t.datetime "created_at"
t.datetime "updated_at"
t.integer "draft_id"
t.integer "draft_position"
end
My question is, how do I properly increase the pick.draft.current_pick inside my test?
I would do 2 things, use increment! when updating the count to increment the value and save the record, and reload the object you're looking at, since its database representation has changed since it was created.
def updraft
increment!(:current_pick)
end
This will update the current_pick and save the object in one shot.
it 'should run the advance_draft method after creating' do
# ... setup
draft.reload.draft_position.should eq 2
end
Now, instead of using the instance of draft that was created before the modification occurred, it's using a version current with the database.

Creating a list of ActiveRecord associations for different types of "task" objects

I currently have a generic task table called tasks that contains the essentials of what any task would require (common attributes).
create_table "tasks", force: true do |t|
t.integer "agent_id"
t.datetime "start"
t.text "note"
t.string "status"
t.datetime "create_date"
end
I also have multiple types of tasks that I want related to a tasks object, such as followup and review_sales_office. Both of these will contain different details, but they are all technically types of tasks.
create_table "followups", force: true do |t|
t.integer "account_id"
t.integer "task_id"
t.integer "account_contact_id"
t.string "contact_method"
end
create_table "review_sales_offices", force: true do |t|
t.integer "sales_office_id"
t.integer "task_id"
t.integer "sales_office_rep"
end
These all work fine if I just want to grab a list of all followup tasks, or a list of all reviews sales office tasks, but how would I go about retrieving a list of all tasks sorted by task_datetime that when output to a view would be clickable to the respective type of task (task 1 and 2 might be a followup, task 3 might be a sales office review).
I can do a Task.joins(...) to get a collection of all tasks (which I think is the correct way to do this), but the task itself isn't necessarily aware of the type of task it is, so I'm not sure how I would generate the appropriate link based on the task type (the link would ideally go to something like an edit page for the appropriate task type).
Any advice?
Word of the day is: polymorphic association!
All you need is:
1) Add task_details_type:string and task_details_id columns to your Task table.
2) Add in your Task model
belongs_to :task_details, polymorphic: true
3) In each Task type add:
has_one :task, as: task_details
And you have a simple model, which will give right task_details without you concerning of its type. In additon you can make it a wrapper:
def method_missing(name, args*, &block)
return task_details.public_send(name, *args, &block) if task_details && task_details.respond_to? name
super
end
Unfortunately it is not possible to declare polymorphic has_one association, so we need to live with putting nonsenses like 'task belongs_to task_details' - tough live.

Resources