activerecord 3.1.12 → 3.2.22.1

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 (117) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +804 -338
  3. data/README.rdoc +3 -3
  4. data/examples/performance.rb +20 -1
  5. data/lib/active_record/aggregations.rb +1 -1
  6. data/lib/active_record/associations/alias_tracker.rb +3 -6
  7. data/lib/active_record/associations/association.rb +13 -45
  8. data/lib/active_record/associations/association_scope.rb +3 -15
  9. data/lib/active_record/associations/belongs_to_association.rb +1 -1
  10. data/lib/active_record/associations/belongs_to_polymorphic_association.rb +2 -1
  11. data/lib/active_record/associations/builder/association.rb +6 -4
  12. data/lib/active_record/associations/builder/belongs_to.rb +7 -4
  13. data/lib/active_record/associations/builder/collection_association.rb +2 -2
  14. data/lib/active_record/associations/builder/has_many.rb +4 -4
  15. data/lib/active_record/associations/builder/has_one.rb +5 -6
  16. data/lib/active_record/associations/builder/singular_association.rb +3 -16
  17. data/lib/active_record/associations/collection_association.rb +65 -32
  18. data/lib/active_record/associations/collection_proxy.rb +8 -41
  19. data/lib/active_record/associations/has_and_belongs_to_many_association.rb +1 -0
  20. data/lib/active_record/associations/has_many_association.rb +11 -7
  21. data/lib/active_record/associations/has_many_through_association.rb +19 -9
  22. data/lib/active_record/associations/has_one_association.rb +23 -13
  23. data/lib/active_record/associations/join_dependency/join_association.rb +6 -1
  24. data/lib/active_record/associations/join_dependency.rb +3 -3
  25. data/lib/active_record/associations/preloader/through_association.rb +3 -3
  26. data/lib/active_record/associations/preloader.rb +14 -10
  27. data/lib/active_record/associations/through_association.rb +8 -4
  28. data/lib/active_record/associations.rb +92 -76
  29. data/lib/active_record/attribute_assignment.rb +221 -0
  30. data/lib/active_record/attribute_methods/deprecated_underscore_read.rb +32 -0
  31. data/lib/active_record/attribute_methods/dirty.rb +21 -11
  32. data/lib/active_record/attribute_methods/primary_key.rb +62 -25
  33. data/lib/active_record/attribute_methods/read.rb +73 -83
  34. data/lib/active_record/attribute_methods/serialization.rb +120 -0
  35. data/lib/active_record/attribute_methods/time_zone_conversion.rb +12 -14
  36. data/lib/active_record/attribute_methods/write.rb +32 -6
  37. data/lib/active_record/attribute_methods.rb +231 -30
  38. data/lib/active_record/autosave_association.rb +44 -26
  39. data/lib/active_record/base.rb +227 -1708
  40. data/lib/active_record/connection_adapters/abstract/connection_pool.rb +150 -148
  41. data/lib/active_record/connection_adapters/abstract/connection_specification.rb +85 -29
  42. data/lib/active_record/connection_adapters/abstract/database_statements.rb +7 -34
  43. data/lib/active_record/connection_adapters/abstract/query_cache.rb +10 -2
  44. data/lib/active_record/connection_adapters/abstract/quoting.rb +7 -4
  45. data/lib/active_record/connection_adapters/abstract/schema_definitions.rb +39 -28
  46. data/lib/active_record/connection_adapters/abstract/schema_statements.rb +48 -19
  47. data/lib/active_record/connection_adapters/abstract_adapter.rb +77 -42
  48. data/lib/active_record/connection_adapters/abstract_mysql_adapter.rb +676 -0
  49. data/lib/active_record/connection_adapters/column.rb +37 -11
  50. data/lib/active_record/connection_adapters/mysql2_adapter.rb +133 -581
  51. data/lib/active_record/connection_adapters/mysql_adapter.rb +136 -693
  52. data/lib/active_record/connection_adapters/postgresql_adapter.rb +209 -97
  53. data/lib/active_record/connection_adapters/schema_cache.rb +69 -0
  54. data/lib/active_record/connection_adapters/sqlite3_adapter.rb +2 -6
  55. data/lib/active_record/connection_adapters/sqlite_adapter.rb +62 -35
  56. data/lib/active_record/counter_cache.rb +9 -4
  57. data/lib/active_record/dynamic_finder_match.rb +12 -0
  58. data/lib/active_record/dynamic_matchers.rb +84 -0
  59. data/lib/active_record/errors.rb +11 -1
  60. data/lib/active_record/explain.rb +86 -0
  61. data/lib/active_record/explain_subscriber.rb +25 -0
  62. data/lib/active_record/fixtures/file.rb +65 -0
  63. data/lib/active_record/fixtures.rb +57 -86
  64. data/lib/active_record/identity_map.rb +3 -4
  65. data/lib/active_record/inheritance.rb +174 -0
  66. data/lib/active_record/integration.rb +60 -0
  67. data/lib/active_record/locking/optimistic.rb +33 -26
  68. data/lib/active_record/locking/pessimistic.rb +23 -1
  69. data/lib/active_record/log_subscriber.rb +8 -4
  70. data/lib/active_record/migration/command_recorder.rb +8 -8
  71. data/lib/active_record/migration.rb +68 -35
  72. data/lib/active_record/model_schema.rb +368 -0
  73. data/lib/active_record/nested_attributes.rb +60 -24
  74. data/lib/active_record/persistence.rb +57 -11
  75. data/lib/active_record/query_cache.rb +6 -6
  76. data/lib/active_record/querying.rb +58 -0
  77. data/lib/active_record/railtie.rb +37 -29
  78. data/lib/active_record/railties/controller_runtime.rb +3 -1
  79. data/lib/active_record/railties/databases.rake +213 -117
  80. data/lib/active_record/railties/jdbcmysql_error.rb +1 -1
  81. data/lib/active_record/readonly_attributes.rb +26 -0
  82. data/lib/active_record/reflection.rb +7 -15
  83. data/lib/active_record/relation/batches.rb +7 -4
  84. data/lib/active_record/relation/calculations.rb +55 -16
  85. data/lib/active_record/relation/delegation.rb +49 -0
  86. data/lib/active_record/relation/finder_methods.rb +16 -11
  87. data/lib/active_record/relation/predicate_builder.rb +8 -6
  88. data/lib/active_record/relation/query_methods.rb +75 -9
  89. data/lib/active_record/relation/spawn_methods.rb +48 -7
  90. data/lib/active_record/relation.rb +78 -32
  91. data/lib/active_record/result.rb +10 -4
  92. data/lib/active_record/sanitization.rb +194 -0
  93. data/lib/active_record/schema_dumper.rb +12 -5
  94. data/lib/active_record/scoping/default.rb +142 -0
  95. data/lib/active_record/scoping/named.rb +200 -0
  96. data/lib/active_record/scoping.rb +152 -0
  97. data/lib/active_record/serialization.rb +1 -43
  98. data/lib/active_record/serializers/xml_serializer.rb +4 -45
  99. data/lib/active_record/session_store.rb +18 -16
  100. data/lib/active_record/store.rb +52 -0
  101. data/lib/active_record/test_case.rb +11 -7
  102. data/lib/active_record/timestamp.rb +17 -3
  103. data/lib/active_record/transactions.rb +27 -6
  104. data/lib/active_record/translation.rb +22 -0
  105. data/lib/active_record/validations/associated.rb +5 -4
  106. data/lib/active_record/validations/uniqueness.rb +8 -8
  107. data/lib/active_record/validations.rb +1 -1
  108. data/lib/active_record/version.rb +3 -3
  109. data/lib/active_record.rb +38 -3
  110. data/lib/rails/generators/active_record/migration/migration_generator.rb +1 -1
  111. data/lib/rails/generators/active_record/migration/templates/migration.rb +12 -3
  112. data/lib/rails/generators/active_record/model/model_generator.rb +9 -1
  113. data/lib/rails/generators/active_record/model/templates/migration.rb +3 -5
  114. data/lib/rails/generators/active_record/model/templates/model.rb +5 -0
  115. data/lib/rails/generators/active_record/session_migration/templates/migration.rb +1 -5
  116. metadata +49 -28
  117. data/lib/active_record/named_scope.rb +0 -200
