activerecord 3.0.0 → 4.0.0

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of activerecord might be problematic. Click here for more details.

Files changed (181) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +2102 -0
  3. data/MIT-LICENSE +20 -0
  4. data/README.rdoc +35 -44
  5. data/examples/performance.rb +110 -100
  6. data/lib/active_record/aggregations.rb +59 -75
  7. data/lib/active_record/associations/alias_tracker.rb +76 -0
  8. data/lib/active_record/associations/association.rb +248 -0
  9. data/lib/active_record/associations/association_scope.rb +135 -0
  10. data/lib/active_record/associations/belongs_to_association.rb +60 -59
  11. data/lib/active_record/associations/belongs_to_polymorphic_association.rb +16 -59
  12. data/lib/active_record/associations/builder/association.rb +108 -0
  13. data/lib/active_record/associations/builder/belongs_to.rb +98 -0
  14. data/lib/active_record/associations/builder/collection_association.rb +89 -0
  15. data/lib/active_record/associations/builder/has_and_belongs_to_many.rb +39 -0
  16. data/lib/active_record/associations/builder/has_many.rb +15 -0
  17. data/lib/active_record/associations/builder/has_one.rb +25 -0
  18. data/lib/active_record/associations/builder/singular_association.rb +32 -0
  19. data/lib/active_record/associations/collection_association.rb +608 -0
  20. data/lib/active_record/associations/collection_proxy.rb +986 -0
  21. data/lib/active_record/associations/has_and_belongs_to_many_association.rb +40 -112
  22. data/lib/active_record/associations/has_many_association.rb +83 -76
  23. data/lib/active_record/associations/has_many_through_association.rb +147 -66
  24. data/lib/active_record/associations/has_one_association.rb +67 -108
  25. data/lib/active_record/associations/has_one_through_association.rb +21 -25
  26. data/lib/active_record/associations/join_dependency/join_association.rb +174 -0
  27. data/lib/active_record/associations/join_dependency/join_base.rb +24 -0
  28. data/lib/active_record/associations/join_dependency/join_part.rb +78 -0
  29. data/lib/active_record/associations/join_dependency.rb +235 -0
  30. data/lib/active_record/associations/join_helper.rb +45 -0
  31. data/lib/active_record/associations/preloader/association.rb +121 -0
  32. data/lib/active_record/associations/preloader/belongs_to.rb +17 -0
  33. data/lib/active_record/associations/preloader/collection_association.rb +24 -0
  34. data/lib/active_record/associations/preloader/has_and_belongs_to_many.rb +60 -0
  35. data/lib/active_record/associations/preloader/has_many.rb +17 -0
  36. data/lib/active_record/associations/preloader/has_many_through.rb +19 -0
  37. data/lib/active_record/associations/preloader/has_one.rb +23 -0
  38. data/lib/active_record/associations/preloader/has_one_through.rb +9 -0
  39. data/lib/active_record/associations/preloader/singular_association.rb +21 -0
  40. data/lib/active_record/associations/preloader/through_association.rb +63 -0
  41. data/lib/active_record/associations/preloader.rb +178 -0
  42. data/lib/active_record/associations/singular_association.rb +64 -0
  43. data/lib/active_record/associations/through_association.rb +87 -0
  44. data/lib/active_record/associations.rb +512 -1224
  45. data/lib/active_record/attribute_assignment.rb +201 -0
  46. data/lib/active_record/attribute_methods/before_type_cast.rb +49 -12
  47. data/lib/active_record/attribute_methods/dirty.rb +51 -28
  48. data/lib/active_record/attribute_methods/primary_key.rb +94 -22
  49. data/lib/active_record/attribute_methods/query.rb +5 -4
  50. data/lib/active_record/attribute_methods/read.rb +63 -72
  51. data/lib/active_record/attribute_methods/serialization.rb +162 -0
  52. data/lib/active_record/attribute_methods/time_zone_conversion.rb +39 -41
  53. data/lib/active_record/attribute_methods/write.rb +39 -13
  54. data/lib/active_record/attribute_methods.rb +362 -29
  55. data/lib/active_record/autosave_association.rb +132 -75
  56. data/lib/active_record/base.rb +83 -1627
  57. data/lib/active_record/callbacks.rb +69 -47
  58. data/lib/active_record/coders/yaml_column.rb +38 -0
  59. data/lib/active_record/connection_adapters/abstract/connection_pool.rb +411 -138
  60. data/lib/active_record/connection_adapters/abstract/database_limits.rb +21 -11
  61. data/lib/active_record/connection_adapters/abstract/database_statements.rb +234 -173
  62. data/lib/active_record/connection_adapters/abstract/query_cache.rb +36 -22
  63. data/lib/active_record/connection_adapters/abstract/quoting.rb +82 -25
  64. data/lib/active_record/connection_adapters/abstract/schema_definitions.rb +176 -414
  65. data/lib/active_record/connection_adapters/abstract/schema_dumper.rb +70 -0
  66. data/lib/active_record/connection_adapters/abstract/schema_statements.rb +562 -232
  67. data/lib/active_record/connection_adapters/abstract/transaction.rb +203 -0
  68. data/lib/active_record/connection_adapters/abstract_adapter.rb +281 -53
  69. data/lib/active_record/connection_adapters/abstract_mysql_adapter.rb +782 -0
  70. data/lib/active_record/connection_adapters/column.rb +318 -0
  71. data/lib/active_record/connection_adapters/connection_specification.rb +96 -0
  72. data/lib/active_record/connection_adapters/mysql2_adapter.rb +273 -0
  73. data/lib/active_record/connection_adapters/mysql_adapter.rb +365 -450
  74. data/lib/active_record/connection_adapters/postgresql/array_parser.rb +97 -0
  75. data/lib/active_record/connection_adapters/postgresql/cast.rb +152 -0
  76. data/lib/active_record/connection_adapters/postgresql/database_statements.rb +242 -0
  77. data/lib/active_record/connection_adapters/postgresql/oid.rb +366 -0
  78. data/lib/active_record/connection_adapters/postgresql/quoting.rb +171 -0
  79. data/lib/active_record/connection_adapters/postgresql/referential_integrity.rb +30 -0
  80. data/lib/active_record/connection_adapters/postgresql/schema_statements.rb +489 -0
  81. data/lib/active_record/connection_adapters/postgresql_adapter.rb +672 -752
  82. data/lib/active_record/connection_adapters/schema_cache.rb +129 -0
  83. data/lib/active_record/connection_adapters/sqlite3_adapter.rb +588 -17
  84. data/lib/active_record/connection_adapters/statement_pool.rb +40 -0
  85. data/lib/active_record/connection_handling.rb +98 -0
  86. data/lib/active_record/core.rb +463 -0
  87. data/lib/active_record/counter_cache.rb +108 -101
  88. data/lib/active_record/dynamic_matchers.rb +131 -0
  89. data/lib/active_record/errors.rb +54 -13
  90. data/lib/active_record/explain.rb +38 -0
  91. data/lib/active_record/explain_registry.rb +30 -0
  92. data/lib/active_record/explain_subscriber.rb +29 -0
  93. data/lib/active_record/fixture_set/file.rb +55 -0
  94. data/lib/active_record/fixtures.rb +703 -785
  95. data/lib/active_record/inheritance.rb +200 -0
  96. data/lib/active_record/integration.rb +60 -0
  97. data/lib/active_record/locale/en.yml +8 -1
  98. data/lib/active_record/locking/optimistic.rb +69 -60
  99. data/lib/active_record/locking/pessimistic.rb +34 -12
  100. data/lib/active_record/log_subscriber.rb +40 -6
  101. data/lib/active_record/migration/command_recorder.rb +164 -0
  102. data/lib/active_record/migration/join_table.rb +15 -0
  103. data/lib/active_record/migration.rb +614 -216
  104. data/lib/active_record/model_schema.rb +345 -0
  105. data/lib/active_record/nested_attributes.rb +248 -119
  106. data/lib/active_record/null_relation.rb +65 -0
  107. data/lib/active_record/persistence.rb +275 -57
  108. data/lib/active_record/query_cache.rb +29 -9
  109. data/lib/active_record/querying.rb +62 -0
  110. data/lib/active_record/railtie.rb +135 -21
  111. data/lib/active_record/railties/console_sandbox.rb +5 -0
  112. data/lib/active_record/railties/controller_runtime.rb +17 -5
  113. data/lib/active_record/railties/databases.rake +249 -359
  114. data/lib/active_record/railties/jdbcmysql_error.rb +16 -0
  115. data/lib/active_record/readonly_attributes.rb +30 -0
  116. data/lib/active_record/reflection.rb +283 -103
  117. data/lib/active_record/relation/batches.rb +38 -34
  118. data/lib/active_record/relation/calculations.rb +252 -139
  119. data/lib/active_record/relation/delegation.rb +125 -0
  120. data/lib/active_record/relation/finder_methods.rb +182 -188
  121. data/lib/active_record/relation/merger.rb +161 -0
  122. data/lib/active_record/relation/predicate_builder.rb +86 -21
  123. data/lib/active_record/relation/query_methods.rb +917 -134
  124. data/lib/active_record/relation/spawn_methods.rb +53 -92
  125. data/lib/active_record/relation.rb +405 -143
  126. data/lib/active_record/result.rb +67 -0
  127. data/lib/active_record/runtime_registry.rb +17 -0
  128. data/lib/active_record/sanitization.rb +168 -0
  129. data/lib/active_record/schema.rb +20 -14
  130. data/lib/active_record/schema_dumper.rb +55 -46
  131. data/lib/active_record/schema_migration.rb +39 -0
  132. data/lib/active_record/scoping/default.rb +146 -0
  133. data/lib/active_record/scoping/named.rb +175 -0
  134. data/lib/active_record/scoping.rb +82 -0
  135. data/lib/active_record/serialization.rb +8 -46
  136. data/lib/active_record/serializers/xml_serializer.rb +21 -68
  137. data/lib/active_record/statement_cache.rb +26 -0
  138. data/lib/active_record/store.rb +156 -0
  139. data/lib/active_record/tasks/database_tasks.rb +203 -0
  140. data/lib/active_record/tasks/firebird_database_tasks.rb +56 -0
  141. data/lib/active_record/tasks/mysql_database_tasks.rb +143 -0
  142. data/lib/active_record/tasks/oracle_database_tasks.rb +45 -0
  143. data/lib/active_record/tasks/postgresql_database_tasks.rb +90 -0
  144. data/lib/active_record/tasks/sqlite_database_tasks.rb +51 -0
  145. data/lib/active_record/tasks/sqlserver_database_tasks.rb +48 -0
  146. data/lib/active_record/test_case.rb +57 -28
  147. data/lib/active_record/timestamp.rb +49 -18
  148. data/lib/active_record/transactions.rb +106 -63
  149. data/lib/active_record/translation.rb +22 -0
  150. data/lib/active_record/validations/associated.rb +25 -24
  151. data/lib/active_record/validations/presence.rb +65 -0
  152. data/lib/active_record/validations/uniqueness.rb +123 -83
  153. data/lib/active_record/validations.rb +29 -29
  154. data/lib/active_record/version.rb +7 -5
  155. data/lib/active_record.rb +83 -34
  156. data/lib/rails/generators/active_record/migration/migration_generator.rb +46 -9
  157. data/lib/rails/generators/active_record/migration/templates/create_table_migration.rb +19 -0
  158. data/lib/rails/generators/active_record/migration/templates/migration.rb +30 -8
  159. data/lib/rails/generators/active_record/model/model_generator.rb +15 -5
  160. data/lib/rails/generators/active_record/model/templates/model.rb +7 -2
  161. data/lib/rails/generators/active_record/model/templates/module.rb +3 -1
  162. data/lib/rails/generators/active_record.rb +4 -8
  163. metadata +163 -121
  164. data/CHANGELOG +0 -6023
  165. data/examples/associations.png +0 -0
  166. data/lib/active_record/association_preload.rb +0 -403
  167. data/lib/active_record/associations/association_collection.rb +0 -562
  168. data/lib/active_record/associations/association_proxy.rb +0 -295
  169. data/lib/active_record/associations/through_association_scope.rb +0 -154
  170. data/lib/active_record/connection_adapters/abstract/connection_specification.rb +0 -113
  171. data/lib/active_record/connection_adapters/sqlite_adapter.rb +0 -401
  172. data/lib/active_record/dynamic_finder_match.rb +0 -53
  173. data/lib/active_record/dynamic_scope_match.rb +0 -32
  174. data/lib/active_record/named_scope.rb +0 -138
  175. data/lib/active_record/observer.rb +0 -140
  176. data/lib/active_record/session_store.rb +0 -340
  177. data/lib/rails/generators/active_record/model/templates/migration.rb +0 -16
  178. data/lib/rails/generators/active_record/observer/observer_generator.rb +0 -15
  179. data/lib/rails/generators/active_record/observer/templates/observer.rb +0 -2
  180. data/lib/rails/generators/active_record/session_migration/session_migration_generator.rb +0 -24
  181. data/lib/rails/generators/active_record/session_migration/templates/migration.rb +0 -16
