How to define method names and object references before actually having to use them (Ruby) - ruby

Say I am keeping track of email correspondances. An enquiry (from a customer) or a reply (from a supporter) is embedded in the order the two parties are corresponding about. They share the exact same logic when put into the database.
My problem is that even though I use the same logic, the object classes are different, the model fields I need to call are different, and the method names are different as well.
How do I put methods and objects references in before I actually have to use them? Does a "string_to_method" method exists or something like that?
Sample code with commentaries:
class Email
include Mongoid::Document
field :from, type: String
field :to, type: String
field :subject, type: String
belongs_to :order, :inverse_of => :emails
def start
email = Email.create!(:from => "sender#example.com", :to => "recipient#example.com", :subject => "Hello")
from_or_to = from # This represents the database field from where I later on will fetch the customers email address. It is either from or to.
enquiries_or_replies = enquiries # This represents a method that should later be called. It is either enquiries or replies.
self.test_if_enquiry_or_reply(from_or_to, enquiries_or_replies)
end
def test_if_enquiry_or_reply(from_or_to, enquiries_or_replies)
order = Order.add_enquiry_or_reply(self, from_or_to, enquiries_or_replies)
self.order = order
self.save
end
end
class Order
include Mongoid::Document
field :email_address, type: String
has_many :emails, :inverse_of => :order
embeds_many :enquiries, :inverse_of => :order
embeds_many :replies, :inverse_of => :order
def self.add_enquiry_or_reply(email, from_or_to, enquiries_or_replies)
order = Order.where(:email_address => email.from_or_to).first # from_or_to could either be from or to.
order.enquiries_or_replies.create!(subject: email.subject) # enquiries_or_replies could either be enquiries or replies.
order
end
end

Judging by the question and the code sample, it sounds like you are mixing concerns too much. My first suggestion would be to re-evaluate your method names and object structure. Ambiguous names like test_if_thing1_or_thing2 and from_or_to (it should just be one thing) will make it very hard for others, and your future self, to understand the code laster.
However, without diverging into a debate on separation of concerns, you can change the methods you call by using public_send (or the private aware send). So you can do
order.public_send(:replies).create!
order.public_send(:enquiries).create!

string to method does exist, it's called eval
so, you could do
method_name = "name"
eval(method_name) #calls the name method

Related

How to read attributes from another model in Rails

I still would consider myself new to Rails. I'm implementing a SMS feature in Rails app that reminds clients of their upcoming appointments. My question is, I have the SMS method in my appointment model, but my client model is where the phone attribute is located. How do I call my phone attribute from the appointment model.
Here is my appointment model
class Appointment < ApplicationRecord
enum status: { confirmed: 0, rescheduled: 1, cancelled: 2}
belongs_to :user
belongs_to :client
validates :start_time, presence: true
validates :end_time, presence: true
after_create :reminder
def reminder
#twilio_number = ENV['TWILIO_NUMBER']
account_sid = ENV['TWILIO_ACCOUNT_SID']
#client = Twilio::REST:Client.new account_sid, ENV['TWILIO_AUTH_TOKEN']
time_str = ((self.start_time).localtime).strftime("%I:%M%p on %b. %d, %Y")
reminder = "Hi #{client.name}. Just a reminder that you have an appointment coming up at #{time_str}."
message = #client.api.account(account_sid).messages.create(
:from => #twilio_number,
:to => client.phone_number,
:body => reminder,
)
end
My client model
class Client < ApplicationRecord
has_many :appointments
has_many :users, through: :appointments
scope :clients_by, ->(user) { where(user_id: user.id) }
end
Based on my current associations setup. In the reminder variable couldn't I just call
reminder = "Hi #{client.name}.?
And for
:to => client.phone_number
to access the phone_number attribute?
Yes your assumption is correct, you can just call client.<attribute>.
However, be aware of the dreaded SELECT N+1 issue. So let's say you do something like
Appointment.all.each do {|a| a.reminder }
If you have 50 appointments, this results in 51 calls to the database, one call to load all the appointments and then a bunch of calls to load each client one by one.
To avoid this issue, you can make use of includes, eager_loads, or preload which all load the associated data more efficiently than individual queries.
The difference between those three methods is covered very well in this article http://blog.scoutapp.com/articles/2017/01/24/activerecord-includes-vs-joins-vs-preload-vs-eager_load-when-and-where. I've quoted a TL;DR excerpt below.
I'd roughly summarize my approach to these methods like this:
If I'm just filtering, use joins.
If I'm accessing relationships, start with includes.
If includes is slow using two separate queries, I'll use eager_load to force a single query and compare performance.
There are many edge cases when accessing relationships via ActiveRecord. Hopefully this is enough to prevent some of the more basic performance deadends when using joins, includes, preload, and eager_load.
Following the advice of that article, we'd rewrite my example as
Appointment.all.includes(:client).each do {|a| a.reminder }

How to verify if an embedded field changed on before_save?

