module DrNicMagicModels # ONE Schema per namespace module # Person, Company, etc share the Object namespace module, ie. ::Person, ::Company # Blog::Post, Blog::Comment, share the Blog namespace module class Schema attr_reader :modul def initialize(modul) @modul = modul @table_name_prefix = modul.instance_variable_get("@table_name_prefix") rescue '' logger.info "Create Schema for #{@modul}, table_name_prefix '#{@table_name_prefix}'" end cattr_accessor :inflector cattr_accessor :superklass # Need to store models etc per-module, not in @ @models def inflector @inflector ||= Inflector.new end # all in lower case please ReservedTables = [:schema_info, :sessions] @models = nil def logger @logger ||= DrNicMagicModels::Logger end def models load_schema if @models.nil? @models end def tables load_schema if @tables.nil? @tables end def table_names load_schema if @table_names.nil? @table_names end def fks_on_table(table_name) load_schema if @models.nil? @fks_by_table[table_name.to_s] || [] end # active record only support 2 column link tables, otherwise use a model table, has_many and through def is_link_table?(table_name) load_schema if @models.nil? return @link_tables[table_name] if ! @link_tables[table_name].nil? column_names = @conn.columns(table_name).map{|x| x.name } @link_tables[table_name] = ! column_names.include?("id") && column_names.length == 2 && column_names.select { |x| x =~ /_id$/ } == column_names return @link_tables[table_name] end def link_tables_for_class(klass) load_schema if @models.nil? end def load_schema(preload = false) return if !@models.nil? @superklass ||= ActiveRecord::Base raise "No database connection" if !(@conn = @superklass.connection) @models = ModelHash.new @tables = Hash.new @fks_by_table = Hash.new @link_tables = Hash.new @table_names = @conn.tables @table_names = @table_names.grep(/^#{@table_name_prefix}/) if @table_name_prefix @table_names = @table_names.sort logger.info "For #{modul} tables are #{@table_names.inspect}" # Work out which tables are in the model and which aren't @table_names.each do |table_name| # deal with reserved tables & link_tables && other stray id-less tables #key = 'id' #case ActiveRecord::Base.primary_key_prefix_type # when :table_name # key = Inflector.foreign_key(table_name, false) # when :table_name_with_underscore # key = Inflector.foreign_key(table_name) #end #next if ReservedTables.include?(table_name.downcase.to_sym) || # is_link_table?(table_name) || # ! @conn.columns(table_name).map{ |x| x.name}.include?(key) table_name_clean = table_name.gsub(/^#{@table_name_prefix}/,'') # a model table then... model_class_name = inflector.class_name(table_name_clean) logger.debug "Got a model table: #{table_name} => class #{model_class_name}" @models[model_class_name] = table_name @tables[table_name] = model_class_name if preload # create by MAGIC! klass = model_class_name.constantize # Process FKs? if @conn.supports_fetch_foreign_keys? tables.each do |table_name| logger.debug "Getting FKs for #{table_name}" @fks_by_table[table_name] = Array.new @conn.foreign_key_constraints(table_name).each do |fk| logger.debug "Got one: #{fk}" @fks_by_table[table_name].push(fk) end # do each fk end # each table end # Try to work out our link tables now... @models.keys.sort.each{|klass| process_table(@models[klass.to_s])} @link_tables.keys.sort.each{|table_name| process_link_table(table_name) if @link_tables[table_name]} end end end def process_table(table_name) logger.debug "Processing model table #{table_name}" # ok, so let's look at the foreign keys on the table... belongs_to_klass = @tables[table_name].constantize rescue return processed_columns = Hash.new fks_on_table(table_name).each do |fk| logger.debug "Found FK column by suffix _id [#{fk.foreign_key}]" has_some_klass = Inflector.classify(fk.reference_table).constantize rescue next processed_columns[fk.foreign_key] = { :has_some_klass => has_some_klass } processed_columns[fk.foreign_key].merge! add_has_some_belongs_to(belongs_to_klass, fk.foreign_key, has_some_klass) rescue next end column_names = @conn.columns(table_name).map{ |x| x.name} column_names.each do |column_name| next if not column_name =~ /_id$/ logger.debug "Found FK column by suffix _id [#{column_name}]" if processed_columns.key?(column_name) logger.debug "Skipping, already processed" next end has_some_klass = Inflector.classify(column_name.sub(/_id$/,"")).constantize rescue next processed_columns[column_name] = { :has_some_klass => has_some_klass } processed_columns[column_name].merge! add_has_some_belongs_to(belongs_to_klass, column_name, has_some_klass) rescue next end #TODO: what if same classes in table? # is this a link table with attributes? (has_many through?) return if processed_columns.keys.length < 2 processed_columns.keys.each do |key1| processed_columns.keys.each do |key2| next if key1 == key2 logger.debug "\n*** #{processed_columns[key1][:has_some_class]}.send 'has_many', #{processed_columns[key2][:belongs_to_name].to_s.pluralize.to_sym}, :through => #{processed_columns[key2][:has_some_name]}\n\n" processed_columns[key1][:has_some_class].send 'has_many', processed_columns[key2][:belongs_to_name].to_s.pluralize.to_sym, :through => processed_columns[key2][:has_some_name].to_sym end end end def add_has_some_belongs_to(belongs_to_klass, belongs_to_fk, has_some_klass) logger.debug "Trying to add a #{belongs_to_klass} belongs_to #{has_some_klass}..." # so this is a belongs_to & has_some style relationship... # is it a has_many, or a has_one? Well, let's assume a has_one has a unique index on the column please... good db design, haha! unique = belongs_to_klass.get_unique_index_columns.include?(belongs_to_fk) belongs_to_name = belongs_to_fk.sub(/_id$/, '').to_sym logger.debug "\n*** #{belongs_to_klass}.send 'belongs_to', #{belongs_to_name}, :class_name => #{has_some_klass}, :foreign_key => #{belongs_to_fk}\n" belongs_to_klass.send(:belongs_to, belongs_to_name, :class_name => has_some_klass.to_s, :foreign_key => belongs_to_fk.to_sym) # work out if we need a prefix has_some_name = ( (unique ? belongs_to_klass.table_name.singularize : belongs_to_klass.table_name) + (belongs_to_name.to_s == has_some_klass.table_name.singularize ? "" : "_as_"+belongs_to_name.to_s) ).downcase.to_sym method = unique ? :has_one : :has_many logger.debug "\n*** #{has_some_klass}.send(#{method}, #{has_some_name}, :class_name => #{belongs_to_klass.to_s}, :foreign_key => #{belongs_to_fk.to_sym})\n\n" has_some_klass.send(method, has_some_name, :class_name => belongs_to_klass.to_s, :foreign_key => belongs_to_fk.to_sym) return { :method => method, :belongs_to_name => belongs_to_name, :has_some_name => has_some_name, :has_some_class => has_some_klass } end def process_link_table(table_name) logger.debug "Processing link table #{table_name}" classes_map = Hash.new column_names = @conn.columns(table_name).map{ |x| x.name} # use foreign keys first fks_on_table(table_name).each do |fk| logger.debug "Processing fk: #{fk}" klass = Inflector.classify(fk.reference_table).constantize rescue logger.debug("Cannot find model #{class_name} for table #{fk.reference_table}") && return classes_map[fk.foreign_key] = klass end logger.debug "Got #{classes_map.keys.length} references from FKs" if classes_map.keys.length < 2 #Fall back on good ol _id recognition column_names.each do |column_name| # check we haven't processed by fks already next if ! classes_map[column_name].nil? referenced_table = column_name.sub(/_id$/, '') begin klass = Inflector.classify(referenced_table).constantize # fall back on FKs here if ! klass.nil? classes_map[column_name] = klass end rescue end end end # not detected the link table? logger.debug "Got #{classes_map.keys.length} references" logger.debug "Cannot detect both tables referenced in link table" && return if classes_map.keys.length != 2 logger.debug "Adding habtm relationship" logger.debug "\n*** #{classes_map[column_names[0]]}.send 'has_and_belongs_to_many', #{column_names[1].sub(/_id$/,'').pluralize.to_sym}, :class_name => #{classes_map[column_names[1]].to_s}, :join_table => #{table_name.to_sym}\n" logger.debug "\n*** #{classes_map[column_names[1]]}.send 'has_and_belongs_to_many', #{column_names[0].sub(/_id$/,'').pluralize.to_sym}, :class_name => #{classes_map[column_names[0]].to_s}, :join_table => #{table_name.to_sym}\n\n" classes_map[column_names[0]].send 'has_and_belongs_to_many', column_names[1].sub(/_id$/,'').pluralize.to_sym, :class_name => classes_map[column_names[1]].to_s, :join_table => table_name.to_sym classes_map[column_names[1]].send 'has_and_belongs_to_many', column_names[0].sub(/_id$/,'').pluralize.to_sym, :class_name => classes_map[column_names[0]].to_s, :join_table => table_name.to_sym end end class ModelHash < Hash def unenquire(class_id) @enquired ||= {} @enquired[class_id = class_id.to_s] = false end def enquired?(class_id) @enquired ||= {} @enquired[class_id.to_s] end def [](class_id) enquired?(class_id = class_id.to_s) @enquired[class_id] = true super(class_id) end end end