@@ -6,14 +6,13 @@ rescue LoadError
6
6
  end
7
7
 
8
8
  require 'yaml'
9
- require 'csv'
10
9
  require 'zlib'
11
10
  require 'active_support/dependencies'
12
11
  require 'active_support/core_ext/array/wrap'
13
12
  require 'active_support/core_ext/object/blank'
14
13
  require 'active_support/core_ext/logger'
15
14
  require 'active_support/ordered_hash'
16
- require 'active_support/core_ext/module/deprecation'
15
+ require 'active_record/fixtures/file'
17
16
 
18
17
  if defined? ActiveRecord
19
18
  class FixtureClassNotFound < ActiveRecord::ActiveRecordError #:nodoc:
@@ -23,8 +22,6 @@ else
23
22
  end
24
23
  end
25
24
 
26
- class FixturesFileNotFound < StandardError; end
27
-
28
25
  module ActiveRecord
29
26
  # \Fixtures are a way of organizing data that you want to test against; in short, sample data.
30
27
  #
@@ -422,11 +419,15 @@ module ActiveRecord
422
419
  cache_for_connection(connection).update(fixtures_map)
423
420
  end
424
421
 
425
- def self.instantiate_fixtures(object, fixture_name, fixtures, load_instances = true)
422
+ #--
423
+ # TODO:NOTE: in the next version, the __with_new_arity suffix and
424
+ # the method with the old arity will be removed.
425
+ #++
426
+ def self.instantiate_fixtures__with_new_arity(object, fixture_set, load_instances = true) # :nodoc:
426
427
  if load_instances