@@ -0,0 +1,200 @@
1
+ require 'active_support/core_ext/hash/indifferent_access'
2
+
3
+ module ActiveRecord
4
+ module Inheritance
5
+ extend ActiveSupport::Concern
6
+
7
+ included do
8
+ # Determine whether to store the full constant name including namespace when using STI
9
+ class_attribute :store_full_sti_class, instance_writer: false
10
+ self.store_full_sti_class = true
11
+ end
12
+
13
+ module ClassMethods
14
+ # Determines if one of the attributes passed in is the inheritance column,
15
+ # and if the inheritance column is attr accessible, it initializes an
16
+ # instance of the given subclass instead of the base class
17
+ def new(*args, &block)
18
+ if abstract_class? || self == Base
19
+ raise NotImplementedError, "#{self} is an abstract class and can not be instantiated."
20
+ end
21
+ if (attrs = args.first).is_a?(Hash)
22
+ if subclass = subclass_from_attrs(attrs)
23
+ return subclass.new(*args, &block)
24
+ end
25
+ end
26
+ # Delegate to the original .new
27
+ super
28
+ end
29
+
30
+ # True if this isn't a concrete subclass needing a STI type condition.
31
+ def descends_from_active_record?
32
+ if self == Base
33
+ false
34
+ elsif superclass.abstract_class?
35
+ superclass.descends_from_active_record?
36
+ else
37
+ superclass == Base || !columns_hash.include?(inheritance_column)
38
+ end
39
+ end
40
+
41
+ def finder_needs_type_condition? #:nodoc:
42
+ # This is like this because benchmarking justifies the strange :false stuff
43
+ :true == (@finder_needs_type_condition ||= descends_from_active_record? ? :false : :true)
44
+ end
45
+
46
+ def symbolized_base_class
47
+ @symbolized_base_class ||= base_class.to_s.to_sym
48
+ end
49
+
50
+ def symbolized_sti_name
51
+ @symbolized_sti_name ||= sti_name.present? ? sti_name.to_sym : symbolized_base_class
52
+ end
53
+
54
+ # Returns the class descending directly from ActiveRecord::Base, or
55
+ # an abstract class, if any, in the inheritance hierarchy.
56
+ #
57
+ # If A extends AR::Base, A.base_class will return A. If B descends from A
58
+ # through some arbitrarily deep hierarchy, B.base_class will return A.
59
+ #
60
+ # If B < A and C < B and if A is an abstract_class then both B.base_class
61
+ # and C.base_class would return B as the answer since A is an abstract_class.
62
+ def base_class
63
+ unless self < Base
64
+ raise ActiveRecordError, "#{name} doesn't belong in a hierarchy descending from ActiveRecord"
65
+ end
66
+
67
+ if superclass == Base || superclass.abstract_class?
68
+ self
69
+ else
70
+ superclass.base_class
71
+ end
72
+ end
73
+
74
+ # Set this to true if this is an abstract class (see <tt>abstract_class?</tt>).
75
+ # If you are using inheritance with ActiveRecord and don't want child classes
76
+ # to utilize the implied STI table name of the parent class, this will need to be true.
77
+ # For example, given the following:
78
+ #
79
+ # class SuperClass < ActiveRecord::Base
80
+ # self.abstract_class = true
81
+ # end
82
+ # class Child < SuperClass
83
+ # self.table_name = 'the_table_i_really_want'
84
+ # end
85
+ #
86
+ #
87
+ # <tt>self.abstract_class = true</tt> is required to make <tt>Child<.find,.create, or any Arel method></tt> use <tt>the_table_i_really_want</tt> instead of a table called <tt>super_classes</tt>
88
+ #
89
+ attr_accessor :abstract_class
90
+
91
+ # Returns whether this class is an abstract class or not.
92
+ def abstract_class?
93
+ defined?(@abstract_class) && @abstract_class == true
94
+ end
95
+
96
+ def sti_name
97
+ store_full_sti_class ? name : name.demodulize
98
+ end
99
+
100
+ protected
101
+
102
+ # Returns the class type of the record using the current module as a prefix. So descendants of
103
+ # MyApp::Business::Account would appear as MyApp::Business::AccountSubclass.
104
+ def compute_type(type_name)
105
+ if type_name.match(/^::/)
106
+ # If the type is prefixed with a scope operator then we assume that
107
+ # the type_name is an absolute reference.
108
+ ActiveSupport::Dependencies.constantize(type_name)
109
+ else
110
+ # Build a list of candidates to search for
111
+ candidates = []
112
+ name.scan(/::|$/) { candidates.unshift "#{$`}::#{type_name}" }
113
+ candidates << type_name
114
+
115
+ candidates.each do |candidate|
116
+ begin
117
+ constant = ActiveSupport::Dependencies.constantize(candidate)
118
+ return constant if candidate == constant.to_s
119
+ rescue NameError => e
120
+ # We don't want to swallow NoMethodError < NameError errors
121
+ raise e unless e.instance_of?(NameError)
122
+ end
123
+ end
124
+
125
+ raise NameError, "uninitialized constant #{candidates.first}"
126
+ end
127
+ end
128
+
129
+ private
130
+
131
+ # Called by +instantiate+ to decide which class to use for a new
132
+ # record instance. For single-table inheritance, we check the record
133
+ # for a +type+ column and return the corresponding class.
134
+ def discriminate_class_for_record(record)
135
+ if using_single_table_inheritance?(record)
136
+ find_sti_class(record[inheritance_column])
137
+ else
138
+ super
139
+ end
140
+ end
141
+
142
+ def using_single_table_inheritance?(record)
143
+ record[inheritance_column].present? && columns_hash.include?(inheritance_column)
144
+ end
145
+
146
+ def find_sti_class(type_name)
147
+ if store_full_sti_class
148
+ ActiveSupport::Dependencies.constantize(type_name)
149
+ else
150
+ compute_type(type_name)
151
+ end
152
+ rescue NameError
153
+ raise SubclassNotFound,
154
+ "The single-table inheritance mechanism failed to locate the subclass: '#{type_name}'. " +
155
+ "This error is raised because the column '#{inheritance_column}' is reserved for storing the class in case of inheritance. " +
156
+ "Please rename this column if you didn't intend it to be used for storing the inheritance class " +
157
+ "or overwrite #{name}.inheritance_column to use another column for that information."
158
+ end
159
+
160
+ def type_condition(table = arel_table)
161
+ sti_column = table[inheritance_column.to_sym]
162
+ sti_names = ([self] + descendants).map { |model| model.sti_name }
163
+
164
+ sti_column.in(sti_names)
165
+ end
166
+
167
+ # Detect the subclass from the inheritance column of attrs. If the inheritance column value
168
+ # is not self or a valid subclass, raises ActiveRecord::SubclassNotFound
169
+ # If this is a StrongParameters hash, and access to inheritance_column is not permitted,
170
+ # this will ignore the inheritance column and return nil
171
+ def subclass_from_attrs(attrs)
172
+ subclass_name = attrs.with_indifferent_access[inheritance_column]
173
+
174
+ if subclass_name.present? && subclass_name != self.name
175
+ subclass = subclass_name.safe_constantize
176
+
177
+ unless descendants.include?(subclass)
178
+ raise ActiveRecord::SubclassNotFound.new("Invalid single-table inheritance type: #{subclass_name} is not a subclass of #{name}")
179
+ end
180
+
181
+ subclass
182
+ end
183
+ end
184
+ end
185
+
186
+ private
187
+
188
+ # Sets the attribute used for single table inheritance to this class name if this is not the
189
+ # ActiveRecord::Base descendant.
190
+ # Considering the hierarchy Reply < Message < ActiveRecord::Base, this makes it possible to
191
+ # do Reply.new without having to set <tt>Reply[Reply.inheritance_column] = "Reply"</tt> yourself.
192
+ # No such attribute would be set for objects of the Message class in that example.
193
+ def ensure_proper_type
194
+ klass = self.class
195
+ if klass.finder_needs_type_condition?
196
+ write_attribute(klass.inheritance_column, klass.sti_name)
197
+ end
198
+ end
199
+ end
200
+ end
@@ -0,0 +1,60 @@
1
+ module ActiveRecord
2
+ module Integration
3
+ extend ActiveSupport::Concern
4
+
5
+ included do
6
+ ##
7
+ # :singleton-method:
8
+ # Indicates the format used to generate the timestamp in the cache key.
9
+ # Accepts any of the symbols in <tt>Time::DATE_FORMATS</tt>.
10
+ #
11
+ # This is +:nsec+, by default.
12
+ class_attribute :cache_timestamp_format, :instance_writer => false
13
+ self.cache_timestamp_format = :nsec
14
+ end
15
+
16
+ # Returns a String, which Action Pack uses for constructing an URL to this
17
+ # object. The default implementation returns this record's id as a String,
18
+ # or nil if this record's unsaved.
19
+ #
20
+ # For example, suppose that you have a User model, and that you have a
21
+ # <tt>resources :users</tt> route. Normally, +user_path+ will
22
+ # construct a path with the user object's 'id' in it:
23
+ #
24
+ # user = User.find_by(name: 'Phusion')
25
+ # user_path(user) # => "/users/1"
26
+ #
27
+ # You can override +to_param+ in your model to make +user_path+ construct
28
+ # a path using the user's name instead of the user's id:
29
+ #
30
+ # class User < ActiveRecord::Base
31
+ # def to_param # overridden
32
+ # name
33
+ # end
34
+ # end
35
+ #
36
+ # user = User.find_by(name: 'Phusion')
37
+ # user_path(user) # => "/users/Phusion"
38
+ def to_param
39
+ # We can't use alias_method here, because method 'id' optimizes itself on the fly.
40
+ id && id.to_s # Be sure to stringify the id for routes
41
+ end
42
+
43
+ # Returns a cache key that can be used to identify this record.
44
+ #
45
+ # Product.new.cache_key # => "products/new"
46
+ # Product.find(5).cache_key # => "products/5" (updated_at not available)
47
+ # Person.find(5).cache_key # => "people/5-20071224150000" (updated_at available)
48
+ def cache_key
49
+ case
50
+ when new_record?
51
+ "#{self.class.model_name.cache_key}/new"
52
+ when timestamp = max_updated_column_timestamp
53
+ timestamp = timestamp.utc.to_s(cache_timestamp_format)
54
+ "#{self.class.model_name.cache_key}/#{id}-#{timestamp}"
55
+ else
56
+ "#{self.class.model_name.cache_key}/#{id}"
57
+ end
58
+ end
59
+ end
60
+ end
@@ -4,12 +4,19 @@ en:
4
4
  #created_at: "Created at"