I am running Ruby 2.1 and Mongoid 5.0 (no Rails).
I want to track on a before_save callback whether or not an embedded field has changed.
I can use the document.attribute_changed? or document.changed methods to check normal fields, but somehow these don't work on relations (embed_one, has_one, etc).
Is there a way of detecting these changes before saving the document?
My model is something like this
class Company
include Mongoid::Document
include Mongoid::Attributes::Dynamic
field :name, type: String
#...
embeds_one :address, class_name: 'Address', inverse_of: :address
#...
before_save :activate_flags
def activate_flags
if self.changes.include? 'address'
#self.changes never includes "address"
end
if self.address_changed?
#This throws an exception
end
end
One example of how I save my document is:
#...
company.address = AddressUtilities.parse address
company.save
#After this, the callback is triggered, but self.changes is empty...
#...
I have read the documentation and Google the hell out of it, but I can't find a solution?
I have found this gem, but it's old and doesn't work with the newer versions of Mongoid. I want to check if there is another way of doing it before considering on trying to fix/pull request the gem...
Adding these two methods to your Model and calling get_embedded_document_changes should provide you an hash with the changes to all its embedded documents:
def get_embedded_document_changes
data = {}
relations.each do |name, relation|
next unless [:embeds_one, :embeds_many].include? relation.macro.to_sym
# only if changes are present
child = send(name.to_sym)
next unless child
next if child.previous_changes.empty?
child_data = get_previous_changes_for_model(child)
data[name] = child_data
end
data
end
def get_previous_changes_for_model(model)
data = {}
model.previous_changes.each do |key, change|
data[key] = {:from => change[0], :to => change[1]}
end
data
end
[ source: https://gist.github.com/derickbailey/1049304 ]

Mongoid model with hardcoded data

I have a mongoid model
class MyMongoidModel
include Mongoid::Document
include Mongoid::Timestamps
field :name, :type => String
field :data_id, :type => Integer
has_and_belongs_to_many :the_other_model, :class_name => 'class_name_model'
has_many :model2
def self.all
[
#.... the hardcoded data that will never be changed
]
end
end
it's used by the other model and it uses them as well. However, it contains the data that won't be changed for a very long time, let's say, at all. Thus, I don't want to retrieve it from db, I want it to be hardcoded and, at the same time, I want it acts like a normal mongoid model. Using caching is not what I'm looking for.
I hope you understand what I mean.
How do accomplish it?
There's a great gem called active_hash that provides this functionality for ActiveRecord: defining a fixed set of data as models you can reference/relate to normal models, but have it defined in code and loaded in memory (not stored/retrieved from DB).
https://github.com/zilkey/active_hash
Interestingly, since Mongoid and ActiveRecord both share common ActiveModel basis, you may be able to use active_hash with a Mongoid document now.
For example:
class Country < ActiveHash::Base
self.data = [
{:id => 1, :name => "US"},
{:id => 2, :name => "Canada"}
]
end
class Order
include Mongoid::Document
include Mongoid::Timestamps
has_one :country
end

Querying and sorting embedded document in mongoid

I have three classes
class Post
include Mongoid::Document
include Mongoid::Timestamps
belongs_to :user, :inverse_of => nil
embeds_many :comments, :as => :commentable
field :content, :type => String
end
class Commment
include Mongoid::Document
include Mongoid::Timestamps
belongs_to :user, :inverse_of => nil
embedded_in :commentable, :polymoriphic => true
end
class User
has_many :posts, :dependent => :destroy
field :name, :type => String
end
Whenever the user creates a new comment, I want to compare the contents of it with the latest comment that the user has made. Here is my code that fetches the latest comment by the user:
comments_user=[]
Post.where("comments.user_id" => user.id).
only(:comments).
each {|p| comments_user += p.comments.where(:user_id => user.id).to_a}
latest_comment = comments_user.sort_by{|comment| comment[:updated_at]}.reverse.first
The above code gives me the result but the approach taken is not efficient as I have to traverse through all the posts that the user has commmented to find the latest comment. If any, can anyone provide me a more efficient solution to this problem?
Simply speaking, Isn't there any way I can get all the comments made by this user?
This should fetch the latest user`s comment:
Post.where("comments.user_id" => user.id).order_by(:'comments.updated_at'.desc).limit(1).only(:comments).first
This is standard problem with embedding. It greatly improves some queries ("load post with all its comments"), but makes others non-efficient/impractical ("find latest comment of a user").
I see two options here:
Keep embedding and duplicate data. That is, when user makes a comment, embed this comment to a post document and to the user document. This data duplication has its drawbacks, of course (what if you need to edit comments?);
Stop embedding and start referencing. This means that comment is now a top level entity. You can't quickly load a post with comments, because there are no joins. But other queries are faster now, and there's no data duplication.

activerecord validation - validates_associated

I'm unclear on what this method actually does or when to use it.
Lets say I have these models:
Person < ...
# id, name
has_many :phone_numbers
end
PhoneNumber < ...
# id, number
belongs_to :person
validates_length_of :number, :in => 9..12
end
When I create phone numbers for a person like this:
#person = Person.find(1)
#person.phone_numbers.build(:number => "123456")
#person.phone_numbers.build(:number => "12346789012")
#person.save
The save fails because the first number wasn't valid. This is a good thing, to me. But what I don't understand is if its already validating the associated records what is the function validates_associated?
You can do has_many :phone_numbers, validate: false and the validation you're seeing wouldn't happen.
Why use validates_associated then? You might want to do validates_associated :phone_numbers, on: :create and skip validation on update (e.g. if there was already bad data in your db and you didn't want to hassle existing users about it).
There are other scenarios. has_one according to docs is validate: false by default. So you need validates_associated to change that.

Resources