Class: ActiveRecord::Base

Inherits:
Object
  • Object
show all
Defined in:
lib/base.rb,
lib/validations.rb,
lib/associations.rb

Constant Summary collapse

@@class_name_hash =

Stores a dictionary of table_name => class_name mappings to facilitate reverse lookup with acceptable performance

{}
@@logger =
Logger.new(STDERR)

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(attributes = nil) ⇒ Base

Returns a new instance of Base.



173
174
175
176
177
# File 'lib/base.rb', line 173

def initialize(attributes = nil)
  base_initialize(attributes)
  self.class.generate_associations
  self.class.generate_validations
end

Class Method Details

.associationsObject



96
97
98
# File 'lib/base.rb', line 96

def associations
  @associations ? @associations : {}
end

.base_class_nameObject



67
# File 'lib/base.rb', line 67

alias :base_class_name :class_name

.base_instantiateObject



78
# File 'lib/base.rb', line 78

alias :base_instantiate :instantiate

.base_set_table_nameObject

Save table name keys as all UPPERCASE to simplify lookup



40
# File 'lib/base.rb', line 40

alias :base_set_table_name :set_table_name

.base_table_exists?Object



100
# File 'lib/base.rb', line 100

alias :base_table_exists? :table_exists?

.class_name(table_name = table_name) ⇒ Object

:nodoc:



68
69
70
71
72
73
74
75
# File 'lib/base.rb', line 68

def class_name(table_name = table_name) # :nodoc:
  #Attempt to retrieve class name from cache before attempting to generate it       
  name = @@class_name_hash[table_name.upcase]
  if !name
    return base_class_name(table_name)
  end
  name
end

.clear_cached_associationsObject



19
20
21
22
# File 'lib/associations.rb', line 19

def clear_cached_associations
  @associations = nil
  logger.info("DRYSQL >> Cleared cached schema for: #{self}")
end

.clear_cached_validationsObject



107
108
109
110
# File 'lib/validations.rb', line 107

def clear_cached_validations
  @validations = nil
  logger.info("DRYSQL >> Cleared cached validations for: #{self}")
end

.constraintsObject



149
150
151
152
153
154
# File 'lib/base.rb', line 149

def constraints
  unless @constraints
    @constraints = connection.constraints(table_name, "#{name} Constraints")
  end
  @constraints 
end

.foreign_constraintsObject

foreign_constraints are those constraints that are defined on other tables, but reference this table (i.e. FKs into this table)



92
93
94
# File 'lib/base.rb', line 92

def foreign_constraints     
  constraints.select {|constraint| constraint.is_foreign_constraint?(table_name)}
end

.generate_associationsObject

The @associations instvar is used as an indicator as to whether the associations have already been generated or not. The first time an instance of a DrySQL-enabled subclass of ActiveRecord::Base is created, the associations are generated for the class.



10
11
12
13
14
15
16
17
# File 'lib/associations.rb', line 10

def generate_associations
  unless @associations 
    generate_belongs_to_associations
    generate_has_many_associations
    generate_through_associations
    @associations = reflections
  end
end

.generate_ormObject

This method will generate associations and validations for all defined subclasses of ActiveRecord::Base.

If any of the constraints processed refer to undefined model classes, these classes will be defined as long as they conform to the ActiveRecord naming conventions.

This method swallows exceptions that occur during the generation of associations and validations, but logs these exceptions in detail to STDERR.



133
134
135
136
137
138
139
140
141
142
143
144
145
146
# File 'lib/base.rb', line 133

def generate_orm
  models = @@subclasses.to_a.flatten.uniq
  models.each do |model|
    begin
      if model != Base
        model.generate_associations
        model.generate_validations
      end
    rescue Exception
      logger.error("DRYSQL ERROR >> Could not generate ORM for #{model}")
      logger.error($!.backtrace.join("\n"))
    end
  end
end

.generate_validationsObject

The @validations instvar is used as an indicator as to whether the validations have already been generated or not. The first time an instance of a DrySQL-enabled subclass of ActiveRecord::Base is created, the validations are generated for the class.

**NOTE: Some of this code and/or the ideas behind some of this code was borrowed from Simon Harris

and Red Hill Consulting's Schema Validations plug-in. Thanks Simon!
See     http://www.redhillconsulting.com.au/rails_plugins.html#schema_validations 
for info about this plug-in

Additional features added to Schema_Validations: 1) proper error message generated for boolean inclusion failure (:inclusion)

2) all validations are generated with allow_nil=>true because…

3) all validations are superceded with the new validates_nullability_of,

which generates an error for any field whose associated column has a NOT NULL constraint upon it,
for whom no default value is specified, and for whom the value is not generated by the DB

4) do not generate validates_uniqueness_of by default for unique fields. This is a performance hit, and

in my opinion completely defeats the purpose of an application-side data validation because it
requires a query of the DB. Why not just let the DB do the work and handle the duplicate key exception?

5) Do not generate validates_presence_of for non-nullable fields. This will throw an exception for fields that

contain an empty string, even though that may in fact be a valid value according to the table constraints.
This approach also fails to consider that a default value might be specified for a non-nullable field in which case
we do not need to block the null field from being saved to the DB

