/
monitored_hash.rb
105 lines (93 loc) · 3.24 KB
/
monitored_hash.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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
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
# Author: Stephen Sykes
module SlimScrooge
# A MonitoredHash allows us to return only some columns into the @attributes
# of an ActiveRecord model object, but to notice when an attribute that
# wasn't fetched is accessed.
#
# Also, when a result is first fetched for a particular callsite, we monitor
# all the columns so that we can immediately learn which columns are needed.
#
class MonitoredHash < Hash
attr_accessor :callsite, :result_set, :monitored_columns
# Create a monitored hash. The unmonitored_columns are accessed like a regular
# hash. The monitored columns kept separately, and new_column_access is called
# before they are returned.
#
def self.[](monitored_columns, unmonitored_columns, callsite)
hash = MonitoredHash.new {|hash, key| hash.new_column_access(key)}
hash.monitored_columns = monitored_columns
hash.merge!(unmonitored_columns)
hash.callsite = callsite
hash
end
# Called when an unknown column is requested, through the default proc.
# If the column requested is valid, and the result set is not completely
# loaded, then we reload. Otherwise just note the column with add_seen_column.
#
def new_column_access(name)
if @callsite.columns_hash.has_key?(name)
@result_set.reload! if @result_set && name != @callsite.primary_key
Callsites.add_seen_column(@callsite, name)
end
@monitored_columns[name]
end
# Reload if needed before allowing assignment
#
def []=(name, value)
if has_key?(name)
return super
elsif @result_set && @callsite.columns_hash.has_key?(name)
@result_set.reload!
Callsites.add_seen_column(@callsite, name)
end
@monitored_columns[name] = value
end
# Returns the column names
#
def keys
@result_set ? @callsite.columns_hash.keys : super | @monitored_columns.keys
end
# Check for a column name
#
def has_key?(name)
@result_set ? @callsite.columns_hash.has_key?(name) : super || @monitored_columns.has_key?(name)
end
alias_method :include?, :has_key?
# Called by Hash#update when reload is called on an ActiveRecord object
#
def to_hash
@result_set.reload! if @result_set
@monitored_columns.merge(self)
end
def freeze
@result_set.reload! if @result_set
@monitored_columns.freeze
super
end
# Marshal
# Dump a real hash - can't dump a monitored hash due to default proc
#
def _dump(depth)
Marshal.dump(to_hash)
end
def self._load(str)
Marshal.load(str)
end
end
end
# We need to change the update method of Hash so that it *always*
# calls to_hash on MonitoredHash instances. This is because it
# normally checks if other_hash is a kind of Hash, and doesn't bother
# calling to_hash if so. But we need it to call to_hash, because
# otherwise update will not get the complete columns from a
# MonitoredHash
#
# This is not harmful - to_hash in a regular Hash just returns self.
#
class Hash
alias_method :c_update, :update
def update(other_hash, &block)
c_update(other_hash.is_a?(::SlimScrooge::MonitoredHash) ? other_hash.to_hash : other_hash,
&block)
end
end