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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +804 -338
- data/README.rdoc +3 -3
- data/examples/performance.rb +20 -1
- data/lib/active_record/aggregations.rb +1 -1
- data/lib/active_record/associations/alias_tracker.rb +3 -6
- data/lib/active_record/associations/association.rb +13 -45
- data/lib/active_record/associations/association_scope.rb +3 -15
- data/lib/active_record/associations/belongs_to_association.rb +1 -1
- data/lib/active_record/associations/belongs_to_polymorphic_association.rb +2 -1
- data/lib/active_record/associations/builder/association.rb +6 -4
- data/lib/active_record/associations/builder/belongs_to.rb +7 -4
- data/lib/active_record/associations/builder/collection_association.rb +2 -2
- data/lib/active_record/associations/builder/has_many.rb +4 -4
- data/lib/active_record/associations/builder/has_one.rb +5 -6
- data/lib/active_record/associations/builder/singular_association.rb +3 -16
- data/lib/active_record/associations/collection_association.rb +65 -32
- data/lib/active_record/associations/collection_proxy.rb +8 -41
- data/lib/active_record/associations/has_and_belongs_to_many_association.rb +1 -0
- data/lib/active_record/associations/has_many_association.rb +11 -7
- data/lib/active_record/associations/has_many_through_association.rb +19 -9
- data/lib/active_record/associations/has_one_association.rb +23 -13
- data/lib/active_record/associations/join_dependency/join_association.rb +6 -1
- data/lib/active_record/associations/join_dependency.rb +3 -3
- data/lib/active_record/associations/preloader/through_association.rb +3 -3
- data/lib/active_record/associations/preloader.rb +14 -10
- data/lib/active_record/associations/through_association.rb +8 -4
- data/lib/active_record/associations.rb +92 -76
- data/lib/active_record/attribute_assignment.rb +221 -0
- data/lib/active_record/attribute_methods/deprecated_underscore_read.rb +32 -0
- data/lib/active_record/attribute_methods/dirty.rb +21 -11
- data/lib/active_record/attribute_methods/primary_key.rb +62 -25
- data/lib/active_record/attribute_methods/read.rb +73 -83
- data/lib/active_record/attribute_methods/serialization.rb +120 -0
- data/lib/active_record/attribute_methods/time_zone_conversion.rb +12 -14
- data/lib/active_record/attribute_methods/write.rb +32 -6
- data/lib/active_record/attribute_methods.rb +231 -30
- data/lib/active_record/autosave_association.rb +44 -26
- data/lib/active_record/base.rb +227 -1708
- data/lib/active_record/connection_adapters/abstract/connection_pool.rb +150 -148
- data/lib/active_record/connection_adapters/abstract/connection_specification.rb +85 -29
- data/lib/active_record/connection_adapters/abstract/database_statements.rb +7 -34
- data/lib/active_record/connection_adapters/abstract/query_cache.rb +10 -2
- data/lib/active_record/connection_adapters/abstract/quoting.rb +7 -4
- data/lib/active_record/connection_adapters/abstract/schema_definitions.rb +39 -28
- data/lib/active_record/connection_adapters/abstract/schema_statements.rb +48 -19
- data/lib/active_record/connection_adapters/abstract_adapter.rb +77 -42
- data/lib/active_record/connection_adapters/abstract_mysql_adapter.rb +676 -0
- data/lib/active_record/connection_adapters/column.rb +37 -11
- data/lib/active_record/connection_adapters/mysql2_adapter.rb +133 -581
- data/lib/active_record/connection_adapters/mysql_adapter.rb +136 -693
- data/lib/active_record/connection_adapters/postgresql_adapter.rb +209 -97
- data/lib/active_record/connection_adapters/schema_cache.rb +69 -0
- data/lib/active_record/connection_adapters/sqlite3_adapter.rb +2 -6
- data/lib/active_record/connection_adapters/sqlite_adapter.rb +62 -35
- data/lib/active_record/counter_cache.rb +9 -4
- data/lib/active_record/dynamic_finder_match.rb +12 -0
- data/lib/active_record/dynamic_matchers.rb +84 -0
- data/lib/active_record/errors.rb +11 -1
- data/lib/active_record/explain.rb +86 -0
- data/lib/active_record/explain_subscriber.rb +25 -0
- data/lib/active_record/fixtures/file.rb +65 -0
- data/lib/active_record/fixtures.rb +57 -86
- data/lib/active_record/identity_map.rb +3 -4
- data/lib/active_record/inheritance.rb +174 -0
- data/lib/active_record/integration.rb +60 -0
- data/lib/active_record/locking/optimistic.rb +33 -26
- data/lib/active_record/locking/pessimistic.rb +23 -1
- data/lib/active_record/log_subscriber.rb +8 -4
- data/lib/active_record/migration/command_recorder.rb +8 -8
- data/lib/active_record/migration.rb +68 -35
- data/lib/active_record/model_schema.rb +368 -0
- data/lib/active_record/nested_attributes.rb +60 -24
- data/lib/active_record/persistence.rb +57 -11
- data/lib/active_record/query_cache.rb +6 -6
- data/lib/active_record/querying.rb +58 -0
- data/lib/active_record/railtie.rb +37 -29
- data/lib/active_record/railties/controller_runtime.rb +3 -1
- data/lib/active_record/railties/databases.rake +213 -117
- data/lib/active_record/railties/jdbcmysql_error.rb +1 -1
- data/lib/active_record/readonly_attributes.rb +26 -0
- data/lib/active_record/reflection.rb +7 -15
- data/lib/active_record/relation/batches.rb +7 -4
- data/lib/active_record/relation/calculations.rb +55 -16
- data/lib/active_record/relation/delegation.rb +49 -0
- data/lib/active_record/relation/finder_methods.rb +16 -11
- data/lib/active_record/relation/predicate_builder.rb +8 -6
- data/lib/active_record/relation/query_methods.rb +75 -9
- data/lib/active_record/relation/spawn_methods.rb +48 -7
- data/lib/active_record/relation.rb +78 -32
- data/lib/active_record/result.rb +10 -4
- data/lib/active_record/sanitization.rb +194 -0
- data/lib/active_record/schema_dumper.rb +12 -5
- data/lib/active_record/scoping/default.rb +142 -0
- data/lib/active_record/scoping/named.rb +200 -0
- data/lib/active_record/scoping.rb +152 -0
- data/lib/active_record/serialization.rb +1 -43
- data/lib/active_record/serializers/xml_serializer.rb +4 -45
- data/lib/active_record/session_store.rb +18 -16
- data/lib/active_record/store.rb +52 -0
- data/lib/active_record/test_case.rb +11 -7
- data/lib/active_record/timestamp.rb +17 -3
- data/lib/active_record/transactions.rb +27 -6
- data/lib/active_record/translation.rb +22 -0
- data/lib/active_record/validations/associated.rb +5 -4
- data/lib/active_record/validations/uniqueness.rb +8 -8
- data/lib/active_record/validations.rb +1 -1
- data/lib/active_record/version.rb +3 -3
- data/lib/active_record.rb +38 -3
- data/lib/rails/generators/active_record/migration/migration_generator.rb +1 -1
- data/lib/rails/generators/active_record/migration/templates/migration.rb +12 -3
- data/lib/rails/generators/active_record/model/model_generator.rb +9 -1
- data/lib/rails/generators/active_record/model/templates/migration.rb +3 -5
- data/lib/rails/generators/active_record/model/templates/model.rb +5 -0
- data/lib/rails/generators/active_record/session_migration/templates/migration.rb +1 -5
- metadata +49 -28
- 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 '
|
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
|
-
|
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
|
-
|
428
|
+
fixture_set.each do |fixture_name, fixture|
|
428
429
|
begin
|
429
|
-
object.instance_variable_set "@#{
|
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.
|
439
|
-
ActiveRecord::Fixtures.instantiate_fixtures(object,
|
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
|
-
|
649
|
-
|
650
|
-
|
651
|
-
|
652
|
-
|
653
|
-
|
654
|
-
|
655
|
-
|
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}
|
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
|
-
|
894
|
-
|
895
|
-
|
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?
|
916
|
-
|
917
|
-
|
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.
|
942
|
-
ActiveRecord::Fixtures.instantiate_fixtures(self,
|
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
|
121
|
-
|
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, "
|
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, "
|
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
|
-
|
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
|
-
|
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 = '
|
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
|