6) Perform validation auto-generation on all columns rather than just those returned by Base.content_columns

I believe there is value in validating our PK, FK, and counter columns


74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
# File 'lib/validations.rb', line 74

def generate_validations
  unless @validations 
    columns.each do |column|
			# Columns are considered nullable if any of the following are true:
			#
			# 1) Column does not have a NOT NULL constraint
			# 2) Column has a default value specified
			# 3) Column is generated 
			#    (DrySQL makes the assumption that if a column is a PK and of type integer, then it is generated)
			if !(column.is_nullable? || (column.name.upcase == self.primary_key.upcase && column.type == :integer))
      	self.validates_nullability_of column.name
       	logger.info("DRYSQL >> GENERATED VALIDATION: #{self.name} validates_nullability_of :#{column.name}")
      end
      
   if column.type == :integer   
     self.validates_numericality_of column.name, :allow_nil => true, :only_integer => true
     logger.info("DRYSQL >> GENERATED VALIDATION: #{self.name} validates_numericality_of :#{column.name}, :allow_nil=>true, :only_integer=>true")
   elsif column.number?
     self.validates_numericality_of column.name, :allow_nil => true
     logger.info("DRYSQL >> GENERATED VALIDATION: #{self.name} validates_numericality_of :#{column.name}, :allow_nil=>true")
   elsif column.text? && column.limit
     self.validates_length_of column.name, :allow_nil => true, :maximum => column.limit
     logger.info("DRYSQL >> GENERATED VALIDATION: #{self.name} validates_length_of :#{column.name}, :allow_nil=>true, :maximum=>#{column.limit}")
   elsif column.type == :boolean
     self.validates_inclusion_of column.name, :in => [true, false], :allow_nil =>true, :message => ActiveRecord::Errors.default_error_messages[:inclusion]
     logger.info("DRYSQL >> GENERATED VALIDATION: #{self.name} validates_inclusion_of :#{column.name}, :in=>[true, false], \
       :allow_nil=>true, :message=>ActiveRecord::Errors.default_error_messages[:inclusion]")
   end
    end         
  end
  @validations=true
end

.instantiate(record) ⇒ Object



79
80
81
82
83
# File 'lib/base.rb', line 79

def instantiate(record)
  generate_associations
  generate_validations
  base_instantiate(record)
end

.loggerObject



30
31
32
# File 'lib/base.rb', line 30

def logger
  @@logger
end

.primary_keyObject

Overrides primary_key in ActiveRecord::Base

This implementation will need to be adjusted should composite primary keys be supported in the future

set_primary_key generates an accessor method on the caller that returns the string value –> this generated accessor will be invoked on future calls to primary_key




54
55
56
57
58
59
60
61
62
63
64
# File 'lib/base.rb', line 54

def primary_key
  primary = table_constraints.detect {|constraint| constraint.primary_key?}
  if primary.nil?
    logger.error("DRYSQL >> No primary key defined for table #{table_name}") 
  else 
    primary_name = primary.column_names[0]
    set_primary_key(primary_name)
    logger.info("DRYSQL >> Identified PRIMARY KEY for #{self}: #{primary_name}")
  end
  primary_name
end

.regenerate_cached_schemaObject



112
113
114
115
116
117
118
119
120
121
# File 'lib/base.rb', line 112

def regenerate_cached_schema
  clear_cached_associations
  clear_cached_validations
  @constraints = nil
  @columns = nil
  primary = table_constraints.detect {|constraint| constraint.primary_key?}
  primary_name = primary.column_names[0]
  set_primary_key(primary_name)
  logger.info("DRYSQL >> Reset PRIMARY KEY for #{self}: #{primary_name}")
end

.set_drysql_log_output(file) ⇒ Object



34
35
36
37
# File 'lib/base.rb', line 34

def set_drysql_log_output(file)
  @@logger = Logger.new(file)
		@@logger.level = Logger::INFO
end

.set_table_name(value = nil, &block) ⇒ Object



41
42
43
44
# File 'lib/base.rb', line 41

def set_table_name(value = nil, &block)
  base_set_table_name(value)
  @@class_name_hash["#{table_name.upcase}"] = self.name
end

.table_constraintsObject

table_constraints are those constraints defined on this table



86
87
88
# File 'lib/base.rb', line 86

def table_constraints
  constraints.reject {|constraint| constraint.is_foreign_constraint?(table_name)}
end

.table_exists?(class_name = nil) ⇒ Boolean

Returns:

  • (Boolean)


101
102
103
104
105
106
107
108
109
110
# File 'lib/base.rb', line 101

def table_exists?(class_name=nil)
  if class_name.nil? then base_table_exists?
  else
    if connection.respond_to?(:tables)
      connection.tables.include? construct_table_name_from_class_name(class_name)
    else
      false
    end
  end
end

Instance Method Details

#base_initializeObject



172
# File 'lib/base.rb', line 172

alias :base_initialize :initialize

#constraintsObject



179
180
181
# File 'lib/base.rb', line 179

def constraints
  self.class.constraints
end