Class: ActiveRecord::Base

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

Overview

These extensions in ActiveRecord::Base are for iSeries support

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.



169
170
171
172
173
# File 'lib/base.rb', line 169

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

Class Method Details

.associationsObject



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

def associations
  @associations ? @associations : {}
end

.base_class_nameObject



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

alias :base_class_name :class_name

.base_instantiateObject



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

alias :base_instantiate :instantiate

.base_set_table_nameObject

Save table name keys as all UPPERCASE to simplify lookup



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

alias :base_set_table_name :set_table_name

.base_table_exists?Object



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

alias :base_table_exists? :table_exists?

.class_name(table_name = table_name) ⇒ Object

:nodoc:



63
64
65
66
67
68
69
70
# File 'lib/base.rb', line 63

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



102
103
104
105
# File 'lib/validations.rb', line 102

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

.find_one(id, options) ⇒ Object

Finds a record based on the id value. The column associated with the primary key can be of a numeric type however DB2 doesn’t allow quoted numeric values (e.g WHERE id = ‘10’). Therefore this overrides the default method to check for the column type, and leaves the value in the query unquoted in cases of numeric columns



19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
# File 'lib/connection_adapters/ibm_db2_adapter.rb', line 19

def find_one(id, options)
  # If the adapter in use is the IBM DB2 Adapter
  if connection.kind_of?(ConnectionAdapters::IBM_DB2Adapter)
    if !connection.iseries then return ibm_find_one(id, options) end
    
    conditions = " AND (#{sanitize_sql(options[:conditions])})" if options[:conditions]
    # Retrieves the sql type of the column associated to the primary key
    cstmt = connection.execute("SELECT coltype FROM SYSCOLUMNS WHERE \
    table_name ='#{table_name.upcase}' AND column_name ='#{primary_key.upcase}'")
    DB2::fetch_row(cstmt)
    primary_key_type = DB2::result(cstmt, 0)
      
    # Frees the results set associated with the statement
    DB2::free_result(cstmt)
      
    # If the column is a numeric type
    if primary_key_type =~ /int|double|real|decimal|numeric/i
      # Assign the unquoted id value to san_id
      san_id = sanitize_numeric(id)
    else
      # The column is not numeric, so +sanitize+ it
      san_id = sanitize(id)
    end 
      
    # Adds the options based on the san_id value
    options.update :conditions => "#{table_name}.#{primary_key} = #{san_id}#{conditions}"
      
    if result = find_initial(options)
        result
    else
        raise RecordNotFound, "Couldn't find #{name} with ID=#{id}#{conditions}"
      end
  else
    # You're not using the IBM DB2 Adapter therefore the default method is recalled
    default_find_one(id, options)      
  end
end

.find_some(ids, options) ⇒ Object

Finds a record based on a list of ids. The primary key column type could be numeric, and DB2 doesn’t allow quoted numeric values. Therefore this version of the method checks for the data type and if it’s a numeric type the value in the query is unquoted. If the column datatype is not numeric, the value is properly quoted/sanitized



63
64
65
66
67
68
69
70
71
72
73
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
# File 'lib/connection_adapters/ibm_db2_adapter.rb', line 63

def find_some(ids, options)
  # If the adapter in use is the IBM DB2 Adapter
  if connection.kind_of?(ConnectionAdapters::IBM_DB2Adapter)
    if !connection.iseries then return ibm_find_some(ids, options) end
    
    conditions = " AND (#{sanitize_sql(options[:conditions])})" if options[:conditions]
    # Retrieves the sql type of the column associated to the primary key
    cstmt = connection.execute("SELECT typename FROM SYSCOLUMNS WHERE \
    table_name ='#{table_name.upcase}' AND column_name ='#{primary_key.upcase}'")
    DB2::fetch_row(cstmt)
    primary_key_type = DB2::result(cstmt, 0)
    
    # Frees the results set associated with the statement
    DB2::free_result(cstmt)
    
    # If the column is a numeric type
    if primary_key_type =~ /int|double|real|decimal|numeric/i
      # Generates a comma separated list of ids
      ids_list   = ids.map {|id| sanitize_numeric(id) }.join(',')
    else
      # Generates a comma separated list of quoted/sanitized ids
      ids_list   = ids.map { |id| sanitize(id) }.join(',')
    end 
    
    # Adds the options to the query, based on the generated +ids_list+
    options.update :conditions => "#{table_name}.#{primary_key} IN (#{ids_list})#{conditions}" 
    
    result = find_every(options)
  
    if result.size == ids.size
      result
    else
      raise RecordNotFound, "Couldn't find all #{name.pluralize} with IDs (#{ids_list})#{conditions}"
    end
  else
    # You're not using the IBM DB2 Adapter therefore the default method is recalled
    default_find_some(ids, options)
  end
end

.foreign_constraintsObject

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



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

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.



128
129
130
131
132
133
134
135
136
137
138
139
140
141
# File 'lib/base.rb', line 128

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
# File 'lib/validations.rb', line 74

def generate_validations
  unless @validations 
      columns.each do |column|
      
      if !(column.null || column.default_specified? || column.generated?)
        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

.ibm_find_oneObject



14
# File 'lib/connection_adapters/ibm_db2_adapter.rb', line 14

alias_method :ibm_find_one, :find_one

.ibm_find_someObject



58
# File 'lib/connection_adapters/ibm_db2_adapter.rb', line 58

alias_method :ibm_find_some, :find_some

.instantiate(record) ⇒ Object



74
75
76
77
78
# File 'lib/base.rb', line 74

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




49
50
51
52
53
54
55
56
57
58
59
# File 'lib/base.rb', line 49

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_name.to_a[0]
    set_primary_key(primary_name)
    logger.info("DRYSQL >> Identified PRIMARY KEY for #{self}: #{primary_name}")
  end
  primary_name
end

.regenerate_cached_schemaObject



107
108
109
110
111
112
113
114
115
116
# File 'lib/base.rb', line 107

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_name
  set_primary_key(primary_name)
  logger.info("DRYSQL >> Reset PRIMARY KEY for #{self}: #{primary_name}")
end

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



36
37
38
39
# File 'lib/base.rb', line 36

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



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

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

.table_exists?(class_name = nil) ⇒ Boolean

Returns:

  • (Boolean)


96
97
98
99
100
101
102
103
104
105
# File 'lib/base.rb', line 96

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



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

alias :base_initialize :initialize

#constraintsObject



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

def constraints
  self.class.table_constraints
end