require File.join(File.dirname(__FILE__), '/db_block_group_map_base') # This is a more sophisticated sharding method based on a two layer database-backed # blocks map that holds block-shard associations. Record blocks are mapped to groups # (= database schemas) and groups are mapped to shards (= databases). # # It automatically creates new blocks for new keys and assigns them to existing groups. # Warning: make sure to create at least one shard and one group before inserting any records. # module DbCharmer module Sharding module Method class DbBlockSchemaMap include DbBlockGroupMapBase # Shard connection info model class Shard < ::ActiveRecord::Base validates_presence_of :db_host validates_presence_of :db_port validates_presence_of :db_user validates_presence_of :db_pass, :unless => Proc.new { |shard| shard.db_pass=='' } validates_presence_of :schema_name_prefix has_many :groups, :class_name => 'DbCharmer::Sharding::Method::DbBlockSchemaMap::Group' end # Table group info model class Group < ::ActiveRecord::Base validates_presence_of :shard_id belongs_to :shard, :class_name => 'DbCharmer::Sharding::Method::DbBlockSchemaMap::Shard' end def connection_name(shard_id) "db_charmer_db_block_schema_map_#{name}_s%d" % shard_id end def schema_name(schema_name_prefix, group_id) "%s_%05d" % [ schema_name_prefix, group_id ] end #--------------------------------------------------------------------------------------------------------------- # Create configuration (use mapping connection as a template) def shard_connection_config(shard, group_id) # Format connection name connection_name = connection_name(shard.id) schema_name = schema_name(shard.schema_name_prefix, group_id) # Here we get the mapping connection's configuration # They do not expose configs so we hack in and get the instance var # FIXME: Find a better way, maybe move config method to our ar extenstions connection.config.clone.merge( # Name for the connection factory :connection_name => connection_name, # Connection params :host => shard.db_host, :slave_host => shard.db_slave_host, :port => shard.db_port, :username => shard.db_user, :password => shard.db_pass, :database => shard.db_name, :schema_name => schema_name, :shard_id => shard.id, :sharder_name => self.name ) end #--------------------------------------------------------------------------------------------------------------- def create_shard(params) params = params.symbolize_keys [ :db_host, :db_port, :db_user, :db_name, :schema_name_prefix ].each do |arg| raise ArgumentError, "Missing required parameter: #{arg}" unless params[arg] end # Prepare model prepare_shard_models # Create the record Shard.create! do |shard| shard.db_host = params[:db_host] shard.db_slave_host = (params[:db_slave_host] || '') if shard.respond_to? :db_slave_host shard.db_port = params[:db_port] shard.db_user = params[:db_user] shard.db_pass = params[:db_pass] || '' shard.db_name = params[:db_name] shard.schema_name_prefix = params[:schema_name_prefix] end end def create_group(shard, open, enabled) # Prepare model prepare_shard_models # Create the record group = Group.create! do |group| group.shard_id = shard.id group.open = open group.enabled = enabled end old_connection = Group.connection conn_config = shard_connection_config(shard, group.id) Group.switch_connection_to(conn_config) schema_name = schema_name(shard.schema_name_prefix, group.id) # create schema only if it doesn't already exist sql = "SELECT schema_name FROM information_schema.schemata WHERE schema_name='#{schema_name}'" existing_schema = Group.connection.execute(sql) unless existing_schema.first sql = "CREATE SCHEMA #{schema_name}" Group.connection.execute(sql) end Group.switch_connection_to(old_connection) end def shard_connections # Find all groups prepare_shard_models groups = Group.all(:conditions => { :enabled => true }, :include => :shard) # Map them to shards groups.map { |group| shard_connection_config(group.shard, group.id) } end # Prepare model for working with our shards table def prepare_shard_models Shard.switch_connection_to(connection) Shard.table_name = shards_table Group.switch_connection_to(connection) Group.table_name = groups_table end end # To be mixed in AR#base. # Allows a schema name to be set at the same time a db connection is selected. module SchemaTableNamePrefix def set_schema_table_name_prefix(con) #Rails.logger.debug "set_schema_table_name_prefix: self=#{self} @dbcharmer_table_name_prefix=#{@dbcharmer_table_name_prefix}" if con.is_a?(Hash) && con[:schema_name] new_prefix = con[:schema_name] + '.' else new_prefix = '' if self.to_s=='ActiveRecord::Base' # this is for migrations end # remove the old dbcharmer prefix first if !@dbcharmer_table_name_prefix.blank? && @dbcharmer_table_name_prefix != new_prefix self.table_name_prefix = self.table_name_prefix.gsub(/#{Regexp.escape(@dbcharmer_table_name_prefix)}/, '') end # Set the new table_name_prefix if new_prefix && @dbcharmer_table_name_prefix != new_prefix @dbcharmer_table_name_prefix = new_prefix self.table_name_prefix = "#{new_prefix}#{self.table_name_prefix}" # Reset all forms of table_name that were memoized in Rails. # Don't do it in the context of migrations where the current class # is AR::Base and there is no actual table name. unless self.to_s=='ActiveRecord::Base' #Rails.logger.debug "set_schema_table_name_prefix: resetting" reset_cached_table_name end end end # Rails memoizes table_name and table_name_prefix at the time the models are loaded. # This method forces refreshing those names def reset_cached_table_name self.reset_table_name @arel_table = nil @relation = nil end end end end end