427
- fixtures.each do |name, fixture|
428
+ fixture_set.each do |fixture_name, fixture|
428
429
  begin
429
- object.instance_variable_set "@#{name}", fixture.find
430
+ object.instance_variable_set "@#{fixture_name}", fixture.find
430
431
  rescue FixtureClassNotFound
431
432
  nil
432
433
  end
@@ -434,9 +435,24 @@ module ActiveRecord
434
435
  end
435
436
  end
436
437
 
438
+ # The use with parameters <tt>(object, fixture_set_name, fixture_set, load_instances = true)</tt> is deprecated, +fixture_set_name+ parameter is not used.
439
+ # Use as:
440
+ #
441
+ # instantiate_fixtures(object, fixture_set, load_instances = true)
442
+ def self.instantiate_fixtures(object, fixture_set, load_instances = true, rails_3_2_compatibility_argument = true)
443
+ unless load_instances == true || load_instances == false
444
+ ActiveSupport::Deprecation.warn(
445
+ "ActiveRecord::Fixtures.instantiate_fixtures with parameters (object, fixture_set_name, fixture_set, load_instances = true) is deprecated and shall be removed from future releases. Use it with parameters (object, fixture_set, load_instances = true) instead (skip fixture_set_name).",
446
+ caller)
447
+ fixture_set = load_instances
448
+ load_instances = rails_3_2_compatibility_argument
449
+ end
450
+ instantiate_fixtures__with_new_arity(object, fixture_set, load_instances)
451
+ end
452
+
437
453
  def self.instantiate_all_loaded_fixtures(object, load_instances = true)
438
- all_loaded_fixtures.each do |table_name, fixtures|
439
- ActiveRecord::Fixtures.instantiate_fixtures(object, table_name, fixtures, load_instances)
454
+ all_loaded_fixtures.each_value do |fixture_set|
455
+ ActiveRecord::Fixtures.instantiate_fixtures(object, fixture_set, load_instances)
440
456
  end
441
457
  end
442
458
 
@@ -467,7 +483,7 @@ module ActiveRecord
467
483
  connection,
468
484
  table_name,
469
485
  class_names[table_name.to_sym] || table_name.classify,
470
- File.join(fixtures_directory, path))
486
+ ::File.join(fixtures_directory, path))
471
487
  end
472
488
 
473
489
  all_loaded_fixtures.update(fixtures_map)
@@ -573,7 +589,7 @@ module ActiveRecord
573
589
 
574
590
  # interpolate the fixture label
575
591
  row.each do |key, value|
576
- row[key] = label if value == "$LABEL"
592
+ row[key] = label if value.is_a?(String) && value == "$LABEL"
577
593
  end
578
594
 
579
595
  # generate a primary key if necessary
@@ -645,81 +661,23 @@ module ActiveRecord
645
661
  end
646
662
 
647
663
  def read_fixture_files
