views:

203

answers:

0

In my User model, I want to search for whether a user has multiple accounts (aka 'multis').

Users have sessions; sessions are customized to set

  1. creator_id = ID of first user the session was logged in as, and
  2. updater_id = ID of last user the session was logged in as

(Both columns are indexed.)

If I detect that the two are different when a session is saved, I save the session for posterity (and this query), because it means that the user logged in as one and then logged in as the other - aka they're a multi. (To catch the recursive base case, the creator_id is then reset on the current session.)

Here's the code that does it:

class Session < ActiveRecord::SessionStore::Session
  attr_accessor :skip_setters

  before_save :set_ip
  before_save :set_user

    def set_user
      return true if self.skip_setters
      # First user on this session
      self.creator_id ||= self.data[:user_id] 
      # Last user on this session
      self.updater_id = self.data[:user_id] if self.data[:user_id] 
      if self.creator_id and self.updater_id and 
       self.creator_id != self.updater_id
        logger.error "MULTI LOGIN: User #{self.creator.login} and \
          #{self.updater.login} from #{self.ip}"

        # Save a copy for later inspection
        backup = Session.new {|dup_session| 
          dup_session.attributes = self.attributes
          # overwrite the session_id so we don't conflict with the 
          # current one & it can't be used to log in
          dup_session.session_id = ActiveSupport::SecureRandom.hex(16)
          dup_session.skip_setters = true
        }
        backup.save

        # Set this session to be single user again.
        # Updater is what the user looks for;
        # creator is the one that's there to trigger this.
        self.creator_id = self.updater_id
      end
    end

  # etc... e.g. log IP
end

The following query does work.

However, it's ugly, not very efficient, and doesn't play well with Rails' association methods. Keep in mind that sessions is an extremely large and heavily used table, and users almost as much.

I'd like to change this into a has_many association (so that all the associational magic works); possibly :through => a :multi_sessions association. It should capture both directions of multihood, not just one like the current association.

How can this be improved?

class User < ActiveRecord::Base
 has_many :sessions, :foreign_key => 'updater_id'

 # This association is only unidirectional; it won't catch the reverse case
 # (i.e. someone logged in first as this user and then as the other)
 has_many :multi_sessions, :foreign_key => 'updater_id', 
  :conditions => 'sessions.updater_id != sessions.creator_id',
  :class_name => 'Session'
 has_many :multi_users, :through => :multi_sessions,
  :source => 'creator', :class_name => 'User'

 ...

 # This does catch both, but is pretty ugly :(
 def multis
  # Version 1
  User.find_by_sql "SELECT DISTINCT users.* FROM users \
    INNER JOIN sessions \
      ON (sessions.updater_id = #{self.id} XOR sessions.creator_id = {self.id}) AND \
         (sessions.updater_id = users.id XOR sessions.creator_id = users.id) \
    WHERE users.id !=  #{self.id}"
  # Version 2
  User.find(sessions.find(:all, :conditions => 'creator_id != updater_id', 
    :select => 'DISTINCT creator_id, updater_id').map{|x|
      [x.creator_id, x.updater_id]}.flatten.uniq - [self.id])
 end
end