Re-structuring a legacy app I wanted to re-use one of the model names because it is too perfect not to.
I want to make it totally invisible to the outside, JSON API, preferably even the controller layer, that these models do not have the default table name. But I cannot figure out the final puzzle piece. aliasing the foreign keys without making a chain and breaking it all.
So I have two tables. The old table perfections which I will be migrating away from, but not in one go unfortunately. Then the new table imperfections which will be taking over.
I can easily mask the table names:
class Perfection < ApplicationRecord
self.table_name = "imperfections"
end
class LegacyPerfection < ApplicationRecord
self.table_name = "perfections"
end
Models holding a foreign key to these though. That is my problem. In fact I have one problem model. It belongs to both these models. I can alias only one of the foreign keys to look like what I want from the outside but aliasing both I get an alias chain because of the shared name.
class OtherThing < ApplicationRecord
belongs_to :perfection, foreign_key: :imperfection_id
belongs_to :legacy_perfection, foreign_key: :perfection_id, optional: true
alias_attribute :legacy_perfection_id, 'perfection_id'
alias_attribute :perfection_id, 'imperfection_id'
end
I totally see why this makes an alias chain. The aliases are effectively doing the equivalent to this (and more for the getters snd stuff)
class OtherThing < ApplicationRecord
belongs_to :perfection, foreign_key: :imperfection_id
belongs_to :legacy_perfection, foreign_key: :perfection_id, optional: true
def legacy_perfection_id=(value)
self.perfection_id = value
end
def perfection_id=(value)
self.imperfection_id = value
end
end
If I call other_thing.legacy_perfection_id = 1 this number will be saved into imperfection_id.
I wonder if I can do something else... like actually change the attribute names somehow, and end up with a model where the parameters you pass to create or update are not revealing the internals.
For now I will do the transformation in the controller but I would like to have it be even cleaner.
The input parameters you pass to your model are not really linked to its attributes or columns - they are linked to setter methods. assign_attributes basically just does:
attributes.each do |k, v|
send("#{k}=", v)
end
So if you want the model to accept new_attribute as input but still assign the old attribute you can do it with:
def new_attribute=(value)
self.old_attribute = value
end
In some cases though it can be useful to have adapters. Like if API v1 accepts old_attribute and API v2 accepts new_attribute. To keep the difference from leaking into the model layer you add an adapter that transforms the params before you assign them. This is actually a controller concern in MVC as the controller is responsible for accepting user input and passing it to models.
Related
I'm building an application where users are part of an Organisation. An organisation has many Lists, which in turn have many ListItems.
Now, I would like for admin users to be able to specify which attributes are available on list items, based on the organisation they belong to (or rather, on the organisation their list belongs to), without having to touch any code.
So far, when defining attributes that are not bound to a specific column in the database, I have used document_serializable, a nifty little gem (based on virtus) which serializes virtual attributes to a JSONB column in the db. I like this approach, because I get all of virtus' goodies (types, coercion, validations, etc.), and because data ends up sitting in a JSONB column, meaning it can be loaded quickly, indexed, and searched through with relative ease.
I would like to keep using this approach when adding these user-defined attributes on the fly. So I'd like to do something like:
class ListItem < ApplicationRecord
belongs_to :list
delegate :organisation, to: :list
organisation.list_attributes.each do |a, t|
attribute a, t
end
end
Where Organisation#list_attributes returns the user-defined hash of attribute names and their associated types, which, for example, might look like:
{
name: String,
age: Integer
}
As you might have guessed, this does not work, because organisation.list_attributes.each actually runs in the context of ListItem, which is an instance of Class, and Class doesn't have an #organisation method. I hope that's worded in a way that makes sense1.
I've tried using after_initialize, but at that point in the object's lifecycle, #attribute is owned by ActiveRecord::AttributeMethods::Read and not DocumentSerializable::ClassMethods, so it's an entirely different method and I can't figure out wether I can still access the one I need, and wether that would even work.
Another alternative would be to find the organisation in question in some explicit way, Organisation#find-style, but I honestly don't know where I should store the information necessary to do so.
So, my question: at the moment of instantiating (initializing or loading2) a record, is there a way I can retrieve a hash stored in a database column of one of its relations? Or am I trying to build this in a completely misguided way, and if so, how else should I go about it?
1 To clarify, if I were to use the hash directly like so:
class ListItem < ApplicationRecord
belongs_to :list
delegate :organisation, to: :list
{
name: String,
age: Integer
}.each do |a, t|
attribute a, t
end
end
it would work, my issue is solely with getting a record's relation at this earlier point in time.
2 My understanding is that Rails runs a model's code whenever a record of that type is created or loaded from the database, meaning the virtual attributes are defined anew every time this happens, which is why I'm asking how to do this in both cases.
at the moment of instantiating (initializing or loading) a record, is
there a way I can retrieve a hash stored in a database column of one
of its relations?
Yes. This is fairly trivial as long as your relations are setup correctly / simply. Lets say we have these three models:
class ListItem < ApplicationRecord
belongs_to :list
end
class List < ApplicationRecord
belongs_to :organisation
has_many :list_items
end
class Organisation < ApplicationRecord
has_many :lists
end
We can instantiate a ListItem and then retrieve data from anyone of its parents.
#list_item = ListItem.find(5) # assume that the proper inherited
foreign_keys exist for this and
its parent
#list = #list_item.list
#hash = #list.organisation.special_hash_of_org
And if we wanted to do this at every instance of a ListItem, we can use Active Record Callbacks like this:
class ListItem < ApplicationRecord
belongs_to :list
# this is called on ListItem.new and whenever we pull from our DB
after_initialize do |list_item|
puts "You have initialized a ListItem!"
list = list_item.list
hash = list.organisation.special_hash_of_org
end
end
But after_initialize feels like a strange usage for this kind of thing. Maybe a helper method would be a better option!
I have a Character class and a Siblingship class.
To denote that one character is a "sibling" of another, I save a Siblingship instance with a character_id (the "primary" Character being edited) and a sibling_id (the character being marked as a sibling to the primary character).
I would like to add functionality to automatically create a second Siblingship instance with the reverse IDs (so if I mark Alice as Bob's sibling, I also mark Bob as Alice's sibling).
This would be an easy after_add if I put the logic in the Character class:
class Character < ActiveRecord::Base
has_many :siblingships
has_many :siblings, through: :siblingships, after_add: :reciprocate
def reciprocate(sibling)
...
end
end
However, this is a larger project with ~100 different relations like this (connecting a dozen different "content" classes, not just Characters), and storing the after_add (and, often, a reversed after_remove) on the Character model would get very unwieldy fast.
So I'd like to store the logic of "what to do when an association of this type is created" on that association, instead of the class that holds that association.
Something like:
class Character < ActiveRecord::Base
has_many :siblingships
has_many :siblings, through: :siblingships, after_add: Siblingship.reciprocate
end
Is there a good way to do this? I've also tried specifying reciprocate as an instance method, like
has_many :siblings, through: :siblingships, after_add: Siblingship.new.reciprocate
But not only does it error [wrong number of arguments (given 0, expected 1)] on the method, but it also feels wrong to be instantiating Siblingships here just to get to an instance method.
I would very much appreciate any solutions on how to solve this problem while keeping the Character class clean, and preferably keeping logic about each relation in that relation's class.
It looks like this can be solved with simple hooks on the joining model (e.g. Siblingship) instead of trying to add them to the relevant class they join (e.g. Character).
The code's a bit abstracted out from the concrete example I gave in the question because I needed a solution that worked across unlimited joining classes with minimal code duplication. Here's what I ended up doing to get two-way linking (both on create and delete) working:
class Character < ActiveRecord::Base
has_many :siblingships
has_many :siblings, through: :siblingships
end
And then, for each joining class:
class Siblingship < ActiveRecord::Base
include SmartContentLinking
LINK_TYPE = :two_way
belongs_to :character
belongs_to :sibling, class_name: 'Character'
# Since this is a two-way relation, also create an opposite relation
after_create do
self.reciprocate relation: :siblingships, parent_object_ref: :character, added_object_ref: :sibling
end
# Since this is a two-way relation, also delete any opposite relation
after_destroy do
this_object = Character.find_by(id: self.character_id)
other_object = Character.find_by(id: self.sibling_id)
other_object.siblings.delete this_object
end
end
Obviously there are still things to pull out to make the code a lot cleaner (and entirely abstracted into the SmartContentLinking concern), but this works for now.
The SmartContentLinking concern:
require 'active_support/concern'
module SmartContentLinking
extend ActiveSupport::Concern
# Default linking to one-way. All possible values:
# - :one_way
# - :two_way
LINK_TYPE = :one_way
included do
def reciprocate relation:, parent_object_ref:, added_object_ref:
parent_object = self.send(parent_object_ref)
added_object = self.send(added_object_ref)
# if some_character.siblingships.pluck(:sibling_id).include?(parent_object.id)
if added_object.send(relation).pluck("#{added_object_ref}_id").include?(parent_object.id)
# Two-way relation already exists
else
# If a two-way relation doesn't already exist, create it
added_object.send(relation) << relation.to_s.singularize.camelize.constantize.create({
"#{parent_object_ref}": added_object, # character: sibling
"#{added_object_ref}": parent_object # sibling: character
})
end
end
end
end
Let's ignore ActiveRecord and look at what you're trying to achieve.
When a Character has a new sibling added, the sibling's existing
sibling relationships needs to be realigned.
You mentioned that this is a project with ~100 similar relations. So, we might generalize the above to:
When a new relationship is formed, existing relationships on both
sides need to be realigned.
I'm going to work with the first definition as I don't know the rest of your domain. You might change this example based on your better understanding of your domain.
Based on the definition, the code might look something like:
class Character
def add_sibling_relationship(another_character)
siblings.add(another_character)
another_character.realign_sibling_relationships(self)
end
def realign_sibling_relationships(sibling)
siblings.add(sibling)
end
end
Everything is still in Character, which is going to be a problem once the relationships grow. So let's decouple the process from the ActiveRecord model.
We're talking about siblinghood here, so that's what we'll call the resulting class:
class Siblinghood
def initialize(sibling_one, sibling_two)
#sibling_one, #sibling_two = sibling_one, sibling_two
end
def form
#sibling_one.siblings << #sibling_two unless #sibling_one.siblings.include?(#sibling_two)
#sibling_two.siblings << #sibling_one unless #sibling_two.siblings.include?(#sibling_one)
end
end
This changes how we create siblings. Instead of
#character.siblings.add(#other_character)
we now need to call:
Siblinghood.new(#character, #other_character).form
Let's take it a step further. If we wanted to remove this sibling relationship, where would a logical place be to put that logic?
Siblinghood.new(#character, #other_character).destroy
Now, we have somewhere to consolidate the logic of managing siblinghood. However, this means our controllers and other classes need to be aware of Siblinghood, which isn't necessary - no other class cares about how siblings are formed.
Let's move invocation back into Character.
class Character
def add_sibling(other_character)
Siblinghood.new(self, other_character).form
end
def remove_sibling(other_character)
Siblinghood.new(self, other_character).destroy
end
end
This gives us a nice balance - Character only implements as many methods as external classes need to know, and all the logic required for sibling management is tucked away neatly.
Context:
Each Order has many Items & Logistics. Each Item & Logistic (as well as the Order itself) have many Revenues.
I am creating Order + Items & Logistics at once using an accepts_nested_attributes_for on Order. However, Revenues gets created using an after_create callback on each of the models Order, Item, and Logistics. Why? Because given the difference in interpretation in these models, the code reads cleaner this way. (But if this way of doing it is what's causing this question to be asked, I will obviously reconsider!)
One key attribute that I need to store in Revenues is pp_charge_id. But pp_charge_id is not something that either Order, Items, or Logistics needs to worry about. I've attached an attr_accessor :pp_charge_id to Order, so that one works fine, however, once I'm in the child Items or Logistics models, I no longer have access to pp_charge_id which again I need to save an associated Revenue. How should I do this?
Controller Code:
#order = Order.new(params) #params includes Order params, and nested params for child Item & Logistics
#order.pp_charge_id = "cash"
#order.save #I need this to not only save the Order, the children Item & Logistics, but then to also create the associated Revenue for each of the aforementioned 3 models
ORDER Model Code:
has_many :items
has_many :revenues
attr_accessor :pp_charge_id
after_create :create_revenue
def create_revenue
self.revenues.create(pp_charge_id: self.pp_charge_id)
end
#This WORKS as expected because of the attr_accessor
ITEM/ LOGISTIC model code:
has_many :revenues
belongs_to :order
after_create :create_revenue
def create_revenue
self.revenues.create(pp_charge_id: self.order.pp_charge_id)
end
#This DOES NOT work because self.order.pp_charge_id is nil
ORDER model code:
belongs_to :order
belongs_to :item
belongs_to :logistic
Again I understand the attr_accessor is not designed to persist across a request or even if the Order itself is reloaded. But it also doesn't make sense to save it redundantly in a table that has no use for it. If the only way to do this is to put the pp_charge_id into the params for the order and save everything all at once (including Revenues), then let me know because I know how to do that. (Again, would just rather avoid that because of how it's interpreted: params are coming from User, Revenue data is something I'm providing)
I think if you want the order's pp_charge_id to apply to all its items and logistics, I'd put all that into the order's after_create callback:
# order.rb
def create_revenue
revenues.create(pp_charge_id: pp_charge_id)
items.each {|i| i.revenues.create(pp_charge_id: pp_charge_id)}
logistics.each {|l| l.revenues.create(pp_charge_id: pp_charge_id)}
end
EDIT: Alternately, you could add inverse_of to your belongs_to declarations, and then I believe Item#create_revenue would see the same Order instance that you set in the controller. So if you also added an attr_accessor to the Item class, you could write its create_revenue like this:
# item.rb
def create_revenue
revenues.create(pp_charge_id: pp_charge_id || order.pp_charge_id)
end
This should cover the new requirement you've mentioned in your comment.
instead of using after_create and accessors you should consider having a proper method that does exactly what you need, ie:
Order.create_with_charge(:cash, params)
i find it disturbing to persist redundant information in the database just because the code reads cleaner that way!
Here is my setup, followed by an explanation of what I am trying to accomplish.
class Layer < ActiveRecord::Base
has_and_belongs_to_many :components
end
class Component < ActiveRecord::Base
has_and_belongs_to_many :layers
end
class ImageComponent < Component
# I want this table to inherit from the Component table
# I should be able to add image-specific fields to this table
end
class VideoComponent < Component
# I want this table to inherit from the Component table
# I should be able to add video-specific fields to this table
end
What I want to be able to do:
layer.components << ImageComponent.create
layer.components << VideoComponent.create
In practice, I realize that ImageComponent and VideoComponent will actually have to inherit from ActiveRecord::Base. Is there any way to nicely implement model subclassing in Rails?
Right now I have my Component model setup to be polymorphic such that ImageComponent and VideoComponent each has_one :component, as: :componentable. This adds a layer of annoyance and ugliness to my code though:
image_component = ImageComponent.create
component = Component.create
component.componentable = image_component
layer.components << component
I guess a simple way to explain this is that I want to implement a habtm relationship between Layers and Components. I have multiple types of Components (i.e. ImageComponent, VideoComponent) that each have the same base structure but different fields associated with them. Any suggestions on ways this can be accomplished? I feel that I am missing something because my code feels hackish.
The "official" way to achieve this in Rails is to use Single Table Inheritance. Support for STI is built into ActiveRecord: http://api.rubyonrails.org/classes/ActiveRecord/Base.html#class-ActiveRecord::Base-label-Single+table+inheritance
If you want to use Multi Table Inheritance you would have to implement it by yourself...
here the main issue is between the Component and its types and not Layer and Component. i had a similar problem. will explain the solution specific to ur problem.
Store the type(Image/Video) as resource for Component and have a controller for Component and not all the types()
let the model structure be as
Component < ActiveRecord::Base
accepts_nested_attributes_for :resource
belongs_to :resource, :polymorphic => true, :dependent => :destroy
def resource_attributes=(params = {})
self.resource = spec_type.constantize.new unless self.resource
self.resource.attributes = params.select{|k| self.resource.attribute_names.include?(k) || self.resource.class::ACCESSOR.include?(k.to_sym)}
end
#component will be either image or video and not both
Image < ActiveRecord::Base
has_one :component, as :resource
Video < ActiveRecord::Base
has_one :component, as :resource
and a single controller as ComponentsController for CRUD of Component. Since the Component accepts attributes for resource(ie image/video), u can save the component as well as the resource and add normal validations for each resource.
the basic view for adding a Component can be as
= form_for(#component, :url => components_path, :method => :post) do |f|
= fields of Component
= f.fields_for :resource, build_resource('image') do |image|
= fields of resource Image
= f.fields_for :resource, build_resource('video') do |video|
= fields of resource Video
the fields for Image/Video can be added using the helper method
module ComponentsHelper
def build_resource(klass)
klass = "{klass.capitalize}"
object = eval("#{klass}.new")
if #component.resource.class.name == klass
object = #component.resource
end
return object
end
end
since the Component can have only one related resource(image/video), u need to select the the resource type on the view(in my case it was a dropdown list) and depending upon the selected resource show its fields and hide/remove all other resources fields(if image is selected, remove video fields using javascript). When the form is submitted, the method from Component model filters out all the key-value pairs for the intended resource and creates the component and its related resource.
Also
1) keep the field names for each resource unique cause when the form is submitted, the hidden resource(unwanted resources) fields are submitted which overwrite the intended resource fields.
2) the above model structure gives problem for resource attr_accessor only(they are not accessible on rails console). it can be solved as
ACCESSOR = ['accessor1', 'accessor2'] #needed accessors
has_one :component, :as => :resource
attr_accessor *ACCESSOR
See how to implement jobpost functionality that has 3 fixed categoris
i hope this helps.
With STI, you are sharing the same table with several model classes, so if you want subclassed models to have unique fields (database columns), then they need to be represented in that common table. From the comments in your example, it appears that this is what you want.
There is a trick you can do, however, which involves having a string column in the table that each model can use to store custom serialized data. In order to do this, it has to be OK that these data elements aren't indexed, because you won't be able to easily search on them within SQL. Let's say you call this field aux. Put this in the parent model:
require 'ostruct'
serialize :aux, OpenStruct
Now let's say you want the fields called manager and experience in a subclassed model, but none of the other STI models need this field and you won't need to search based on these attributes. So you can do this in the subclassed model:
# gets the value
def manager
return self.aux.manager
end
# sets the value
def manager=(value)
self.aux.manager = value
end
# gets the value
def experience
return self.aux.experience
end
# sets the value
def experience=(value)
self.aux.experience = value
end
In this example, single table inheritance still works fine and you also get custom persistant attributes for subclassed models. This gives you the benefits of sharing code and database resources among several models, but also allows each model to have unique attributes.
I have a couple of models that are composites of multiple objects. I basically manage them manually for saves and updates. However, when I select items out, I don't have access to the associated properties of said item. For example:
class ObjectConnection < ActiveRecord::Base
def self.get_three_by_location_id location_id
l=ObjectConnection.find_all_by_location_id(location_id).first(3)
r=[]
l.each_with_index do |value, key|
value[:engine_item]=Item.find(value.engine_id)
value[:chassis_item]=Item.find(value.chassis_id)
r << value
end
return r
end
end
and each item:
class Item < ActiveRecord::Base
has_many :assets, :as => :assetable, :dependent => :destroy
When I use the ObjectLocation.find_three_by_location_id, I don't have access to assets whereas if use Item.find(id) in most other situations, I do.
I tried using includes but that didn't seem to do it.
thx
Sounds like the simplest solution would be to add methods to your ObjectConnection model for easy access like so:
class ObjectConnection < ActiveRecord::Base
def engine
Engine.find(engine_id)
end
def chassis
Chassis.find(chassis_id)
end
# rest of class omitted...
I'm not exactly sure what you're asking... If this doesn't answer what you're asking, then can you try to be a little bit more clear with what exactly you are trying to accomplish? Are the Chassis and Engine mdoels supposed to be polymorphic associations with your Item model?
Also, the code you're using above won't work due to the fact that you are trying to dynamically set properties on a model. It's not your calls to Item.find that are failing, it's your calls to value[:engine_item]= and value[:chassis_item] that are failing. You would need to modify it to be something like this if you wanted to keep that flow:
def self.get_three_by_location_id location_id
l=ObjectConnection.find_all_by_location_id(location_id).first(3)
r=[]
l.each_with_index do |obj_conn, key|
# at this point, obj_conn is an ActiveRecord object class, you can't dynamically set attributes on it at this point
value = obj_conn.attributes # returns the attributes of the ObjectConnection as a hash where you can then add additional key/value pairs like on the next 2 lines
value[:engine_item]=Item.find(value.engine_id)
value[:chassis_item]=Item.find(value.chassis_id)
r << value
end
r
end
But I still think that this whole method seems unnecessary due to the fact that if you setup proper associations on your ObjectConnection model to begin with, then you don't need to go and try to handle the associations manually like you're attempting to do here.