648
- if File.file?(yaml_file_path)
649
- read_yaml_fixture_files
650
- elsif File.file?(csv_file_path)
651
- read_csv_fixture_files
652
- else
653
- raise FixturesFileNotFound, "Could not find #{yaml_file_path} or #{csv_file_path}"
654
- end
655
- end
656
-
657
- def read_yaml_fixture_files
658
- yaml_string = (Dir["#{@fixture_path}/**/*.yml"].select { |f|
659
- File.file?(f)
660
- } + [yaml_file_path]).map { |file_path| IO.read(file_path) }.join
661
-
662
- if yaml = parse_yaml_string(yaml_string)
663
- # If the file is an ordered map, extract its children.
664
- yaml_value =
665
- if yaml.respond_to?(:type_id) && yaml.respond_to?(:value)
666
- yaml.value
667
- else
668
- [yaml]
669
- end
670
-
671
- yaml_value.each do |fixture|
672
- raise Fixture::FormatError, "Bad data for #{@class_name} fixture named #{fixture}" unless fixture.respond_to?(:each)
673
- fixture.each do |name, data|
674
- unless data
675
- raise Fixture::FormatError, "Bad data for #{@class_name} fixture named #{name} (nil)"
676
- end
677
-
678
- fixtures[name] = ActiveRecord::Fixture.new(data, model_class)
664
+ yaml_files = Dir["#{@fixture_path}/{**,*}/*.yml"].select { |f|
665
+ ::File.file?(f)
666
+ } + [yaml_file_path]
667
+
668
+ yaml_files.each do |file|
669
+ Fixtures::File.open(file) do |fh|
670
+ fh.each do |name, row|
671
+ fixtures[name] = ActiveRecord::Fixture.new(row, model_class)
679
672
  end
680
673
  end
681
674
  end
682
675
  end
683
676
 
684
- def read_csv_fixture_files
685
- reader = CSV.parse(erb_render(IO.read(csv_file_path)))
686
- header = reader.shift
687
- i = 0
688
- reader.each do |row|
689
- data = {}
690
- row.each_with_index { |cell, j| data[header[j].to_s.strip] = cell.to_s.strip }
691
- fixtures["#{@class_name.to_s.underscore}_#{i+=1}"] = ActiveRecord::Fixture.new(data, model_class)
692
- end
693
- end
694
- deprecate :read_csv_fixture_files
695
-
696
677
  def yaml_file_path
697
678
  "#{@fixture_path}.yml"
698
679
  end
699
680
 
700
- def csv_file_path
701
- @fixture_path + ".csv"
702
- end
703
-
704
- def yaml_fixtures_key(path)
705
- File.basename(@fixture_path).split(".").first
706
- end
707
-
708
- RESCUE_ERRORS = [ ArgumentError ]
709
-
710
- if defined?(Psych) && defined?(Psych::SyntaxError)
711
- RESCUE_ERRORS << Psych::SyntaxError
712
- end
713
-
714
- def parse_yaml_string(fixture_content)
715
- YAML::load(erb_render(fixture_content))
716
- rescue *RESCUE_ERRORS => error
717
- raise Fixture::FormatError, "a YAML error occurred parsing #{yaml_file_path}. Please note that YAML must be consistently indented using spaces. Tabs are not allowed. Please have a look at http://www.yaml.org/faq.html\nThe exact error was:\n #{error.class}: #{error}", error.backtrace
718
- end
719
-
720
- def erb_render(fixture_content)
721
- ERB.new(fixture_content).result
722
- end
723
681
  end
724
682
 
725
683
  class Fixture #:nodoc:
@@ -794,7 +752,7 @@ module ActiveRecord
794
752
 
795
753
  def fixtures(*fixture_names)
796
754
  if fixture_names.first == :all
797
- fixture_names = Dir["#{fixture_path}/**/*.{yml,csv}"]
755
+ fixture_names = Dir["#{fixture_path}/{**,*}/*.{yml}"]
798
756
  fixture_names.map! { |f| f[(fixture_path.size + 1)..-5] }
799
757
  else
800
758
  fixture_names = fixture_names.flatten.map { |n| n.to_s }
@@ -880,6 +838,7 @@ module ActiveRecord
880
838
  end
881
839
 
882
840
  @fixture_cache = {}
841
+ @fixture_connections = []
883
842
  @@already_loaded_fixtures ||= {}
884
843
 
885
844
  # Load fixtures once and begin transaction.
@@ -890,9 +849,12 @@ module ActiveRecord
890
849
  @loaded_fixtures = load_fixtures
891
850
  @@already_loaded_fixtures[self.class] = @loaded_fixtures
892
851
  end