5
5
  #updated_at: "Updated at"
6
6
 
7
+ # Default error messages
8
+ errors:
9
+ messages:
10
+ taken: "has already been taken"
11
+
7
12
  # Active Record models configuration
8
13
  activerecord:
9
14
  errors:
10
15
  messages:
11
- taken: "has already been taken"
12
16
  record_invalid: "Validation failed: %{errors}"
17
+ restrict_dependent_destroy:
18
+ one: "Cannot delete record because a dependent %{record} exists"
19
+ many: "Cannot delete record because dependent %{record} exist"
13
20
  # Append your own errors here or at the model/attributes scope.
14
21
 
15
22
  # You can define own errors for models or model attributes.
@@ -3,16 +3,17 @@ module ActiveRecord
3
3
  # == What is Optimistic Locking
4
4
  #
5
5
  # Optimistic locking allows multiple users to access the same record for edits, and assumes a minimum of
6
- # conflicts with the data. It does this by checking whether another process has made changes to a record since
7
- # it was opened, an ActiveRecord::StaleObjectError is thrown if that has occurred and the update is ignored.
6
+ # conflicts with the data. It does this by checking whether another process has made changes to a record since
7
+ # it was opened, an <tt>ActiveRecord::StaleObjectError</tt> exception is thrown if that has occurred
8
+ # and the update is ignored.
8
9
  #
