Module: Sequel::Plugins::IdentityMap::ClassMethods

Defined in:
lib/sequel/plugins/identity_map.rb

Instance Method Summary collapse

Instance Method Details

#associate(type, name, opts = {}, &block) ⇒ Object

Override the default :eager_loader option for many_*_many associations to work with an identity_map. If the :eager_graph association option is used, you’ll probably have to use :uniq=>true on the current association amd :cartesian_product_number=>2 on the association mentioned by :eager_graph, otherwise you’ll end up with duplicates because the row proc will be getting called multiple times for the same object. If you do have duplicates and you use :eager_graph, they’ll probably be lost. Making that work correctly would require changing a lot of the core architecture, such as how graphing and eager graphing work.



44
45
46
47
48
49
50
51
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
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
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
# File 'lib/sequel/plugins/identity_map.rb', line 44

def associate(type, name, opts = {}, &block)
  if opts[:eager_loader]
    super
  elsif type == :many_to_many
    opts = super
    el = opts[:eager_loader] 
    model = self
    left_pk = opts[:left_primary_key]
    uses_lcks = opts[:uses_left_composite_keys]
    uses_rcks = opts[:uses_right_composite_keys]
    right = opts[:right_key]
    join_table = opts[:join_table]
    left = opts[:left_key]
    lcks = opts[:left_keys]
    left_key_alias = opts[:left_key_alias] ||= opts.default_associated_key_alias
    opts[:eager_loader] = lambda do |eo|
      return el.call(eo) unless model.identity_map
      h = eo[:key_hash][left_pk]
      eo[:rows].each{|object| object.associations[name] = []}
      r = uses_rcks ? rcks.zip(opts.right_primary_keys) : [[right, opts.right_primary_key]]
      l = uses_lcks ? [[lcks.map{|k| SQL::QualifiedIdentifier.new(join_table, k)}, h.keys]] : [[left, h.keys]]

      # Replace the row proc to remove the left key alias before calling the previous row proc.
      # Associate the value of the left key alias with the associated object (through its object_id).
      # When loading the associated objects, lookup the left key alias value and associate the
      # associated objects to the main objects if the left key alias value matches the left primary key
      # value of the main object.
      # 
      # The deleting of the left key alias from the hash before calling the previous row proc
      # is necessary when an identity map is used, otherwise if the same associated object is returned more than
      # once for the association, it won't know which of current objects to associate it to.
      ds = opts.associated_class.inner_join(join_table, r + l)
      pr = ds.row_proc
      h2 = {}
      ds.row_proc = proc do |hash|
        hash_key = if uses_lcks
          left_key_alias.map{|k| hash.delete(k)}
        else
          hash.delete(left_key_alias)
        end
        obj = pr.call(hash)
        (h2[obj.object_id] ||= []) << hash_key
        obj
      end
      model.eager_loading_dataset(opts, ds, Array(opts.select), eo[:associations], eo) .all do |assoc_record|
        if hash_keys = h2.delete(assoc_record.object_id)
          hash_keys.each do |hash_key|
            if objects = h[hash_key]
              objects.each{|object| object.associations[name].push(assoc_record)}
            end
          end
        end
      end
    end
    opts
  elsif type == :many_through_many
    opts = super
    el = opts[:eager_loader] 
    model = self
    left_pk = opts[:left_primary_key]
    left_key = opts[:left_key]
    uses_lcks = opts[:uses_left_composite_keys]
    left_keys = Array(left_key)
    left_key_alias = opts[:left_key_alias]
    opts[:eager_loader] = lambda do |eo|
      return el.call(eo) unless model.identity_map
      h = eo[:key_hash][left_pk]
      eo[:rows].each{|object| object.associations[name] = []}
      ds = opts.associated_class 
      opts.reverse_edges.each{|t| ds = ds.join(t[:table], Array(t[:left]).zip(Array(t[:right])), :table_alias=>t[:alias])}
      ft = opts[:final_reverse_edge]
      conds = uses_lcks ? [[left_keys.map{|k| SQL::QualifiedIdentifier.new(ft[:table], k)}, h.keys]] : [[left_key, h.keys]]

      # See above comment in many_to_many eager_loader
      ds = ds.join(ft[:table], Array(ft[:left]).zip(Array(ft[:right])) + conds, :table_alias=>ft[:alias])
      pr = ds.row_proc
      h2 = {}
      ds.row_proc = proc do |hash|
        hash_key = if uses_lcks
          left_key_alias.map{|k| hash.delete(k)}
        else
          hash.delete(left_key_alias)
        end
        obj = pr.call(hash)
        (h2[obj.object_id] ||= []) << hash_key
        obj
      end
      model.eager_loading_dataset(opts, ds, Array(opts.select), eo[:associations], eo).all do |assoc_record|
        if hash_keys = h2.delete(assoc_record.object_id)
          hash_keys.each do |hash_key|
            if objects = h[hash_key]
              objects.each{|object| object.associations[name].push(assoc_record)}
            end
          end
        end
      end
    end
    opts
  else
    super
  end
end

#call(row) ⇒ Object

If the identity map is in use, check it for a current copy of the object. If a copy does not exist, create a new object and add it to the identity map. If a copy exists, add any values in the given row that aren’t currently in the object to the object’s values. This allows you to only request certain fields in an initial query, make modifications to some of those fields and request other, potentially overlapping fields in a new query, and not have the second query override fields you modified.



166
167
168
169
170
171
172
173
174
175
# File 'lib/sequel/plugins/identity_map.rb', line 166

def call(row)
  return super unless idm = identity_map
  if o = idm[identity_map_key(Array(primary_key).map{|x| row[x]})]
    o.merge_db_update(row)
  else
    o = super
    idm[identity_map_key(o.pk)] = o
  end
  o
end

#identity_mapObject

Returns the current thread-local identity map. Should be a hash if there is an active identity map, and nil otherwise.



149
150
151
# File 'lib/sequel/plugins/identity_map.rb', line 149

def identity_map
  Thread.current[:sequel_identity_map]
end

#identity_map_key(pk) ⇒ Object

The identity map key for an object of the current class with the given pk. May not always be correct for a class which uses STI.



155
156
157
# File 'lib/sequel/plugins/identity_map.rb', line 155

def identity_map_key(pk)
  "#{self}:#{pk ? Array(pk).join(',') : "nil:#{rand}"}"
end

#with_identity_mapObject

Take a block and inside that block use an identity map to ensure a 1-1 correspondence of objects to the database row they represent.



179
180
181
182
183
184
185
186
187
# File 'lib/sequel/plugins/identity_map.rb', line 179

def with_identity_map
  return yield if identity_map
  begin
    self.identity_map = {}
    yield
  ensure
    self.identity_map = nil
  end
end