In our Rails application, the Post resource can be made by either a User or an Admin.
Thus, we have an ActiveRecord model class called Post, with a belongs_to :author, polymorphic: true.
However, in certain conditions, the system itself is supposed to be able to create posts.
Therefore, I'm looking for a way to add e.g. System as author.
Obviously, there will only ever be one System, so it is not stored in the database.
Naïvely attempting to just add an instance (e.g. the singleton instance) of class System; end as author returns errors like NoMethodError: undefined method `primary_key' for System:Class.
What would be the cleanest way to solve this?
Is there a way to write a 'fake' ActiveRecord model that is not actually part of the database?
There's two ways that I see that make the most sense:
Option A: Add a 'system' Author record to the DB
This isn't a horrible idea, it just shifts the burden onto you making sure certain records are present in every environment. But you can always create these records in seed files if you want to ensure they're always created.
The benefit over option B is that you can just use standard ActiveRecord queries to find all of the system's Posts.
Option B: Leave the association nil and add a new flag for :created_by_system
This is what I would opt for. If a Post was made by the system, just leave the author reference blank and set a special flag to indicate this model was created internally.
You can still have a method to quickly get a list of all of them just by making a scope:
scope :from_system, -> { where(created_by_system: :true) }
Which one you choose I think depends on whether you want to be able to query Post.author and get information about the System. In that case you need to take option A. Otherwise, I would use option B. I'm sure there's some other ways to do it too but I think this makes the most sense.
Finally I ended up with creating the following 'fake' model class that does not require any changes to the database schema.
It which leverages a bit of meta-programming:
# For the cases in which the System itself needs to be given an identity.
# (such as when it does an action normally performed by a User or Admin, etc.)
class System
include ActiveModel::Model
class << self
# The most beautiful kind of meta-singleton
def class
self
end
def instance
self
end
# Calling`System.new` is a programmer mistake;
# they should use plain `System` instead.
private :new
def primary_key
:id
end
def id
1
end
def readonly?
true
end
def persisted?
true
end
def _read_attribute(attr)
return self.id if attr == :id
nil
end
def polymorphic_name
self.name
end
def destroyed?
false
end
def new_record?
false
end
end
end
Of main note here is that System is both its own class and its own instance.
This has the following advantages:
We can just pass Post.new(creator: System) rather than System.new or System.instance
There is at any point only one system.
We can define the class methods that ActiveRecord requires (polymorphic_name) on System itself rather than on Class.
Of course, whether you like this kind of metaprogramming or find it too convoluted is very subjective.
What is less subjective is that overriding ActiveRecord's _read_attribute is not nice; we are depending on an implementation detail of ActiveRecord. Unfortunately to my knowledge there is no public API exposed that could be used to do this more cleanly. (In our project, we have some specs in place to notify us immediately when ActiveRecord might change this.)
Related
Motivation
The motivation was that I want to embed the serialization of any model that have been included in a Relation chain. What I've done works at the relation level but if I get one record, the serialization can't take advantage of what I've done.
What I've achieved so far
Basically what I'm doing is using the method includes_values of the class ActiveRecord::Relation, which simply tells me what things have been included so far. I.e
> Appointment.includes(:patient).includes(:slot).includes_values
=> [:patient, :slot]
To take advantage of this, I'm overwriting the as_json method at the ActiveRecord::Relation level, with this initializer:
# config/initializers/active_record_patches.rb
module ActiveRecord
class Relation
def as_json(**options)
super(options.merge(include: includes_values)) # I could precondition this behaviour with a config
end
end
end
What it does is to add for me the option include in the as_json method of the relation.
So, the old chain:
Appointment.includes(:patient).includes(:slot).as_json(include: [:patient, :slot])
can be wrote now without the last include:
Appointment.includes(:patient).includes(:slot).as_json
obtaining the same results (the Patient and Slot models are embedded in the generated hash).
THE PROBLEM
The problem is that because the method includes_values is of the class ActiveRecord::Relation, I can't use it at the record level to know if a call to includes have been done.
So currently, when I get a record from such queries, and call as_json on it, I don't get the embedded models.
And the actual problem is to answer:
how to know the included models in the query chain that retrieved the
current record, given that it happened?
If I could answer this question, then I could overwrite the as_json method in my own Models with:
class ApplicationRecord < ActiveRecord::Base
self.abstract_class = true
extend Associations
def as_json(**options)
super(options.merge(include: included_models_in_the_query_that_retrieved_me_as_a_record))
end
end
One Idea
One Idea I have is to overwrite the includes somewhere (could be in my initializer overwriting directly the ActiveRecord::Relation class, or my ApplicationRecord class). But once I'm there, I don't find an easy way to "stamp" arbitrary information in the Records produced by the relation.
This solution feels quite clumsy and there might be better options out there.
class ApplicationRecord < ActiveRecord::Base
def as_json(**options)
loaded_associations = _reflections.each_value
.select { |reflection| association(reflection.name).loaded? }
.map(&:name)
super(options.merge(include: loaded_associations))
end
end
Note that this only loads 1st level associations. If Appointment.includes(patient: :person) then only :patient will be returned since :person is nested. If you plan on making the thing recursive beware of circular loaded associations.
Worth pointing out is that you currently merge include: ... over the provided options. Giving a user no choice to use other include options. I recommend using reverse_merge instead. Or swap the placements around {includes: ...}.merge(options).
I have Customer and each customer has_many Properties. Customers belong to a Company.
I'm trying to add a certain Property to each one of a single Company's Customers. I only want this change to happen once.
I'm thinking about using a migration but it doesn't seem right to create a migration for a change that I only ever want to happen once, and only on one of my users.
Is there a right way to do this?
You can just use rails console.
In rails c:
Company.where(conditions).last.customers.each do |customer|
customer.properties << Property.where(condition)
customer.save!
end
Validation
Depending on how you're changing the Customer model, I'd include a simple vaidation on the before_update callback to see if the attribute is populated or not:
#app/models/Customer.rb
class Customer < ActiveRecord::Base
before_update :is_valid?
private
def is_valid?
return if self.attribute.present?
end
end
This will basically check if the model has the attribute populated. If it does, it means you'll then be able to update it, else it will break
--
Strong_Params
An alternative will be to set the strong_params so that the attribute you want to remain constant will not be changed when you update / create the element:
#app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
...
private
def strong_params
params.require(:model).permit(:only, :attributes, :to, :update)
end
end
It would be much more helpful if you explained the context as to why you need this type of functionality - that will give people the ability to create a real solution, instead of proposing ideas
I have two User objects existing_user, current_user, how should I traverse the User.attributes.keys and check if they are equal.
Probably something like this:
class User < ActiveRecord::Base
# ...
# untested, but the logic seems sound.
def equals?(user)
User.attributes.keys.each do |k|
return false unless self[k] == user[k]
end
true
end
end
You could then call current_user.equals?(existing_user).
The background of this question is not completely clear to me. Usually when one is refering to current_user like you do, the background is ruby-on-rails, not only ruby like stated in the subject. I guess, you are getting the current_user via an external login-process (cas, facebook, etc) and internally keep a user-table for additional attributes.
Given my assumption is true, I would recommend, to identify one uniq attribute of the external user-model and store it in your internal user-table. You might want to validate this attributes uniqueness. When making your comparison, restrict to comparing this single attribute on the externally logged-in user and the internally kept users.
I'm trying to do this
has_many :roles, :before_add => :enforce_unique
def enforce_unique(assoc)
false if exists? assoc
end
From the docs: "If a before_add callback throws an exception, the object does not get added to the collection". The using false above does not prevent the add, so I'm forced to do this:
def enforce_unique(assoc)
raise if exists? assoc
end
This way, it's true that it doesn't get added, but it also raises an exception that has to be handled. Not very useful to me here. I would prefer this to behave more like regular AR callback before_save, where returning FALSE also prevents the save (or add) but doesn't raise an exception.
In this case above, I would prefer this to just not add the assoc silently. Is there a way to do this? I missing something? Or is raising an exception the only option here?
The way to solve it I think is to use throw and catch, which in Ruby are meant for flow control. Raising an exception is not a good fit, since this isn't an exceptional circumstance.
I ended up doing:
catch(:duplicate) do
association.create({})
end
And then in the before_add callback, I did:
if(Class.where({}).first)
throw :duplicate
end
More on throw/catch here:
http://rubylearning.com/blog/2011/07/12/throw-catch-raise-rescue-im-so-confused/
If the association isn't polymorphic you can do something like:
validates_uniqueness_of :name_of_model
inside of Role where name_of_model us what you are associating with
this question is a bit old, but i came across the same problem recently. here is how i solved it:
def enforce_unique |obj, x|
v = obj.roles
if i = v.index(x)
v.slice! i
end
end
Since this question is about saving rather than preventing it being included in the list temporarily (eg by a controller that is not interested in controlling its models) you could try overriding save in the related model and just not save it if the role exists:
class Role < ActiveRecord::Base
belongs_to :user, inverse_of: :roles
def save
super unless self.new_record? && user.has_existing_role?(self)
end
end
Sidenote: I don't buy the skinny controller argument when used with the Active Record pattern as business logic has to be put somewhere. With a business domain poor pattern like Active Record (not referring to the Ruby AR gem specifically) it really needs to exist a layer above (ie in the controller layer), you may use service objects or the decorator pattern as a means to achieve this.
Another approach would be to override update methods like << for the association and silently drop the role if it matches an existing one. Details on overriding association methods are in the ActiveRecord Association Class Methods Documentation
I need to cache (and expire) all the models in a table.
For example, if i have a model named Currency, i only have less than 10 possible currencies. Therefore, it would be nice to have:
class Currency < ActiveRecord::Base
cache_all(:expire_in => 10.minutes)
end
so that
Currency.all
Currency.find_by_name("USD")
should not hit the DB.
What do you think it could be a good approach?
Also, if you believe it would be better to use a model that is not backed up by a DB, please comment on this. Please notice that i would like to have a AR-style association.
Since the data set is so small, probably the best thing is to cache it in local memory. There are a couple ways to do this, one is to use Memoization like I show here. However that isn't the most efficient because it will store the all method and find_by_name method in separate caches even though they are the same object.
An alternative is to redefine the methods to cache the objects manually. Something like this.
class Currency < ActiveRecord::Base
def self.all
#all_cache ||= super.map(&:freeze) # freeze so you don't modify the cached objects
end
def self.find_by_name(name)
all.detect { |c| c.name.to_s.downcase == name.to_s.downcase }
end
def self.flush_all_cache
#all_cache = nil
end
end
There may be a plugin to handle this for you, but I haven't looked into that at all.