9
- # Check out ActiveRecord::Locking::Pessimistic for an alternative.
10
+ # Check out <tt>ActiveRecord::Locking::Pessimistic</tt> for an alternative.
10
11
  #
11
12
  # == Usage
12
13
  #
13
- # Active Records support optimistic locking if the field <tt>lock_version</tt> is present. Each update to the
14
- # record increments the lock_version column and the locking facilities ensure that records instantiated twice
15
- # will let the last one saved raise a StaleObjectError if the first was also updated. Example:
14
+ # Active Records support optimistic locking if the field +lock_version+ is present. Each update to the
15
+ # record increments the +lock_version+ column and the locking facilities ensure that records instantiated twice
16
+ # will let the last one saved raise a +StaleObjectError+ if the first was also updated. Example:
16
17
  #
17
18
  # p1 = Person.find(1)
18
19
  # p2 = Person.find(1)
@@ -23,7 +24,7 @@ module ActiveRecord
23
24
  # p2.first_name = "should fail"
24
25
  # p2.save # Raises a ActiveRecord::StaleObjectError
25
26
  #
26
- # Optimistic locking will also check for stale data when objects are destroyed. Example:
27
+ # Optimistic locking will also check for stale data when objects are destroyed. Example:
27
28
  #
28
29
  # p1 = Person.find(1)
