Dude where’s my association?

I ran into an issue I had never faced recently when validating changes made to associated models.

Context:

Let’s suppose we have the following models (non-related info removed):

class User < ApplicationRecord
  has_many :rules
  has_many :activities
end
class Rule < ApplicationRecord
  belongs_to :user
  def check!(activity)
    #PSEUDO CODE
    unless allowed_actions.include?(activity.action) && allowed_tags.include?(activity.tags)
      activity.add_error("Not allowed to do this activity")
    end
  end
end
class Activity < ApplicationRecord
   belongs_to :user
   has_and_belongs_to_many :tags
   validates_with ActivityValidator
end
class Tag < ApplicationRecord
   has_and_belongs_to_many :activities
   # Tags contain information regarding the activity and rules can 'invalidate' an activity based on certain tags
   # E.g Rule does not allow User X to do any activities tagged with 'dangerous'
end
class ActivityValidator < ActiveModel::Validator
  IGNORED_ATTRIBUTES = ["status"]
  def validate(activity)
    # Allow certain changes without validating rules again
    no_changes_present   = (activity.changed_attributes.keys - IGNORED_ATTRIBUTES).empty? 
    return if no_changes_present
    activity.user.rules.each do |rule|
      rule.check!(activity)
    end
  end
end

Issue

Maybe the more seasoned Rails devs have already noticed the problem.

The issue I faced was that when changing any of the attributes on the activity (excluding the ignored status) the Rule was correctly evaluated. However, if there was a Rule which was based on checking Tags when making any changes to just the associated tags the validator passed even with ‘invalid’ Tags.

What was going on? Why aren’t my associations being validated?

First port of call was to slap in a binding.pry to see what no_changes_present and to my suprise it was returning true even when I was either deleting or adding Tags.

It was at this point I found out that activity.changed_attributes (and by extension #changes) ONLY covers changes to that model’s direct attributes and does not include changes to any associated models. Docs for changed_attributes

Yea I know a real ‘duh’ moment (the clue is in the name of the method) but due to less than perfect tests (which I will write more on in another post) this went unnoticed for too long.

Solution

The solution I chose is pretty simple, add the following to the no_changes_present check.

&& !activity.tags.any? { |tag| tag.marked_for_destruction? || tag.new_record? }

Now if a Tag is added (new_record?) or removed (marked_for_destruction?) no_changes_present will now be false and the validation will be carried out as normal.

One gotcha to remember is that we need to do a similar thing for the check! method on the Rule. However this time we only want to validate against Tags that are already associated or newly associated so:

def check!(activity)
  #PSEUDO CODE
  active_tags = activity.tags.reject { | tag | tag.marked_for_destruction? }
  unless allowed_actions.include?(activity.action) && allowed_tags.include?(active_tags)
    activity.add_error("Not allowed to do this activity")
  end
end

This way we can remove the due to be deleted tags to give us a correct validation before saving the edited Activity.

Now I am aware there are probably a million ways I could’ve achieved the same result so I would love to hear from anyone out there who has any suggestions on how else this could be implemented.

Til next time..

Presence in Rails

Some of the most common methods I use on a daily basis when writing if/else’s and guard clauses are ‘presence checkers’. It can be really helpful to know if the Array, String, object etc. has is not just ’empty’/[]/false/nilas your existing logic may fail if passed such data. However Ruby (and Rails) have a number of methods to achieve this and as a junior dev I often found myself checking which to use in each specific scenario.

nil? blank? empty? which one to use?

The methods I come back to the most are blank? and present? (which is actualy just an alias for !blank?) because they offer an extra layer of safety in their return:

blank?present?
niltruefalse
falsetruefalse
truefalsetrue
0falsetrue
1falsetrue
""truefalse
" "truefalse
[]truefalse
[nil]falsetrue
{}truefalse
{a: nil}falsetrue

Either way you are guarenteed a true or false response and can build your logic around that boolean. Other methods are more restrictive:

  • nil? will only return true when passed nil any other value results in false which restricts its usefulness to certain situations.

  • empty? can only be used on Enumerables that is Hash’s ({}) and Array’s ([]) and String’s (" "), trying to use it on an Integer will result in:

=> 0.empty?
NoMethodError: undefined method `empty?' for 0:Integer
  • There is even zero? which is restricted to just Integers and responds true on 0 and false on any other number.

  • Rails, more specifically ActiveRecord offers exists? which when used against an ActiveModel::Model can be used for checking the DB for the existance of a model that matches the parameters passed:

# For a given model of 'Dog'
    Dog.exists?(5) # Checks for the ID of the model in the DB
    Dog.exists?('5') # See above
    Dog.exists?(['name LIKE ?', "%#{query}%"]) # Specify other attributes to query for
    Dog.exists?(name: 'Good Boy') # See above
    Dog.exists?(id: [1, 4, 8]) # Check multiple IDs at once
    Dog.exists?(false) # Return False
    Dog.exists? # Checks if there are any instances of Dog in the DB
    Dog.where(name: 'Skye', good_boy: true).exists? # More specific querying is possible

Further reading on exists?

Performance and presence checking

Unless you are Twitter scale it doesn’t matter.

Of course there are differences between using the different methods against different data types (as can be seen here in this comprehensive SO answer). However for most projects these presence checks will not be a performance bottle neck.

For example my favoured blank? is actually 2x slower than using empty?, this can easily be seen by looking at the source code:


def blank?
  respond_to?(:empty?) ? !!empty? : !self
end

However in most projects this performance hit will not be noticable.

One caveat to the above statement is when dealing with ActiveRecord checks which hit the DB. The topic of performance in those queries is quite involved and is expertly handled in this post which I would encourage anyone interested to read.

Thats all folks

LinkedIn::Messenger.send(`Mike Warren`)  if questions.present?

Hello World

Starting off with one of the most over-used tropes in programming to introduce another, a tech/random thoughts/brain dump blog.

What this blog is:

  • A place for me to write breakdowns of new topics that I encounter while working as a back-end Ruby on Rails dev
  • An opportunity to improve my understanding of CS related topics via writing hopefully useful guides/ TIL (Today I learned) style posts
  • A distraction from the bug tickets I am supposed to be fixing…

What this blog isn’t:

  • A comprehensive source of knowledge on any topics posted
  • The ‘right’ way to do things
  • Interesting (I don’t expect anyone to read this but if one person can be helped by anything written here then that would be great)

Hopefully I can share a few useful things on here and if anyone wants to get in contant please see my GH and LinkedIn linked above.

Cheers!