Search code examples
rubyperformanceruby-on-rails-4where-clauserails-postgresql

How can I combine two Rails 4 where queries that query either side of a boolean condition?


Question:

Is there a way to combine the following two queries (including the assignments) into one query? I'm not sure how much time I'd really save. In other words, I'm not sure if it is worth it, but I'd like to be as efficient as possible.

@contacts = @something.user.contacts.where.not(other_user_id: 0)
@user_contacts = @something.user.contacts.where(other_user_id: 0)

More context:

Here is my contacts table from schema.rb:

  create_table "contacts", force: true do |t|
    t.string   "name"
    t.string   "email"
    t.integer  "user_id"
    t.datetime "created_at"
    t.datetime "updated_at"
    t.string   "profile_picture"
    t.string   "phone_number"
    t.integer  "other_user_id"
  end

And here is the important stuff from the users table:

  create_table "users", force: true do |t|
    t.string   "email"
    t.datetime "created_at"
    t.datetime "updated_at"
    ...
    t.string   "first_name"
    t.string   "second_name"
  end

And here is the pertinent information of the models:

class Contact < ActiveRecord::Base
  belongs_to :user

  VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-]+(\.[a-z]+)*\.[a-z]+\z/i

  validates :name, presence: true
  validates :email, presence: true, format: { with: VALID_EMAIL_REGEX }
  validates :user_id, presence: true

  def get_email_from_name
    self.email
  end
end

[...]

class User < ActiveRecord::Base
  has_many :contacts
  has_many :relationships,
           foreign_key: 'follower_id',
           dependent: :destroy
  has_many :reverse_relationships,
           foreign_key: 'followed_id',
           class_name: 'Relationship',
           dependent: :destroy
  has_many :commitments,
           class_name: 'Commitment',
           dependent: :destroy
  has_many :followers,
           through: :reverse_relationships
  has_many :followed_users,
           through: :relationships,
           source: :followed
  [...]

  before_save { email.downcase! || email }
  VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-]+(\.[a-z]+)*\.[a-z]+\z/i
  validates :email,
            presence: true,
            format: { with: VALID_EMAIL_REGEX },
            uniqueness: { case_sensitive: false }

  [...]

  def follow!(other_user)
    relationships.create!(followed_id: other_user.id)
    if create_contact?(self, id, other_user.id)
      create_contact(other_user.name,
                           other_user.email,
                           self.id,
                           other_user.id,
                           other_user.gravatar_url)
    elsif create_contact?(other_user, other_user.id, id)
      create_contact(name, email, other_user.id, id, gravatar_url)
    end
  end

  def create_contact?(user, followed_id, follower_id)
    user.admin? && ! Relationship.where(followed_id: followed_id, follower_id: follower_id).empty?
  end

  def create_contact(name, email, user_id, other_user_id, profile_picture)
    Contact.create!(name: name,
                         email: email,
                         user_id: user_id,
                         other_user_id: other_user_id,
                         profile_picture: profile_picture)
  end

  def unfollow!(other_user)
    relationships.find_by(followed_id: other_user.id).destroy
    Contact.destroy_all(user_id: self.id, other_user_id: other_user.id)
  end

  [...]
end

The other contacts that may not have an account with the website (yet), and I'd like to distinguish that in the view. So I keep track of which contacts I import through Google contacts using the omniauth gem. For the other contacts, I gather the other users that are friends with current_user.

Goal:

I'd like to save these two record collections to use in the view, but I'd like to avoid looking through all the user's contacts twice, checking the same column in each pass-through.

Any ideas? I'm sure there are lots of ways this can be done, and I'd like to learn as much as I can from this! Thanks in advance!


Solution

  • You can use Array#partition to split up the array in memory, after the query was performed.

    @user_contacts, @contacts = @something.user.contacts.partition{|u| other.id == 0 }
    

    However checking for this magic 0 id is smelly. You should try to get rid of such special cases whenever possible.