module ConnectionManager module SecondaryConnectionBuilder def database_name "#{connection.instance_variable_get(:@config)[:database].to_s}" end def secondary_association_options(method,association,class_name,options={}) new_options = {}.merge(options) if new_options[:class_name].blank? new_options[:class_name] = "#{association.to_s.singularize.classify}::#{class_name}" else new_options[:class_name] = "#{new_options[:class_name]}::#{class_name}" end if [:has_one,:has_many].include?(method) && new_options[:foreign_key].blank? new_options[:foreign_key] = "#{table_name.singularize}_id" end new_options end def build_secondary_associations(class_name) str = "" defined_associations.each do |method,defs| unless defs.blank? defs.each do |association,options| options = {} if options.blank? unless options[:no_readonly] || options[:class_name].to_s.match("::#{class_name}") str << "#{method.to_s} :#{association}, #{secondary_association_options(method,association,class_name,options)};" end end end end str end def secondary_connection_classes(options) if options[:using] && options[:using].is_a?(Array) connection_classes = options[:using].collect{|c| Connections.connection_class_name(c)} else rep_name = "#{options[:name].to_s}_#{Connections.clean_sqlite_db_name(database_name)}" connection_classes = Connections.available_secondary_connections(rep_name) end connection_classes end def replicated(*settings) options = {:name => "slave", :readonly => true, :replication => true}.merge(settings.extract_options!) build_secondary_connections(options) end def sharded(*settings) options = {:name => "shard", :readonly => false, :shards => true}.merge(settings.extract_options!) build_secondary_connections(options) end # Adds subclass with the class name of the type provided in the options, which # defaults to 'slave' if blank, that uses the connection from a connection class. # If :database option is blank?, replicated will assume the database.yml has # slave connections defined as: slave_database_name_test or slave_1_database_name_test, # where slave_1 is the secondary instance, 'database_name' is the actual # name of the database and '_test' is the Rails environment def build_secondary_connections(options={}) connection_classes = secondary_connection_classes(options) if connection_classes.blank? raise ArgumentError, " a secondary connection was not found. Check your database.yml." else connection_methods = [] connection_classes.each do |c| under_scored = c.underscore method_name = under_scored.split("_")[0] method_name = method_name.insert(method_name.index(/\d/),"_") class_name = method_name.classify connection_methods << method_name.to_sym build_secondary_class(class_name,c,options) build_single_secondary_method(method_name,class_name) set_table_name_for_joins end end build_slaves_method(connection_methods) if options[:replication] build_shards_method(connection_methods) if options[:shards] end # Creats a subclass that inherets from the model. The default model_name # class method is overriden to return the super's name, which ensures rails # helpers like link_to called on a secondary stance generate a url for the # master database. If options include readonly, build_secondary_class also # overrides the rails "readonly?" method to ensure saves are prevented. # secondary class can be called directly for operaitons. # Usage: # User::Slave1.where(:id => 1).first => returns results from slave_1 database # User::Slave2.where(:id => 2).first => returns results from slave_2 database # User::Shard1.where(:id => 2).first => returns results from slave_1 database def build_secondary_class(class_name,connection_name,options) class_eval <<-STR, __FILE__, __LINE__ class #{class_name} < self #{build_secondary_associations(class_name)} class << self def connection Connections::#{connection_name}.connection end def model_name ActiveModel::Name.new(#{name}) end end #{'def readonly?; true; end;' if (options[:name] == "readonly" || options[:readonly])} end STR end def set_table_name_for_joins self.table_name_prefix = "#{database_name}." unless database_name.match(/\.sqlite3$/) end # Adds as class method to call a specific secondary conneciton. # Usage: # User.slave_1.where(:id => 2).first => returns results from slave_1 database # User.slave_2.where(:id => 2).first => returns results from slave_2 database def build_single_secondary_method(method_name,class_name) class_eval <<-STR, __FILE__, __LINE__ class << self def #{method_name} self::#{class_name} end end STR end # add a class method that shifts through available connections methods # on each call. # Usage: # User.slave.where(:id => 2).first => can return results from slave_1 or slave_2 def build_slaves_method(connection_methods) class_eval <<-STR, __FILE__, __LINE__ @connection_methods = #{connection_methods} class << self def slaves current = @connection_methods.shift @connection_methods << current send(current) end alias :slave :slaves def connection_methods @connection_methods end end STR end # add a class method that shifts through available connections methods # on each call. # Usage: # User.shards.where(:id => 2).first => can return results from slave_1 or slave_2 def build_shards_method(connection_methods) class_eval <<-STR, __FILE__, __LINE__ @shard_connection_methods = #{connection_methods} class << self def shards ConnectionManager::MethodRecorder.new(@shard_connection_methods) end end STR end end end