When it comes to ORM's, ActiveRecord is personal favourite of mine especially considering features such as AREL an AR Callbacks. In solving challenges, there's always multiple ways of skinning a cat, and here I will show you some interesting issues I've tackled today — and how I approached this from a troubleshooting aspect, and the steps I took literally in my head.
Come on Short Round, join me on a riveting adventure!
With great responsibility, comes great use of the around filter
Today I was troubleshooting an issue seen on a client's staging server, which pulls in Agents over an API. One particular issue was that emails were being sent out with invalid login credentials, when I was pretty sure those records were being persisted — even though I did not write the original implementation of this feature.
before_validation :generate_password def generate_password if self.password.nil? self.password = SecureRandom.hex(4) self.send_login_email end end
At first glance this seemed alright, but I later noticed that a bespoke
create_or_update method was being leveraged by relying on
instance#update_attributes, which will in fact save any instances of a
#new_record?. There was also an underlying issue due to the update nature as well, but I will come back to that.
As you may have figured out by now, this callback runs prior to validation, and the email is sent out without any confirmation of whether the instance was actually persisted to disk or not. One approach may consider use of
ActiveModel::Dirty but I decided to take a different route. One should also note that there is a presence validation on the
password attribute, so any new instances with an empty password would fail the call to
#update_attributes. It was really hard to get a sense of what was going on, even with the excellent
pry gem, so I decided that exceptions should be raised rather than failing silently and replaced the call with
#update_attributes!, which was a good move. This made me see exactly why instances were not being persisted.
I basically needed away to check that if a new password is generated for an Agent, and only then send out the email notification after persisting the agent. To the rescue we have the awesome
around_filter. This is simply a method that yields to a block, and the method's binding context is the record itself; the block is the actual save operation.
around_save :generate_password private def generate_password return if self.persisted? pass_generated = false if self.new_record? password = SecureRandom.hex(4) self.password = password self.password_confirmation = password pass_generated = true end yield send_login_email if pass_generated end
I decided to also throw an assumption into the mix to keep this approach sane, and that is the DB should never contain any Agents with a
nil password attribute — which means that we are really only bothered about new Agents. This allowed me to easily update the password columns (for Devise) on a new instance of
Agent, evaluate the block, and then send the notification making sure that a new password was in fact generated.
However, you'll realise that our
around_save call would never be called as our
#update_attributes call will fail at the validation step. Why? We forgot to ensure the password attribute as non-nil during the validation stage. Let's tweak things a bit
before_validation :generate_password_for_new_record around_save :generate_password private def generate_password_for_new_record self.password = SecureRandom.hex(4) if self.new_record? end def generate_password return if self.persisted? pass_generated = false if self.new_record? password = SecureRandom.hex(4) self.password = password self.password_confirmation = password pass_generated = true end yield send_login_email if pass_generated end def send_login_email if Rails.env.production? || non_production_agent_email? Spree::MyAccount::AgentMailer.new(self.email, self.password).deliver end end
Pragmatic Matching, when you don't need a Match object
I'm sharing a particular trick I've come to use on many an occasion when I've found myself needing to verify if a Regex match has taken place, but really don't care for a Match object. Why burden the GC more than we really need to?
def send_login_email if Rails.env.production? || non_production_agent_email? Spree::MyAccount::AgentMailer.new(self.email, self.password).deliver end end def non_production_agent_email? self.email.match(/clientdomain.co.uk/i).length > 0 end
Whilst looking at this potential conundrum with my trusty crowbar, I noticed a
NoMethodError being raised on calling
#length on nil. It was clear that this is lacking some nil-guards.
Sure, we could also proceed with the approach of calling
#match but let's look at away to avoid that by attempting
self.email =~ /clientdomain.co.uk/i. This approach returns the index of where the match occurred or
nil and we can take advantage of that.
def non_production_agent_email? if self.email && self.email.present? return true if self.email =~ /clientdomain.co.uk/i end false end
I also like using what I've come to called the
short circuit, where a method exits early on for a certain condition; if that condition is not met, the default behaviour follows — the predicate method
#non_production_agent_email? above returns
false by default, and
true only if our conditional is met.
However, as Antonis correctly pointed out, since we are in the context of Rails, there is no need for combining both a nil-guard and
#present? as, this returns
It is also good practice to use double-negation for predicate methods as this inherently implies that we are returning a boolean rather than an object (and in Ruby, objects are truthy as well). So how do we convert and object to a boolean? Yup, break out the double-bang... you know you want to!
def non_production_agent_email? !!(self.email.present? && self.email =~ /clientdomain.co.uk/i) end
I hope you've enjoyed our little adventure and I plan on continuing this series of 'Master Classes' in future. Do tell me what you think in the comments below and if you'd like to discuss this further hit me up on Twitter (@bsodmike).
Thanks to Antonis Tzorvas for pointing out an improvement for the
#non_production_agent_email? predicate method.