diff options
Diffstat (limited to 'activerecord')
-rw-r--r-- | activerecord/CHANGELOG.md | 29 | ||||
-rw-r--r-- | activerecord/lib/active_record/attribute_methods.rb | 3 | ||||
-rw-r--r-- | activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb | 16 | ||||
-rw-r--r-- | activerecord/lib/active_record/core.rb | 45 | ||||
-rw-r--r-- | activerecord/lib/active_record/locking/pessimistic.rb | 22 | ||||
-rw-r--r-- | activerecord/lib/active_record/railtie.rb | 3 | ||||
-rw-r--r-- | activerecord/lib/active_record/relation/query_methods.rb | 10 | ||||
-rw-r--r-- | activerecord/test/cases/attribute_methods/read_test.rb | 8 | ||||
-rw-r--r-- | activerecord/test/cases/base_test.rb | 31 | ||||
-rw-r--r-- | activerecord/test/cases/locking_test.rb | 20 |
10 files changed, 147 insertions, 40 deletions
diff --git a/activerecord/CHANGELOG.md b/activerecord/CHANGELOG.md index f7de341fbe..a458f9e6e4 100644 --- a/activerecord/CHANGELOG.md +++ b/activerecord/CHANGELOG.md @@ -72,6 +72,33 @@ ## Rails 3.2.0 (unreleased) ## +* Added a `with_lock` method to ActiveRecord objects, which starts + a transaction, locks the object (pessimistically) and yields to the block. + The method takes one (optional) parameter and passes it to `lock!`. + + Before: + + class Order < ActiveRecord::Base + def cancel! + transaction do + lock! + # ... cancelling logic + end + end + end + + After: + + class Order < ActiveRecord::Base + def cancel! + with_lock do + # ... cancelling logic + end + end + end + + *Olek Janiszewski* + * 'on' and 'ON' boolean columns values are type casted to true *Santiago Pastorino* @@ -82,7 +109,7 @@ Example: rake db:migrate SCOPE=blog - *Piotr Sarnacki* + *Piotr Sarnacki* * Migrations copied from engines are now scoped with engine's name, for example 01_create_posts.blog.rb. *Piotr Sarnacki* diff --git a/activerecord/lib/active_record/attribute_methods.rb b/activerecord/lib/active_record/attribute_methods.rb index f8815fefc9..a1e34a3aa1 100644 --- a/activerecord/lib/active_record/attribute_methods.rb +++ b/activerecord/lib/active_record/attribute_methods.rb @@ -1,6 +1,5 @@ require 'active_support/core_ext/enumerable' require 'active_support/deprecation' -require 'thread' module ActiveRecord # = Active Record Attribute Methods @@ -38,8 +37,6 @@ module ActiveRecord def define_attribute_methods # Use a mutex; we don't want two thread simaltaneously trying to define # attribute methods. - @attribute_methods_mutex ||= Mutex.new - @attribute_methods_mutex.synchronize do return if attribute_methods_generated? superclass.define_attribute_methods unless self == base_class diff --git a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb index e6ddf8bf8f..dbfb375ba8 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb @@ -171,15 +171,15 @@ module ActiveRecord # Extracts the value from a PostgreSQL column default definition. def self.extract_value_from_default(default) + # This is a performance optimization for Ruby 1.9.2 in development. + # If the value is nil, we return nil straight away without checking + # the regular expressions. If we check each regular expression, + # Regexp#=== will call NilClass#to_str, which will trigger + # method_missing (defined by whiny nil in ActiveSupport) which + # makes this method very very slow. + return default unless default + case default - # This is a performance optimization for Ruby 1.9.2 in development. - # If the value is nil, we return nil straight away without checking - # the regular expressions. If we check each regular expression, - # Regexp#=== will call NilClass#to_str, which will trigger - # method_missing (defined by whiny nil in ActiveSupport) which - # makes this method very very slow. - when NilClass - nil # Numeric types when /\A\(?(-?\d+(\.\d*)?\)?)\z/ $1 diff --git a/activerecord/lib/active_record/core.rb b/activerecord/lib/active_record/core.rb index 22574c4ce7..adba3f710c 100644 --- a/activerecord/lib/active_record/core.rb +++ b/activerecord/lib/active_record/core.rb @@ -1,4 +1,5 @@ require 'active_support/concern' +require 'thread' module ActiveRecord module Core @@ -44,10 +45,10 @@ module ActiveRecord ## # :singleton-method: - # Determines whether to use Time.local (using :local) or Time.utc (using :utc) when pulling - # dates and times from the database. This is set to :local by default. + # Determines whether to use Time.utc (using :utc) or Time.local (using :local) when pulling + # dates and times from the database. This is set to :utc by default. config_attribute :default_timezone, :global => true - self.default_timezone = :local + self.default_timezone = :utc ## # :singleton-method: @@ -80,6 +81,8 @@ module ActiveRecord end def initialize_generated_modules + @attribute_methods_mutex = Mutex.new + # force attribute methods to be higher in inheritance hierarchy than other generated methods generated_attribute_methods generated_feature_methods @@ -152,16 +155,8 @@ module ActiveRecord # User.new({ :first_name => 'Jamie', :is_admin => true }, :without_protection => true) def initialize(attributes = nil, options = {}) @attributes = self.class.initialize_attributes(self.class.column_defaults.dup) - @association_cache = {} - @aggregation_cache = {} - @attributes_cache = {} - @new_record = true - @readonly = false - @destroyed = false - @marked_for_destruction = false - @previously_changed = {} - @changed_attributes = {} - @relation = nil + + init_internals ensure_proper_type @@ -185,13 +180,11 @@ module ActiveRecord # post.title # => 'hello world' def init_with(coder) @attributes = self.class.initialize_attributes(coder['attributes']) - @relation = nil - @attributes_cache, @previously_changed, @changed_attributes = {}, {}, {} - @association_cache = {} - @aggregation_cache = {} - @readonly = @destroyed = @marked_for_destruction = false + init_internals + @new_record = false + run_callbacks :find run_callbacks :initialize @@ -219,7 +212,8 @@ module ActiveRecord @aggregation_cache = {} @association_cache = {} - @attributes_cache = {} + @attributes_cache = {} + @new_record = true ensure_proper_type @@ -330,5 +324,18 @@ module ActiveRecord def to_ary # :nodoc: nil end + + def init_internals + @relation = nil + @aggregation_cache = {} + @association_cache = {} + @attributes_cache = {} + @previously_changed = {} + @changed_attributes = {} + @readonly = false + @destroyed = false + @marked_for_destruction = false + @new_record = true + end end end diff --git a/activerecord/lib/active_record/locking/pessimistic.rb b/activerecord/lib/active_record/locking/pessimistic.rb index 66994e4797..58af92f0b1 100644 --- a/activerecord/lib/active_record/locking/pessimistic.rb +++ b/activerecord/lib/active_record/locking/pessimistic.rb @@ -38,6 +38,18 @@ module ActiveRecord # account2.save! # end # + # You can start a transaction and acquire the lock in one go by calling + # <tt>with_lock</tt> with a block. The block is called from within + # a transaction, the object is already locked. Example: + # + # account = Account.first + # account.with_lock do + # # This block is called within a transaction, + # # account is already locked. + # account.balance -= 100 + # account.save! + # end + # # Database-specific information on row locking: # MySQL: http://dev.mysql.com/doc/refman/5.1/en/innodb-locking-reads.html # PostgreSQL: http://www.postgresql.org/docs/current/interactive/sql-select.html#SQL-FOR-UPDATE-SHARE @@ -50,6 +62,16 @@ module ActiveRecord reload(:lock => lock) if persisted? self end + + # Wraps the passed block in a transaction, locking the object + # before yielding. You pass can the SQL locking clause + # as argument (see <tt>lock!</tt>). + def with_lock(lock = true) + transaction do + lock!(lock) + yield + end + end end end end diff --git a/activerecord/lib/active_record/railtie.rb b/activerecord/lib/active_record/railtie.rb index b1c8ae5b77..058dd58efb 100644 --- a/activerecord/lib/active_record/railtie.rb +++ b/activerecord/lib/active_record/railtie.rb @@ -38,7 +38,8 @@ module ActiveRecord # first time. Also, make it output to STDERR. console do |app| require "active_record/railties/console_sandbox" if app.sandbox? - ActiveRecord::Base.logger = ActiveSupport::Logger.new(STDERR) + console = ActiveSupport::Logger.new(STDERR) + Rails.logger.extend ActiveSupport::Logger.broadcast console end initializer "active_record.initialize_timezone" do diff --git a/activerecord/lib/active_record/relation/query_methods.rb b/activerecord/lib/active_record/relation/query_methods.rb index b5f202ef6a..a8ae7208fc 100644 --- a/activerecord/lib/active_record/relation/query_methods.rb +++ b/activerecord/lib/active_record/relation/query_methods.rb @@ -75,16 +75,16 @@ module ActiveRecord # array, it actually returns a relation object and can have other query # methods appended to it, such as the other methods in ActiveRecord::QueryMethods. # - # This method will also take multiple parameters: + # The argument to the method can also be an array of fields. # - # >> Model.select(:field, :other_field, :and_one_more) + # >> Model.select([:field, :other_field, :and_one_more]) # => [#<Model field: "value", other_field: "value", and_one_more: "value">] # - # Any attributes that do not have fields retrieved by a select - # will return `nil` when the getter method for that attribute is used: + # Accessing attributes of an object that do not have fields retrieved by a select + # will throw <tt>ActiveModel::MissingAttributeError</tt>: # # >> Model.select(:field).first.other_field - # => nil + # => ActiveModel::MissingAttributeError: missing attribute: other_field def select(value = Proc.new) if block_given? to_a.select {|*block_args| value.call(*block_args) } diff --git a/activerecord/test/cases/attribute_methods/read_test.rb b/activerecord/test/cases/attribute_methods/read_test.rb index 0df9ffc0c5..764305459d 100644 --- a/activerecord/test/cases/attribute_methods/read_test.rb +++ b/activerecord/test/cases/attribute_methods/read_test.rb @@ -1,5 +1,6 @@ require "cases/helper" require 'active_support/core_ext/object/inclusion' +require 'thread' module ActiveRecord module AttributeMethods @@ -20,6 +21,13 @@ module ActiveRecord include ActiveRecord::AttributeMethods + def self.define_attribute_methods + # Created in the inherited/included hook for "proper" ARs + @attribute_methods_mutex ||= Mutex.new + + super + end + def self.column_names %w{ one two three } end diff --git a/activerecord/test/cases/base_test.rb b/activerecord/test/cases/base_test.rb index f5c139e85f..87482e4181 100644 --- a/activerecord/test/cases/base_test.rb +++ b/activerecord/test/cases/base_test.rb @@ -187,6 +187,31 @@ class BasicsTest < ActiveRecord::TestCase end end + def test_previously_changed + topic = Topic.find :first + topic.title = '<3<3<3' + assert_equal({}, topic.previous_changes) + + topic.save! + expected = ["The First Topic", "<3<3<3"] + assert_equal(expected, topic.previous_changes['title']) + end + + def test_previously_changed_dup + topic = Topic.find :first + topic.title = '<3<3<3' + topic.save! + + t2 = topic.dup + + assert_equal(topic.previous_changes, t2.previous_changes) + + topic.title = "lolwut" + topic.save! + + assert_not_equal(topic.previous_changes, t2.previous_changes) + end + def test_preserving_time_objects assert_kind_of( Time, Topic.find(1).bonus_time, @@ -664,7 +689,7 @@ class BasicsTest < ActiveRecord::TestCase } topic = Topic.find(1) topic.attributes = attributes - assert_equal Time.local(2004, 6, 24, 16, 24, 0), topic.written_on + assert_equal Time.utc(2004, 6, 24, 16, 24, 0), topic.written_on end def test_multiparameter_attributes_on_time_with_no_date @@ -913,7 +938,7 @@ class BasicsTest < ActiveRecord::TestCase } topic = Topic.find(1) topic.attributes = attributes - assert_equal Time.local(2000, 1, 1, 5, 42, 0), topic.bonus_time + assert_equal Time.utc(2000, 1, 1, 5, 42, 0), topic.bonus_time end def test_boolean @@ -1868,7 +1893,7 @@ class BasicsTest < ActiveRecord::TestCase assert_equal [], NonExistentTable.attribute_names end - def test_attribtue_names_on_abstract_class + def test_attribute_names_on_abstract_class assert_equal [], AbstractCompany.attribute_names end diff --git a/activerecord/test/cases/locking_test.rb b/activerecord/test/cases/locking_test.rb index 0c458d5318..807274ca67 100644 --- a/activerecord/test/cases/locking_test.rb +++ b/activerecord/test/cases/locking_test.rb @@ -388,6 +388,26 @@ unless current_adapter?(:SybaseAdapter, :OpenBaseAdapter) || in_memory_db? end end + def test_with_lock_commits_transaction + person = Person.find 1 + person.with_lock do + person.first_name = 'fooman' + person.save! + end + assert_equal 'fooman', person.reload.first_name + end + + def test_with_lock_rolls_back_transaction + person = Person.find 1 + old = person.first_name + person.with_lock do + person.first_name = 'fooman' + person.save! + raise 'oops' + end rescue nil + assert_equal old, person.reload.first_name + end + if current_adapter?(:PostgreSQLAdapter, :OracleAdapter) def test_no_locks_no_wait first, second = duel { Person.find 1 } |