29
30
  # p2 = Person.find(1)
@@ -36,21 +37,22 @@ module ActiveRecord
36
37
  # You're then responsible for dealing with the conflict by rescuing the exception and either rolling back, merging,
37
38
  # or otherwise apply the business logic needed to resolve the conflict.
38
39
  #
39
- # You must ensure that your database schema defaults the lock_version column to 0.
40
+ # This locking mechanism will function inside a single Ruby process. To make it work across all
41
+ # web requests, the recommended approach is to add +lock_version+ as a hidden field to your form.
40
42
  #
41
43
  # This behavior can be turned off by setting <tt>ActiveRecord::Base.lock_optimistically = false</tt>.
42
- # To override the name of the lock_version column, invoke the <tt>set_locking_column</tt> method.
43
- # This method uses the same syntax as <tt>set_table_name</tt>
44
+ # To override the name of the +lock_version+ column, set the <tt>locking_column</tt> class attribute:
45
+ #
46
+ # class Person < ActiveRecord::Base
47
+ # self.locking_column = :lock_person
48
+ # end
49
+ #
44
50
  module Optimistic
45
51
  extend ActiveSupport::Concern
46
52
 
47
53
  included do
48
- cattr_accessor :lock_optimistically, :instance_writer => false
54
+ class_attribute :lock_optimistically, instance_writer: false
49
55
  self.lock_optimistically = true