893
- ActiveRecord::Base.connection.increment_open_transactions
894
- ActiveRecord::Base.connection.transaction_joinable = false
895
- ActiveRecord::Base.connection.begin_db_transaction
852
+ @fixture_connections = enlist_fixture_connections
853
+ @fixture_connections.each do |connection|
854
+ connection.increment_open_transactions
855
+ connection.transaction_joinable = false
856
+ connection.begin_db_transaction
857
+ end
896
858
  # Load fixtures for every test.
897
859
  else
898
860
  ActiveRecord::Fixtures.reset_cache
@@ -912,13 +874,22 @@ module ActiveRecord
912
874
  end
913
875
 
914
876
  # Rollback changes if a transaction is active.
915
- if run_in_transaction? && ActiveRecord::Base.connection.open_transactions != 0
916
- ActiveRecord::Base.connection.rollback_db_transaction
917
- ActiveRecord::Base.connection.decrement_open_transactions
877
+ if run_in_transaction?
878
+ @fixture_connections.each do |connection|
879
+ if connection.open_transactions != 0
880
+ connection.rollback_db_transaction
881
+ connection.decrement_open_transactions
882
+ end
883
+ end
884
+ @fixture_connections.clear
918
885
  end
919
886
  ActiveRecord::Base.clear_active_connections!
920
887
  end
921
888
 
889
+ def enlist_fixture_connections
890
+ ActiveRecord::Base.connection_handler.connection_pools.values.map(&:connection)
891
+ end
892
+
922
893
  private
923
894
  def load_fixtures
924
895
  fixtures = ActiveRecord::Fixtures.create_fixtures(fixture_path, fixture_table_names, fixture_class_names)
@@ -938,8 +909,8 @@ module ActiveRecord
938
909
  ActiveRecord::Fixtures.instantiate_all_loaded_fixtures(self, load_instances?)
939
910
  else
940
911
  raise RuntimeError, 'Load fixtures before instantiating them.' if @loaded_fixtures.nil?
941
- @loaded_fixtures.each do |fixture_name, fixtures|
942
- ActiveRecord::Fixtures.instantiate_fixtures(self, fixture_name, fixtures, load_instances?)
912
+ @loaded_fixtures.each_value do |fixture_set|
913
+ ActiveRecord::Fixtures.instantiate_fixtures(self, fixture_set, load_instances?)
943
914
  end
944
915
  end
945
916
  end
@@ -117,13 +117,12 @@ module ActiveRecord
117
117
  # model object.
118
118
  def reinit_with(coder)
119
119
  @attributes_cache = {}
120
- dirty = @changed_attributes.keys
121
- @attributes.update(coder['attributes'].except(*dirty))
120
+ dirty = @changed_attributes.keys
121
+ attributes = self.class.initialize_attributes(coder['attributes'].except(*dirty))
122
+ @attributes.update(attributes)
122
123
  @changed_attributes.update(coder['attributes'].slice(*dirty))
123
124
  @changed_attributes.delete_if{|k,v| v.eql? @attributes[k]}
124
125
 
125
- set_serialized_attributes
126
-
127
126
  run_callbacks :find
128
127
 
129
128
  self
