Class: LowCardTables::HasLowCardTable::LowCardAssociation

Inherits:
Object
  • Object
show all
Defined in:
lib/low_card_tables/has_low_card_table/low_card_association.rb

Overview

A LowCardAssociation represents a single association between a referring model class and a referred-to low-card model class. Note that this represents an association between classes, not between objects – that is, there is one instance of this class for a relationship from one referring class to one referred-to class, no matter how many model objects are instantiated.

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(model_class, association_name, options) ⇒ LowCardAssociation

Creates a new instance. model_class is the Class (which must inherit from ActiveRecord::Base) that is the referring model; association_name is the name of the association. options can contain any of the options accepted by LowCardTables::HasLowCardTables::Base#has_low_card_table.



14
15
16
17
18
19
20
21
22
23
24
25
# File 'lib/low_card_tables/has_low_card_table/low_card_association.rb', line 14

def initialize(model_class, association_name, options)
  @model_class = model_class
  @association_name = association_name.to_s
  @options = options.with_indifferent_access

  # We call this here so that if things are configured incorrectly, you'll get an exception at the moment you
  # try to associate the tables, rather than at runtime when you try to actually use them. Blowing up early is
  # good. :)
  foreign_key_column_name

  low_card_class.low_card_referred_to_by(model_class)
end

Instance Attribute Details

#association_nameObject (readonly)

Returns the name of the association – this will always have been the first arguent to has_low_card_table.



9
10
11
# File 'lib/low_card_tables/has_low_card_table/low_card_association.rb', line 9

def association_name
  @association_name
end

Instance Method Details

#class_method_name_to_low_card_method_name_mapObject