50
-
51
- class << self
52
- alias_method :locking_column=, :set_locking_column
53
- end
54
56
  end
55
57
 
56
58
  def locking_enabled? #:nodoc:
@@ -58,28 +60,19 @@ module ActiveRecord
58
60
  end
59
61
 
60
62
  private
61
- def attributes_from_column_definition
62
- result = super
63
-
64
- # If the locking column has no default value set,
65
- # start the lock version at zero. Note we can't use
66
- # locking_enabled? at this point as @attributes may
67
- # not have been initialized yet
68
-
69
- if lock_optimistically && result.include?(self.class.locking_column)
70
- result[self.class.locking_column] ||= 0
71
- end
72
-
73
- return result
63
+ def increment_lock
64
+ lock_col = self.class.locking_column
65
+ previous_lock_value = send(lock_col).to_i
66
+ send(lock_col + '=', previous_lock_value + 1)
74
67
  end
75
68
 
76
- def update(attribute_names = @attributes.keys) #:nodoc:
69
+ def update_record(attribute_names = @attributes.keys) #:nodoc:
77
70
  return super unless locking_enabled?
78
71
  return 0 if attribute_names.empty?
79
72
 
80
73
  lock_col = self.class.locking_column
81
- previous_value = send(lock_col).to_i
82
- send(lock_col + '=', previous_value + 1)
74
+ previous_lock_value = send(lock_col).to_i
75
+ increment_lock
83
76
 
84
77
  attribute_names += [lock_col]
85
78
  attribute_names.uniq!
@@ -87,67 +80,71 @@ module ActiveRecord
87
80
  begin
88
81
  relation = self.class.unscoped
89
82
 