@@ -0,0 +1,174 @@
1
+ require 'active_support/concern'
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
10
+ self.store_full_sti_class = true
11
+ end
12
+
13
+ module ClassMethods
14
+ # True if this isn't a concrete subclass needing a STI type condition.
15
+ def descends_from_active_record?
16
+ if superclass.abstract_class?
17
+ superclass.descends_from_active_record?
18
+ else
19
+ superclass == Base || !columns_hash.include?(inheritance_column)
20
+ end
21
+ end
22
+
23
+ def finder_needs_type_condition? #:nodoc:
24
+ # This is like this because benchmarking justifies the strange :false stuff
25
+ :true == (@finder_needs_type_condition ||= descends_from_active_record? ? :false : :true)
26
+ end
27
+
28
+ def symbolized_base_class
29
+ @symbolized_base_class ||= base_class.to_s.to_sym
30
+ end
31
+
32
+ def symbolized_sti_name
33
+ @symbolized_sti_name ||= sti_name.present? ? sti_name.to_sym : symbolized_base_class
34
+ end
35
+
36
+ # Returns the base AR subclass that this class descends from. If A
37
+ # extends AR::Base, A.base_class will return A. If B descends from A
38
+ # through some arbitrarily deep hierarchy, B.base_class will return A.
39
+ #
40
+ # If B < A and C < B and if A is an abstract_class then both B.base_class
41
+ # and C.base_class would return B as the answer since A is an abstract_class.
42
+ def base_class
43
+ class_of_active_record_descendant(self)
44
+ end
45
+
46
+ # Set this to true if this is an abstract class (see <tt>abstract_class?</tt>).
47
+ attr_accessor :abstract_class
48
+
49
+ # Returns whether this class is an abstract class or not.
50
+ def abstract_class?
51
+ defined?(@abstract_class) && @abstract_class == true
52
+ end
53
+
54
+ def sti_name
55
+ store_full_sti_class ? name : name.demodulize
56
+ end
57
+
58
+ # Finder methods must instantiate through this method to work with the
59
+ # single-table inheritance model that makes it possible to create
60
+ # objects of different types from the same table.
61
+ def instantiate(record)
62
+ sti_class = find_sti_class(record[inheritance_column])
63
+ record_id = sti_class.primary_key && record[sti_class.primary_key]
64
+
65
+ if ActiveRecord::IdentityMap.enabled? && record_id
66
+ instance = use_identity_map(sti_class, record_id, record)
67
+ else
68
+ instance = sti_class.allocate.init_with('attributes' => record)
69
+ end
70
+
71
+ instance
72
+ end
73
+
74
+ protected
75
+
76
+ # Returns the class descending directly from ActiveRecord::Base or an
77
+ # abstract class, if any, in the inheritance hierarchy.
78
+ def class_of_active_record_descendant(klass)
79
+ if klass == Base || klass.superclass == Base || klass.superclass.abstract_class?
80
+ klass
81
+ elsif klass.superclass.nil?
82
+ raise ActiveRecordError, "#{name} doesn't belong in a hierarchy descending from ActiveRecord"
83
+ else
84
+ class_of_active_record_descendant(klass.superclass)
85
+ end
86
+ end
87
+
88
+ # Returns the class type of the record using the current module as a prefix. So descendants of
89
+ # MyApp::Business::Account would appear as MyApp::Business::AccountSubclass.
90
+ def compute_type(type_name)
91
+ if type_name.match(/^::/)
92
+ # If the type is prefixed with a scope operator then we assume that
93
+ # the type_name is an absolute reference.
94
+ ActiveSupport::Dependencies.constantize(type_name)
95
+ else
96
+ # Build a list of candidates to search for
97
+ candidates = []
98
+ name.scan(/::|$/) { candidates.unshift "#{$`}::#{type_name}" }
99
+ candidates << type_name
100
+
101
+ candidates.each do |candidate|
102
+ begin
103
+ constant = ActiveSupport::Dependencies.constantize(candidate)
104
+ return constant if candidate == constant.to_s
105
+ rescue NameError => e
106
+ # We don't want to swallow NoMethodError < NameError errors
107
+ raise e unless e.instance_of?(NameError)
108
+ end
109
+ end
110
+
111
+ raise NameError, "uninitialized constant #{candidates.first}"
112
+ end
113
+ end
114
+
115
+ private
116
+
117
+ def use_identity_map(sti_class, record_id, record)
118
+ if (column = sti_class.columns_hash[sti_class.primary_key]) && column.number?
119
+ record_id = record_id.to_i
120
+ end
121
+
122
+ if instance = IdentityMap.get(sti_class, record_id)
123
+ instance.reinit_with('attributes' => record)
124
+ else
125
+ instance = sti_class.allocate.init_with('attributes' => record)
126
+ IdentityMap.add(instance)
127
+ end
128
+
129
+ instance
130
+ end
131
+
132
+ def find_sti_class(type_name)
133
+ if type_name.blank? || !columns_hash.include?(inheritance_column)
134
+ self
135
+ else
136
+ begin
137
+ if store_full_sti_class
138
+ ActiveSupport::Dependencies.constantize(type_name)
139
+ else
140
+ compute_type(type_name)
141
+ end
142
+ rescue NameError
143
+ raise SubclassNotFound,
144
+ "The single-table inheritance mechanism failed to locate the subclass: '#{type_name}'. " +
145
+ "This error is raised because the column '#{inheritance_column}' is reserved for storing the class in case of inheritance. " +
146
+ "Please rename this column if you didn't intend it to be used for storing the inheritance class " +
147
+ "or overwrite #{name}.inheritance_column to use another column for that information."
148
+ end
149
+ end
150
+ end
151
+
152
+ def type_condition(table = arel_table)
153
+ sti_column = table[inheritance_column.to_sym]
154
+ sti_names = ([self] + descendants).map { |model| model.sti_name }
155
+
156
+ sti_column.in(sti_names)
157
+ end
158
+ end
159
+
160
+ private
161
+
162
+ # Sets the attribute used for single table inheritance to this class name if this is not the
163
+ # ActiveRecord::Base descendant.
164
+ # Considering the hierarchy Reply < Message < ActiveRecord::Base, this makes it possible to
165
+ # do Reply.new without having to set <tt>Reply[Reply.inheritance_column] = "Reply"</tt> yourself.
166
+ # No such attribute would be set for objects of the Message class in that example.
167
+ def ensure_proper_type
168
+ klass = self.class
169
+ if klass.finder_needs_type_condition?
170
+ write_attribute(klass.inheritance_column, klass.sti_name)
171
+ end
172
+ end
173
+ end
174
+ 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 format in the cache key.
9
+ # This is +:number+, by default.
10
+ class_attribute :cache_timestamp_format, :instance_writer => false
11
+ self.cache_timestamp_format = :number
12
+ end
13
+
14
+ # Returns a String, which Action Pack uses for constructing an URL to this
15
+ # object. The default implementation returns this record's id as a String,
16
+ # or nil if this record's unsaved.
17
+ #
18
+ # For example, suppose that you have a User model, and that you have a
19
+ # <tt>resources :users</tt> route. Normally, +user_path+ will
20
+ # construct a path with the user object's 'id' in it:
21
+ #
22
+ # user = User.find_by_name('Phusion')
23
+ # user_path(user) # => "/users/1"
24
+ #
25
+ # You can override +to_param+ in your model to make +user_path+ construct
26
+ # a path using the user's name instead of the user's id:
27
+ #
28
+ # class User < ActiveRecord::Base
29
+ # def to_param # overridden
30
+ # name
31
+ # end
32
+ # end
33
+ #
34
+ # user = User.find_by_name('Phusion')
35
+ # user_path(user) # => "/users/Phusion"
36
+ def to_param
37
+ # We can't use alias_method here, because method 'id' optimizes itself on the fly.
38
+ id && id.to_s # Be sure to stringify the id for routes
39
+ end
40
+
41
+ # Returns a cache key that can be used to identify this record.
42
+ #
43
+ # ==== Examples
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 = self[:updated_at]
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
@@ -37,6 +37,9 @@ module ActiveRecord
37
37
  # You're then responsible for dealing with the conflict by rescuing the exception and either rolling back, merging,