Returns a Hash that maps the names of methods that should be added to the referring class to the names of methods they should invoke on the low-card class. This takes into account both the :delegate option (via its internal call to #delegated_method_names) and the :prefix option.



30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
# File 'lib/low_card_tables/has_low_card_table/low_card_association.rb', line 30

def class_method_name_to_low_card_method_name_map
  return { } if options.has_key?(:delegate) && (! options[:delegate])

  out = { }

  delegated_method_names.each do |column_name|
    desired_method_name = case options[:prefix]
    when true then "#{association_name}_#{column_name}"
    when String, Symbol then "#{options[:prefix]}_#{column_name}"
    when nil then column_name
    else raise ArgumentError, "Invalid :prefix option: #{options[:prefix].inspect}"
    end

    out[desired_method_name] = column_name
    out[desired_method_name + "="] = column_name + "="
  end

  out
end

#create_low_card_object_for(model_instance) ⇒ Object

Given an instance of the referring class, returns an instance of the low-card class that is configured correctly for the current value of the referring column.



83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
# File 'lib/low_card_tables/has_low_card_table/low_card_association.rb', line 83

def create_low_card_object_for(model_instance)
  ensure_correct_class!(model_instance)

  id = get_id_from_model(model_instance)

  out = nil
  if id
    template = low_card_class.low_card_row_for_id(id)
    out = template.dup
    out.id = nil
    out
  else
    out = low_card_class.new
  end

  out
end

#delegated_method_namesObject

Returns an Array of names of methods on the low-card table that should be delegated to. This may be different than the names of methods on the referring class, because of the :prefix option.



52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
# File 'lib/low_card_tables/has_low_card_table/low_card_association.rb', line 52

def delegated_method_names
  value_column_names = low_card_class.low_card_value_column_names.map(&:to_s)

  if options.has_key?(:delegate) && (! options[:delegate])
    [ ]
  elsif options[:delegate].kind_of?(Array) || options[:delegate].kind_of?(String) || options[:delegate].kind_of?(Symbol)
    out = Array(options[:delegate]).map(&:to_s)
    extra = out - value_column_names

    if extra.length > 0
      raise ArgumentError, "You told us to delegate the following methods to low-card class #{low_card_class}, but that model doesn't have these columns: #{extra.join(", ")}; it has these columns: #{value_column_names.join(", ")}"
    end
    out
  elsif options[:delegate] && options[:delegate].kind_of?(Hash) && options[:delegate].keys.map(&:to_s) == %w{except}
    excluded = (options[:delegate][:except] || options[:delegate]['except']).map(&:to_s)
    extra = excluded - value_column_names

    if extra.length > 0
      raise ArgumentError, "You told us to delegate all but the following methods to low-card class #{low_card_class}, but that model doesn't have these columns: #{extra.join(", ")}; it has these columns: #{value_column_names.join(", ")}"
    end

    value_column_names - excluded
  elsif (! options.has_key?(:delegate)) || options[:delegate] == true
    value_column_names
  else
    raise ArgumentError, "Invalid value for :delegate: #{options[:delegate].inspect}"
  end
end

#foreign_key_column_nameObject

Computes the correct name of the foreign-key column based on the options passed in.



102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
# File 'lib/low_card_tables/has_low_card_table/low_card_association.rb', line 102

def foreign_key_column_name
  @foreign_key_column_name ||= begin
    out = options[:foreign_key]

    unless out
      out = "#{@model_class.name.underscore}_#{association_name}"
      out = $1 if out =~ %r{/[^/]+$}i
      out = out + "_id"
    end

    out = out.to_s if out.kind_of?(Symbol)

    column = model_class.columns.detect { |c| c.name.strip.downcase == out.strip.downcase }
    unless column
      raise ArgumentError, %{You said that #{model_class} has_low_card_table :#{association_name}, and we
have a foreign-key column name of #{out.inspect}, but #{model_class} doesn't seem
to have a column named that at all. Did you misspell it? Or perhaps something else is wrong?

The model class has these columns: #{model_class.columns.map(&:name).sort.join(", ")}}
    end

    out
  end
end

#low_card_classObject

Figures out what the low-card class this association should use is; this uses convention, with some overrides.

By default, for a class User that has_low_card_table :status, it looks for a class UserStatus. This is intentionally different from Rails’ normal conventions, where it would simply look for a class Status. This is because low-card tables are almost always unique to their owning table – i.e., the case where multiple tables say has_low_card_table to the same low-card table is very rare. (This is just because having multiple tables that have – and always will have – the same set of low-card attributes is also quite rare.) Hence, we use a little more default specificity in the naming.



174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
# File 'lib/low_card_tables/has_low_card_table/low_card_association.rb', line 174

def low_card_class
  @low_card_class ||= begin
    # e.g., class User has_low_card_table :status => UserStatus
    out = options[:class] || "#{model_class.name.underscore.singularize}_#{association_name}"

    out = out.to_s if out.kind_of?(Symbol)
    out = out.camelize if out.kind_of?(String)

    if out.kind_of?(String)
      begin
        out = out.constantize
      rescue NameError => ne
        raise ArgumentError, %{You said that #{model_class} has_low_card_table :#{association_name}, and we have a
:class of #{out.inspect}, but, when we tried to load that class (via #constantize),
we got a NameError. Perhaps you misspelled it, or something else is wrong?

NameError: (#{ne.class.name}): #{ne.message}}
      end
    end

    unless out.kind_of?(Class)
      raise ArgumentError, %{You said that #{model_class} has_low_card_table :#{association_name} with a
:class of #{out.inspect}, but that isn't a String or Symbol that represents a class,
or a valid Class object itself.}
    end

    unless out.respond_to?(:is_low_card_table?) && out.is_low_card_table?
      raise ArgumentError, %{You said that #{model_class} has_low_card_table :#{association_name},
and we have class #{out} for that low-card table (which is a Class), but it
either isn't an ActiveRecord model or, if so, it doesn't think it is a low-card
table itself (#is_low_card_table? returns false).

Perhaps you need to declare 'is_low_card_table' on that class?}
    end

    out
  end
end

#update_collapsed_rows(collapse_map, collapsing_update_scheme) ⇒ Object

When a low-card table has a column removed, it will typically have duplicate rows; these duplicate rows are then deleted. But then referring tables need to be updated. This method gets called at that point, with a map of <winner row> => <array of loser rows>, and the collapsing_update_scheme declared by this referring model class. It is responsible for handling whatever collapsing update scheme has been declared properly.



131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
# File 'lib/low_card_tables/has_low_card_table/low_card_association.rb', line 131

def update_collapsed_rows(collapse_map, collapsing_update_scheme)
  if collapsing_update_scheme.respond_to?(:call)
    collapsing_update_scheme.call(collapse_map)
  elsif collapsing_update_scheme == :none
    # nothing to do
  else
    row_chunk_size = collapsing_update_scheme
    current_id = @model_class.order("#{@model_class.primary_key} ASC").first.id

    while true
      current_id = update_collapsed_rows_batch(current_id, row_chunk_size, collapse_map)
      break if (! current_id)
    end
  end
end

#update_foreign_key!(model_instance) ⇒ Object

Updates the foreign key for this association on the given model instance. This is called by LowCardTables::HasLowCardTable::Base#low_card_update_foreign_keys!, which is primarily invoked by a :before_save filter and alternatively can be invoked manually.



150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
# File 'lib/low_card_tables/has_low_card_table/low_card_association.rb', line 150

def update_foreign_key!(model_instance)
  hash = { }

  low_card_object = model_instance._low_card_objects_manager.object_for(self)

  low_card_class.low_card_value_column_names.each do |value_column_name|
    hash[value_column_name] = low_card_object[value_column_name]
  end

  new_id = low_card_class.low_card_find_or_create_ids_for(hash)

  unless get_id_from_model(model_instance) == new_id
    set_id_on_model(model_instance, new_id)
  end
end