90
- affected_rows = relation.where(
91
- relation.table[self.class.primary_key].eq(quoted_id).and(
92
- relation.table[self.class.locking_column].eq(quote_value(previous_value))
83
+ stmt = relation.where(
84
+ relation.table[self.class.primary_key].eq(id).and(
85
+ relation.table[lock_col].eq(self.class.quote_value(previous_lock_value))
93
86
  )
94
- ).arel.update(arel_attributes_values(false, false, attribute_names))
87
+ ).arel.compile_update(arel_attributes_with_values_for_update(attribute_names))
88
+
89
+ affected_rows = self.class.connection.update stmt
95
90
 
96
91
  unless affected_rows == 1
97
- raise ActiveRecord::StaleObjectError, "Attempted to update a stale object: #{self.class.name}"
92
+ raise ActiveRecord::StaleObjectError.new(self, "update")
98
93
  end
99
94
 
100
95
  affected_rows
101
96
 
102
97
  # If something went wrong, revert the version.
103
98
  rescue Exception
104
- send(lock_col + '=', previous_value)
99
+ send(lock_col + '=', previous_lock_value)
105
100
  raise
106
101
  end
107
102
  end
108
103
 
109
- def destroy #:nodoc:
110
- return super unless locking_enabled?
104
+ def destroy_row
105
+ affected_rows = super
111
106
 
112
- unless new_record?
113
- lock_col = self.class.locking_column
114
- previous_value = send(lock_col).to_i
107
+ if locking_enabled? && affected_rows != 1
108
+ raise ActiveRecord::StaleObjectError.new(self, "destroy")
109
+ end
115
110
 
116
- table = self.class.arel_table
117
- predicate = table[self.class.primary_key].eq(id)
118
- predicate = predicate.and(table[self.class.locking_column].eq(previous_value))
111
+ affected_rows
112
+ end
119
113
 
120
- affected_rows = self.class.unscoped.where(predicate).delete_all
114
+ def relation_for_destroy
115
+ relation = super
121
116
 
122
- unless affected_rows == 1
123
- raise ActiveRecord::StaleObjectError, "Attempted to delete a stale object: #{self.class.name}"
124
- end
117
+ if locking_enabled?
118
+ column_name = self.class.locking_column
119
+ column = self.class.columns_hash[column_name]
120
+ substitute = self.class.connection.substitute_at(column, relation.bind_values.length)
121
+
122
+ relation = relation.where(self.class.arel_table[column_name].eq(substitute))
123
+ relation.bind_values << [column, self[column_name].to_i]
125
124
  end
126
125
 
127
- @destroyed = true
128
- freeze
126
+ relation
129
127
  end
130
128
 
131
129
  module ClassMethods
132
130
  DEFAULT_LOCKING_COLUMN = 'lock_version'
133
131
 
134
- # Is optimistic locking enabled for this table? Returns true if the
135
- # +lock_optimistically+ flag is set to true (which it is, by default)
136
- # and the table includes the +locking_column+ column (defaults to
137
- # +lock_version+).
132
+ # Returns true if the +lock_optimistically+ flag is set to true
133
+ # (which it is, by default) and the table includes the
134
+ # +locking_column+ column (defaults to +lock_version+).
138
135
  def locking_enabled?
139
136
  lock_optimistically && columns_hash[locking_column]
140
137
  end
141
138
 
142
139
  # Set the column to use for optimistic locking. Defaults to +lock_version+.
143
- def set_locking_column(value = nil, &block)
144
- define_attr_method :locking_column, value, &block
145
- value
140
+ def locking_column=(value)
141
+ @locking_column = value.to_s
146
142
  end
147
143
 
148
144
  # The version column used for optimistic locking. Defaults to +lock_version+.
149
145
  def locking_column
150
- reset_locking_column
146
+ reset_locking_column unless defined?(@locking_column)
147
+ @locking_column
151
148
  end
152
149
 
153
150
  # Quote the column name used for optimistic locking.
@@ -157,7 +154,7 @@ module ActiveRecord
157
154
 
158
155
  # Reset the column used for optimistic locking back to the +lock_version+ default.
159
156
  def reset_locking_column
160
- set_locking_column DEFAULT_LOCKING_COLUMN
157
+ self.locking_column = DEFAULT_LOCKING_COLUMN
161
158
  end
162
159
 
163
160
  # Make sure the lock version column gets updated when counters are
@@ -166,6 +163,18 @@ module ActiveRecord
166
163
  counters = counters.merge(locking_column => 1) if locking_enabled?
167
164
  super
168
165
  end
166
+
167
+ def column_defaults
168
+ @column_defaults ||= begin
169
+ defaults = super
170
+
171
+ if defaults.key?(locking_column) && lock_optimistically
172
+ defaults[locking_column] ||= 0
173
+ end
174
+
175
+ defaults
176
+ end
177
+ end
169
178
  end
170
179
  end
171
180
  end
@@ -3,30 +3,30 @@ module ActiveRecord
3
3
  # Locking::Pessimistic provides support for row-level locking using
4
4
  # SELECT ... FOR UPDATE and other lock types.
5
5
  #
6
- # Pass <tt>:lock => true</tt> to ActiveRecord::Base.find to obtain an exclusive
6
+ # Pass <tt>lock: true</tt> to <tt>ActiveRecord::Base.find</tt> to obtain an exclusive
7
7
  # lock on the selected rows:
8
8
  # # select * from accounts where id=1 for update
9
- # Account.find(1, :lock => true)
9
+ # Account.find(1, lock: true)
10
10
  #
11
- # Pass <tt>:lock => 'some locking clause'</tt> to give a database-specific locking clause
12
- # of your own such as 'LOCK IN SHARE MODE' or 'FOR UPDATE NOWAIT'.
11
+ # Pass <tt>lock: 'some locking clause'</tt> to give a database-specific locking clause
12
+ # of your own such as 'LOCK IN SHARE MODE' or 'FOR UPDATE NOWAIT'. Example:
13
13
  #
14
- # Example:
15
14
  # Account.transaction do
16
15
  # # select * from accounts where name = 'shugo' limit 1 for update
17
- # shugo = Account.find(:first, :conditions => "name = 'shugo'", :lock => true)
18
- # yuko = Account.find(:first, :conditions => "name = 'yuko'", :lock => true)
16
+ # shugo = Account.where("name = 'shugo'").lock(true).first
17
+ # yuko = Account.where("name = 'yuko'").lock(true).first
19
18
  # shugo.balance -= 100
20
19
  # shugo.save!
21
20
  # yuko.balance += 100
22
21
  # yuko.save!
23
22
  # end
24
23
  #
25
- # You can also use ActiveRecord::Base#lock! method to lock one record by id.
24
+ # You can also use <tt>ActiveRecord::Base#lock!</tt> method to lock one record by id.
26
25
  # This may be better if you don't need to lock every row. Example:
26
+ #
27
27
  # Account.transaction do
28
28
  # # select * from accounts where ...
29
- # accounts = Account.find(:all, :conditions => ...)
29
+ # accounts = Account.where(...)
30
30
  # account1 = accounts.detect { |account| ... }
31
31
  # account2 = accounts.detect { |account| ... }
32
32
  # # select * from accounts where id=? for update
@@ -38,18 +38,40 @@ module ActiveRecord
38
38
  # account2.save!
39
39
  # end
40
40
  #
41
+ # You can start a transaction and acquire the lock in one go by calling
42
+ # <tt>with_lock</tt> with a block. The block is called from within
43
+ # a transaction, the object is already locked. Example:
44
+ #
45
+ # account = Account.first
46
+ # account.with_lock do
47
+ # # This block is called within a transaction,
48
+ # # account is already locked.
49
+ # account.balance -= 100
50
+ # account.save!
51
+ # end
52
+ #
41
53
  # Database-specific information on row locking:
42
54
  # MySQL: http://dev.mysql.com/doc/refman/5.1/en/innodb-locking-reads.html
43
- # PostgreSQL: http://www.postgresql.org/docs/8.1/interactive/sql-select.html#SQL-FOR-UPDATE-SHARE
55
+ # PostgreSQL: http://www.postgresql.org/docs/current/interactive/sql-select.html#SQL-FOR-UPDATE-SHARE
44
56
  module Pessimistic
45
57
  # Obtain a row lock on this record. Reloads the record to obtain the requested
46
58
  # lock. Pass an SQL locking clause to append the end of the SELECT statement
47
- # or pass true for "FOR UPDATE" (the default, an exclusive row lock). Returns
59
+ # or pass true for "FOR UPDATE" (the default, an exclusive row lock). Returns
48
60
  # the locked record.
49
61
  def lock!(lock = true)
50
- reload(:lock => lock) unless new_record?
62
+ reload(:lock => lock) if persisted?
51
63
  self
52
64
  end
65
+
66
+ # Wraps the passed block in a transaction, locking the object
67
+ # before yielding. You pass can the SQL locking clause
68
+ # as argument (see <tt>lock!</tt>).
69
+ def with_lock(lock = true)
70
+ transaction do
71
+ lock!(lock)
72
+ yield
73
+ end
74
+ end
53
75
  end
54
76
  end
55
77
  end
@@ -1,11 +1,13 @@
1
1
  module ActiveRecord
2
2
  class LogSubscriber < ActiveSupport::LogSubscriber
3
+ IGNORE_PAYLOAD_NAMES = ["SCHEMA", "EXPLAIN"]
4
+
3
5
  def self.runtime=(value)
4
- Thread.current["active_record_sql_runtime"] = value
6
+ ActiveRecord::RuntimeRegistry.sql_runtime = value
5
7
  end
6
8
 
7
9
  def self.runtime
8
- Thread.current["active_record_sql_runtime"] ||= 0
10
+ ActiveRecord::RuntimeRegistry.sql_runtime ||= 0
9
11
  end
10
12
 
11
13
  def self.reset_runtime
@@ -18,12 +20,35 @@ module ActiveRecord
18
20
  @odd_or_even = false
19
21
  end
20
22
 
23
+ def render_bind(column, value)
24
+ if column
25
+ if column.binary?
26
+ value = "<#{value.bytesize} bytes of binary data>"
27
+ end
28
+
29
+ [column.name, value]
30
+ else
31
+ [nil, value]
32
+ end
33
+ end
34
+
21
35
  def sql(event)
22
36
  self.class.runtime += event.duration
23
37
  return unless logger.debug?
24
38
 
25
- name = '%s (%.1fms)' % [event.payload[:name], event.duration]
26
- sql = event.payload[:sql].squeeze(' ')
39
+ payload = event.payload
40
+
41
+ return if IGNORE_PAYLOAD_NAMES.include?(payload[:name])
42
+
43
+ name = "#{payload[:name]} (#{event.duration.round(1)}ms)"
44
+ sql = payload[:sql].squeeze(' ')
45
+ binds = nil
46
+
47
+ unless (payload[:binds] || []).empty?
48
+ binds = " " + payload[:binds].map { |col,v|
49
+ render_bind(col, v)
50
+ }.inspect
51
+ end
27
52
 
28
53
  if odd?
29
54
  name = color(name, CYAN, true)
@@ -32,7 +57,16 @@ module ActiveRecord
32
57
  name = color(name, MAGENTA, true)
33
58
  end
34
59
 
35
- debug " #{name} #{sql}"
60
+ debug " #{name} #{sql}#{binds}"
61
+ end
62
+
63
+ def identity(event)
64
+ return unless logger.debug?
65
+
66
+ name = color(event.payload[:name], odd? ? CYAN : MAGENTA, true)
67
+ line = odd? ? color(event.payload[:line], nil, true) : event.payload[:line]
68
+
69
+ debug " #{name} #{line}"
36
70
  end
37
71
 
38
72
  def odd?
@@ -45,4 +79,4 @@ module ActiveRecord
45
79
  end
46
80
  end
47
81
 
48
- ActiveRecord::LogSubscriber.attach_to :active_record
82
+ ActiveRecord::LogSubscriber.attach_to :active_record