38
38
  # or otherwise apply the business logic needed to resolve the conflict.
39
39
  #
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.
42
+ #
40
43
  # You must ensure that your database schema defaults the +lock_version+ column to 0.
41
44
  #
42
45
  # This behavior can be turned off by setting <tt>ActiveRecord::Base.lock_optimistically = false</tt>.
@@ -48,10 +51,6 @@ module ActiveRecord
48
51
  included do
49
52
  cattr_accessor :lock_optimistically, :instance_writer => false
50
53
  self.lock_optimistically = true
51
-
52
- class << self
53
- alias_method :locking_column=, :set_locking_column
54
- end
55
54
  end
56
55
 
57
56
  def locking_enabled? #:nodoc:
@@ -65,21 +64,6 @@ module ActiveRecord
65
64
  send(lock_col + '=', previous_lock_value + 1)
66
65
  end
67
66
 
68
- def attributes_from_column_definition
69
- result = super
70
-
71
- # If the locking column has no default value set,
72
- # start the lock version at zero. Note we can't use
73
- # <tt>locking_enabled?</tt> at this point as
74
- # <tt>@attributes</tt> may not have been initialized yet.
75
-
76
- if result.key?(self.class.locking_column) && lock_optimistically
77
- result[self.class.locking_column] ||= 0
78
- end
79
-
80
- result
81
- end
82
-
83
67
  def update(attribute_names = @attributes.keys) #:nodoc:
84
68
  return super unless locking_enabled?
85
69
  return 0 if attribute_names.empty?
@@ -96,14 +80,14 @@ module ActiveRecord
96
80
 
97
81
  stmt = relation.where(
98
82
  relation.table[self.class.primary_key].eq(id).and(
99
- relation.table[lock_col].eq(quote_value(previous_lock_value))
83
+ relation.table[lock_col].eq(quote_value(previous_lock_value, self.class.columns_hash[lock_col]))
100
84
  )
101
85
  ).arel.compile_update(arel_attributes_values(false, false, attribute_names))
102
86
 
103
87
  affected_rows = connection.update stmt
104
88
 
105
89
  unless affected_rows == 1
106
- raise ActiveRecord::StaleObjectError, "Attempted to update a stale object: #{self.class.name}"
90
+ raise ActiveRecord::StaleObjectError.new(self, "update")
107
91
  end
108
92
 
109
93
  affected_rows
@@ -118,6 +102,8 @@ module ActiveRecord
118
102
  def destroy #:nodoc:
119
103
  return super unless locking_enabled?
120
104
 
105
+ destroy_associations
106
+
121
107
  if persisted?
122
108
  table = self.class.arel_table
123
109
  lock_col = self.class.locking_column
@@ -127,7 +113,7 @@ module ActiveRecord
127
113
  affected_rows = self.class.unscoped.where(predicate).delete_all
128
114
 
129
115
  unless affected_rows == 1
130
- raise ActiveRecord::StaleObjectError, "Attempted to delete a stale object: #{self.class.name}"
116
+ raise ActiveRecord::StaleObjectError.new(self, "destroy")
131
117
  end
132
118
  end
133
119
 
@@ -145,15 +131,24 @@ module ActiveRecord
145
131
  lock_optimistically && columns_hash[locking_column]
146
132
  end
147
133
 
134
+ def locking_column=(value)
135
+ @original_locking_column = @locking_column if defined?(@locking_column)
136
+ @locking_column = value.to_s
137
+ end
138
+
148
139
  # Set the column to use for optimistic locking. Defaults to +lock_version+.
149
140
  def set_locking_column(value = nil, &block)
150
- define_attr_method :locking_column, value, &block
151
- value
141
+ deprecated_property_setter :locking_column, value, block
152
142
  end
153
143
 
154
144
  # The version column used for optimistic locking. Defaults to +lock_version+.
155
145
  def locking_column
156
- reset_locking_column
146
+ reset_locking_column unless defined?(@locking_column)
147
+ @locking_column
148
+ end
149
+
150
+ def original_locking_column #:nodoc:
151
+ deprecated_original_property_getter :locking_column
157
152
  end
158
153
 
159
154
  # Quote the column name used for optimistic locking.
@@ -163,7 +158,7 @@ module ActiveRecord
163
158
 
164
159
  # Reset the column used for optimistic locking back to the +lock_version+ default.
165
160
  def reset_locking_column
166
- set_locking_column DEFAULT_LOCKING_COLUMN
161
+ self.locking_column = DEFAULT_LOCKING_COLUMN
167
162
  end
168
163
 
169
164
  # Make sure the lock version column gets updated when counters are
@@ -172,6 +167,18 @@ module ActiveRecord
172
167
  counters = counters.merge(locking_column => 1) if locking_enabled?
173
168
  super
174
169
  end
170
+
171
+ # If the locking column has no default value set,
172
+ # start the lock version at zero. Note we can't use
173
+ # <tt>locking_enabled?</tt> at this point as
174
+ # <tt>@attributes</tt> may not have been initialized yet.
175
+ def initialize_attributes(attributes, options = {}) #:nodoc:
176
+ if attributes.key?(locking_column) && lock_optimistically
177
+ attributes[locking_column] ||= 0
178
+ end
179
+
180
+ attributes
181
+ end
175
182
  end
176
183
  end
177
184
  end
@@ -14,7 +14,7 @@ module ActiveRecord
14
14
  # Account.transaction do
15
15
  # # select * from accounts where name = 'shugo' limit 1 for update
16
16
  # shugo = Account.where("name = 'shugo'").lock(true).first
17
- # yuko = Account.where("name = 'shugo'").lock(true).first
17
+ # yuko = Account.where("name = 'yuko'").lock(true).first
18
18
  # shugo.balance -= 100
19
19
  # shugo.save!
20
20
  # yuko.balance += 100
@@ -38,6 +38,18 @@ 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
55
  # PostgreSQL: http://www.postgresql.org/docs/current/interactive/sql-select.html#SQL-FOR-UPDATE-SHARE
@@ -50,6 +62,16 @@ module ActiveRecord
50
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