activerecord 6.1.7 → 7.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +616 -1290
- data/MIT-LICENSE +1 -1
- data/README.rdoc +31 -31
- data/examples/performance.rb +2 -2
- data/lib/active_record/aggregations.rb +17 -14
- data/lib/active_record/association_relation.rb +2 -12
- data/lib/active_record/associations/alias_tracker.rb +25 -19
- data/lib/active_record/associations/association.rb +60 -21
- data/lib/active_record/associations/association_scope.rb +17 -12
- data/lib/active_record/associations/belongs_to_association.rb +37 -11
- data/lib/active_record/associations/belongs_to_polymorphic_association.rb +13 -4
- data/lib/active_record/associations/builder/association.rb +11 -5
- data/lib/active_record/associations/builder/belongs_to.rb +41 -14
- data/lib/active_record/associations/builder/collection_association.rb +10 -3
- data/lib/active_record/associations/builder/has_and_belongs_to_many.rb +3 -7
- data/lib/active_record/associations/builder/has_many.rb +4 -4
- data/lib/active_record/associations/builder/has_one.rb +4 -4
- data/lib/active_record/associations/builder/singular_association.rb +6 -2
- data/lib/active_record/associations/collection_association.rb +46 -36
- data/lib/active_record/associations/collection_proxy.rb +44 -16
- data/lib/active_record/associations/disable_joins_association_scope.rb +59 -0
- data/lib/active_record/associations/errors.rb +265 -0
- data/lib/active_record/associations/foreign_association.rb +10 -3
- data/lib/active_record/associations/has_many_association.rb +29 -19
- data/lib/active_record/associations/has_many_through_association.rb +19 -8
- data/lib/active_record/associations/has_one_association.rb +20 -10
- data/lib/active_record/associations/has_one_through_association.rb +1 -1
- data/lib/active_record/associations/join_dependency/join_association.rb +30 -27
- data/lib/active_record/associations/join_dependency.rb +28 -20
- data/lib/active_record/associations/nested_error.rb +47 -0
- data/lib/active_record/associations/preloader/association.rb +212 -53
- data/lib/active_record/associations/preloader/batch.rb +48 -0
- data/lib/active_record/associations/preloader/branch.rb +153 -0
- data/lib/active_record/associations/preloader/through_association.rb +50 -16
- data/lib/active_record/associations/preloader.rb +50 -121
- data/lib/active_record/associations/singular_association.rb +15 -3
- data/lib/active_record/associations/through_association.rb +25 -14
- data/lib/active_record/associations.rb +429 -522
- data/lib/active_record/asynchronous_queries_tracker.rb +60 -0
- data/lib/active_record/attribute_assignment.rb +1 -5
- data/lib/active_record/attribute_methods/before_type_cast.rb +24 -2
- data/lib/active_record/attribute_methods/composite_primary_key.rb +84 -0
- data/lib/active_record/attribute_methods/dirty.rb +73 -22
- data/lib/active_record/attribute_methods/primary_key.rb +47 -27
- data/lib/active_record/attribute_methods/query.rb +31 -19
- data/lib/active_record/attribute_methods/read.rb +14 -11
- data/lib/active_record/attribute_methods/serialization.rb +174 -37
- data/lib/active_record/attribute_methods/time_zone_conversion.rb +15 -9
- data/lib/active_record/attribute_methods/write.rb +12 -15
- data/lib/active_record/attribute_methods.rb +164 -52
- data/lib/active_record/attributes.rb +57 -54
- data/lib/active_record/autosave_association.rb +74 -57
- data/lib/active_record/base.rb +27 -5
- data/lib/active_record/callbacks.rb +19 -35
- data/lib/active_record/coders/column_serializer.rb +61 -0
- data/lib/active_record/coders/json.rb +1 -1
- data/lib/active_record/coders/yaml_column.rb +70 -46
- data/lib/active_record/connection_adapters/abstract/connection_handler.rb +284 -0
- data/lib/active_record/connection_adapters/abstract/connection_pool/queue.rb +211 -0
- data/lib/active_record/connection_adapters/abstract/connection_pool/reaper.rb +79 -0
- data/lib/active_record/connection_adapters/abstract/connection_pool.rb +325 -604
- data/lib/active_record/connection_adapters/abstract/database_limits.rb +5 -17
- data/lib/active_record/connection_adapters/abstract/database_statements.rb +199 -60
- data/lib/active_record/connection_adapters/abstract/query_cache.rb +230 -64
- data/lib/active_record/connection_adapters/abstract/quoting.rb +119 -131
- data/lib/active_record/connection_adapters/abstract/savepoints.rb +4 -3
- data/lib/active_record/connection_adapters/abstract/schema_creation.rb +21 -20
- data/lib/active_record/connection_adapters/abstract/schema_definitions.rb +186 -31
- data/lib/active_record/connection_adapters/abstract/schema_dumper.rb +14 -1
- data/lib/active_record/connection_adapters/abstract/schema_statements.rb +378 -143
- data/lib/active_record/connection_adapters/abstract/transaction.rb +361 -76
- data/lib/active_record/connection_adapters/abstract_adapter.rb +624 -163
- data/lib/active_record/connection_adapters/abstract_mysql_adapter.rb +348 -165
- data/lib/active_record/connection_adapters/column.rb +13 -0
- data/lib/active_record/connection_adapters/mysql/column.rb +1 -0
- data/lib/active_record/connection_adapters/mysql/database_statements.rb +29 -130
- data/lib/active_record/connection_adapters/mysql/quoting.rb +81 -55
- data/lib/active_record/connection_adapters/mysql/schema_creation.rb +9 -0
- data/lib/active_record/connection_adapters/mysql/schema_definitions.rb +10 -1
- data/lib/active_record/connection_adapters/mysql/schema_dumper.rb +8 -2
- data/lib/active_record/connection_adapters/mysql/schema_statements.rb +45 -14
- data/lib/active_record/connection_adapters/mysql2/database_statements.rb +152 -0
- data/lib/active_record/connection_adapters/mysql2_adapter.rb +107 -68
- data/lib/active_record/connection_adapters/pool_config.rb +26 -16
- data/lib/active_record/connection_adapters/pool_manager.rb +19 -9
- data/lib/active_record/connection_adapters/postgresql/column.rb +30 -1
- data/lib/active_record/connection_adapters/postgresql/database_statements.rb +114 -54
- data/lib/active_record/connection_adapters/postgresql/oid/array.rb +1 -1
- data/lib/active_record/connection_adapters/postgresql/oid/cidr.rb +6 -0
- data/lib/active_record/connection_adapters/postgresql/oid/date.rb +8 -0
- data/lib/active_record/connection_adapters/postgresql/oid/date_time.rb +5 -0
- data/lib/active_record/connection_adapters/postgresql/oid/hstore.rb +53 -14
- data/lib/active_record/connection_adapters/postgresql/oid/interval.rb +1 -1
- data/lib/active_record/connection_adapters/postgresql/oid/money.rb +3 -2
- data/lib/active_record/connection_adapters/postgresql/oid/range.rb +12 -3
- data/lib/active_record/connection_adapters/postgresql/oid/timestamp.rb +15 -0
- data/lib/active_record/connection_adapters/postgresql/oid/timestamp_with_time_zone.rb +30 -0
- data/lib/active_record/connection_adapters/postgresql/oid/type_map_initializer.rb +18 -6
- data/lib/active_record/connection_adapters/postgresql/oid/uuid.rb +14 -4
- data/lib/active_record/connection_adapters/postgresql/oid.rb +2 -0
- data/lib/active_record/connection_adapters/postgresql/quoting.rb +137 -104
- data/lib/active_record/connection_adapters/postgresql/referential_integrity.rb +28 -0
- data/lib/active_record/connection_adapters/postgresql/schema_creation.rb +92 -2
- data/lib/active_record/connection_adapters/postgresql/schema_definitions.rb +173 -3
- data/lib/active_record/connection_adapters/postgresql/schema_dumper.rb +78 -0
- data/lib/active_record/connection_adapters/postgresql/schema_statements.rb +403 -77
- data/lib/active_record/connection_adapters/postgresql/utils.rb +9 -10
- data/lib/active_record/connection_adapters/postgresql_adapter.rb +520 -253
- data/lib/active_record/connection_adapters/schema_cache.rb +326 -102
- data/lib/active_record/connection_adapters/sqlite3/column.rb +62 -0
- data/lib/active_record/connection_adapters/sqlite3/database_statements.rb +78 -55
- data/lib/active_record/connection_adapters/sqlite3/quoting.rb +68 -54
- data/lib/active_record/connection_adapters/sqlite3/schema_creation.rb +22 -0
- data/lib/active_record/connection_adapters/sqlite3/schema_definitions.rb +20 -0
- data/lib/active_record/connection_adapters/sqlite3/schema_dumper.rb +16 -0
- data/lib/active_record/connection_adapters/sqlite3/schema_statements.rb +66 -22
- data/lib/active_record/connection_adapters/sqlite3_adapter.rb +372 -130
- data/lib/active_record/connection_adapters/statement_pool.rb +7 -0
- data/lib/active_record/connection_adapters/trilogy/database_statements.rb +99 -0
- data/lib/active_record/connection_adapters/trilogy_adapter.rb +229 -0
- data/lib/active_record/connection_adapters.rb +130 -6
- data/lib/active_record/connection_handling.rb +132 -146
- data/lib/active_record/core.rb +310 -253
- data/lib/active_record/counter_cache.rb +68 -34
- data/lib/active_record/database_configurations/connection_url_resolver.rb +10 -4
- data/lib/active_record/database_configurations/database_config.rb +34 -10
- data/lib/active_record/database_configurations/hash_config.rb +107 -31
- data/lib/active_record/database_configurations/url_config.rb +38 -13
- data/lib/active_record/database_configurations.rb +96 -60
- data/lib/active_record/delegated_type.rb +90 -20
- data/lib/active_record/deprecator.rb +7 -0
- data/lib/active_record/destroy_association_async_job.rb +4 -2
- data/lib/active_record/disable_joins_association_relation.rb +39 -0
- data/lib/active_record/dynamic_matchers.rb +3 -3
- data/lib/active_record/encryption/auto_filtered_parameters.rb +66 -0
- data/lib/active_record/encryption/cipher/aes256_gcm.rb +101 -0
- data/lib/active_record/encryption/cipher.rb +53 -0
- data/lib/active_record/encryption/config.rb +68 -0
- data/lib/active_record/encryption/configurable.rb +60 -0
- data/lib/active_record/encryption/context.rb +42 -0
- data/lib/active_record/encryption/contexts.rb +76 -0
- data/lib/active_record/encryption/derived_secret_key_provider.rb +18 -0
- data/lib/active_record/encryption/deterministic_key_provider.rb +14 -0
- data/lib/active_record/encryption/encryptable_record.rb +230 -0
- data/lib/active_record/encryption/encrypted_attribute_type.rb +175 -0
- data/lib/active_record/encryption/encrypted_fixtures.rb +38 -0
- data/lib/active_record/encryption/encrypting_only_encryptor.rb +12 -0
- data/lib/active_record/encryption/encryptor.rb +170 -0
- data/lib/active_record/encryption/envelope_encryption_key_provider.rb +55 -0
- data/lib/active_record/encryption/errors.rb +15 -0
- data/lib/active_record/encryption/extended_deterministic_queries.rb +157 -0
- data/lib/active_record/encryption/extended_deterministic_uniqueness_validator.rb +28 -0
- data/lib/active_record/encryption/key.rb +28 -0
- data/lib/active_record/encryption/key_generator.rb +53 -0
- data/lib/active_record/encryption/key_provider.rb +46 -0
- data/lib/active_record/encryption/message.rb +33 -0
- data/lib/active_record/encryption/message_pack_message_serializer.rb +76 -0
- data/lib/active_record/encryption/message_serializer.rb +96 -0
- data/lib/active_record/encryption/null_encryptor.rb +25 -0
- data/lib/active_record/encryption/properties.rb +76 -0
- data/lib/active_record/encryption/read_only_null_encryptor.rb +28 -0
- data/lib/active_record/encryption/scheme.rb +100 -0
- data/lib/active_record/encryption.rb +58 -0
- data/lib/active_record/enum.rb +170 -62
- data/lib/active_record/errors.rb +210 -27
- data/lib/active_record/explain.rb +21 -12
- data/lib/active_record/explain_registry.rb +11 -6
- data/lib/active_record/explain_subscriber.rb +1 -1
- data/lib/active_record/fixture_set/file.rb +15 -1
- data/lib/active_record/fixture_set/model_metadata.rb +14 -4
- data/lib/active_record/fixture_set/render_context.rb +2 -0
- data/lib/active_record/fixture_set/table_row.rb +70 -14
- data/lib/active_record/fixture_set/table_rows.rb +4 -4
- data/lib/active_record/fixtures.rb +179 -112
- data/lib/active_record/future_result.rb +178 -0
- data/lib/active_record/gem_version.rb +4 -4
- data/lib/active_record/inheritance.rb +85 -31
- data/lib/active_record/insert_all.rb +148 -32
- data/lib/active_record/integration.rb +14 -10
- data/lib/active_record/internal_metadata.rb +123 -23
- data/lib/active_record/legacy_yaml_adapter.rb +2 -39
- data/lib/active_record/locking/optimistic.rb +43 -27
- data/lib/active_record/locking/pessimistic.rb +15 -6
- data/lib/active_record/log_subscriber.rb +41 -29
- data/lib/active_record/marshalling.rb +59 -0
- data/lib/active_record/message_pack.rb +124 -0
- data/lib/active_record/middleware/database_selector/resolver.rb +10 -10
- data/lib/active_record/middleware/database_selector.rb +23 -13
- data/lib/active_record/middleware/shard_selector.rb +62 -0
- data/lib/active_record/migration/command_recorder.rb +113 -16
- data/lib/active_record/migration/compatibility.rb +235 -46
- data/lib/active_record/migration/default_strategy.rb +22 -0
- data/lib/active_record/migration/execution_strategy.rb +19 -0
- data/lib/active_record/migration/join_table.rb +1 -1
- data/lib/active_record/migration/pending_migration_connection.rb +21 -0
- data/lib/active_record/migration.rb +374 -177
- data/lib/active_record/model_schema.rb +145 -158
- data/lib/active_record/nested_attributes.rb +61 -23
- data/lib/active_record/no_touching.rb +3 -3
- data/lib/active_record/normalization.rb +163 -0
- data/lib/active_record/persistence.rb +282 -283
- data/lib/active_record/promise.rb +84 -0
- data/lib/active_record/query_cache.rb +18 -25
- data/lib/active_record/query_logs.rb +189 -0
- data/lib/active_record/query_logs_formatter.rb +41 -0
- data/lib/active_record/querying.rb +44 -9
- data/lib/active_record/railtie.rb +229 -71
- data/lib/active_record/railties/controller_runtime.rb +25 -11
- data/lib/active_record/railties/databases.rake +189 -256
- data/lib/active_record/railties/job_runtime.rb +23 -0
- data/lib/active_record/readonly_attributes.rb +41 -3
- data/lib/active_record/reflection.rb +332 -103
- data/lib/active_record/relation/batches/batch_enumerator.rb +38 -9
- data/lib/active_record/relation/batches.rb +200 -65
- data/lib/active_record/relation/calculations.rb +301 -112
- data/lib/active_record/relation/delegation.rb +33 -22
- data/lib/active_record/relation/finder_methods.rb +123 -52
- data/lib/active_record/relation/merger.rb +26 -19
- data/lib/active_record/relation/predicate_builder/array_handler.rb +2 -2
- data/lib/active_record/relation/predicate_builder/association_query_value.rb +38 -4
- data/lib/active_record/relation/predicate_builder/polymorphic_array_value.rb +10 -7
- data/lib/active_record/relation/predicate_builder/relation_handler.rb +5 -1
- data/lib/active_record/relation/predicate_builder.rb +29 -22
- data/lib/active_record/relation/query_attribute.rb +30 -12
- data/lib/active_record/relation/query_methods.rb +870 -163
- data/lib/active_record/relation/record_fetch_warning.rb +10 -9
- data/lib/active_record/relation/spawn_methods.rb +7 -6
- data/lib/active_record/relation/where_clause.rb +15 -36
- data/lib/active_record/relation.rb +736 -145
- data/lib/active_record/result.rb +67 -54
- data/lib/active_record/runtime_registry.rb +71 -13
- data/lib/active_record/sanitization.rb +84 -34
- data/lib/active_record/schema.rb +39 -23
- data/lib/active_record/schema_dumper.rb +90 -31
- data/lib/active_record/schema_migration.rb +74 -23
- data/lib/active_record/scoping/default.rb +72 -15
- data/lib/active_record/scoping/named.rb +6 -13
- data/lib/active_record/scoping.rb +65 -34
- data/lib/active_record/secure_password.rb +60 -0
- data/lib/active_record/secure_token.rb +21 -3
- data/lib/active_record/serialization.rb +6 -1
- data/lib/active_record/signed_id.rb +30 -9
- data/lib/active_record/statement_cache.rb +7 -7
- data/lib/active_record/store.rb +10 -10
- data/lib/active_record/suppressor.rb +13 -15
- data/lib/active_record/table_metadata.rb +7 -3
- data/lib/active_record/tasks/database_tasks.rb +288 -149
- data/lib/active_record/tasks/mysql_database_tasks.rb +16 -7
- data/lib/active_record/tasks/postgresql_database_tasks.rb +35 -26
- data/lib/active_record/tasks/sqlite_database_tasks.rb +16 -7
- data/lib/active_record/test_databases.rb +1 -1
- data/lib/active_record/test_fixtures.rb +173 -155
- data/lib/active_record/testing/query_assertions.rb +121 -0
- data/lib/active_record/timestamp.rb +32 -19
- data/lib/active_record/token_for.rb +123 -0
- data/lib/active_record/touch_later.rb +12 -7
- data/lib/active_record/transaction.rb +132 -0
- data/lib/active_record/transactions.rb +118 -41
- data/lib/active_record/translation.rb +3 -5
- data/lib/active_record/type/adapter_specific_registry.rb +32 -14
- data/lib/active_record/type/hash_lookup_type_map.rb +34 -1
- data/lib/active_record/type/internal/timezone.rb +7 -2
- data/lib/active_record/type/serialized.rb +9 -7
- data/lib/active_record/type/time.rb +4 -0
- data/lib/active_record/type/type_map.rb +17 -20
- data/lib/active_record/type.rb +1 -2
- data/lib/active_record/type_caster/connection.rb +4 -4
- data/lib/active_record/validations/absence.rb +1 -1
- data/lib/active_record/validations/associated.rb +13 -7
- data/lib/active_record/validations/numericality.rb +5 -4
- data/lib/active_record/validations/presence.rb +5 -28
- data/lib/active_record/validations/uniqueness.rb +65 -15
- data/lib/active_record/validations.rb +12 -5
- data/lib/active_record/version.rb +1 -1
- data/lib/active_record.rb +444 -32
- data/lib/arel/alias_predication.rb +1 -1
- data/lib/arel/attributes/attribute.rb +0 -8
- data/lib/arel/collectors/bind.rb +2 -0
- data/lib/arel/collectors/composite.rb +7 -0
- data/lib/arel/collectors/sql_string.rb +1 -1
- data/lib/arel/collectors/substitute_binds.rb +1 -1
- data/lib/arel/crud.rb +28 -22
- data/lib/arel/delete_manager.rb +18 -4
- data/lib/arel/errors.rb +10 -0
- data/lib/arel/factory_methods.rb +4 -0
- data/lib/arel/filter_predications.rb +9 -0
- data/lib/arel/insert_manager.rb +2 -3
- data/lib/arel/nodes/binary.rb +6 -7
- data/lib/arel/nodes/bound_sql_literal.rb +65 -0
- data/lib/arel/nodes/casted.rb +1 -1
- data/lib/arel/nodes/cte.rb +36 -0
- data/lib/arel/nodes/delete_statement.rb +12 -13
- data/lib/arel/nodes/filter.rb +10 -0
- data/lib/arel/nodes/fragments.rb +35 -0
- data/lib/arel/nodes/function.rb +1 -0
- data/lib/arel/nodes/homogeneous_in.rb +1 -9
- data/lib/arel/nodes/insert_statement.rb +2 -2
- data/lib/arel/nodes/leading_join.rb +8 -0
- data/lib/arel/nodes/{and.rb → nary.rb} +9 -2
- data/lib/arel/nodes/node.rb +115 -5
- data/lib/arel/nodes/select_core.rb +2 -2
- data/lib/arel/nodes/select_statement.rb +2 -2
- data/lib/arel/nodes/sql_literal.rb +13 -0
- data/lib/arel/nodes/table_alias.rb +4 -0
- data/lib/arel/nodes/update_statement.rb +8 -3
- data/lib/arel/nodes.rb +7 -2
- data/lib/arel/predications.rb +14 -4
- data/lib/arel/select_manager.rb +11 -5
- data/lib/arel/table.rb +9 -6
- data/lib/arel/tree_manager.rb +8 -15
- data/lib/arel/update_manager.rb +20 -5
- data/lib/arel/visitors/dot.rb +81 -90
- data/lib/arel/visitors/mysql.rb +23 -5
- data/lib/arel/visitors/postgresql.rb +1 -22
- data/lib/arel/visitors/sqlite.rb +25 -0
- data/lib/arel/visitors/to_sql.rb +170 -36
- data/lib/arel/visitors/visitor.rb +2 -2
- data/lib/arel.rb +23 -4
- data/lib/rails/generators/active_record/application_record/USAGE +8 -0
- data/lib/rails/generators/active_record/application_record/templates/application_record.rb.tt +1 -1
- data/lib/rails/generators/active_record/migration/templates/create_table_migration.rb.tt +4 -1
- data/lib/rails/generators/active_record/migration.rb +3 -1
- data/lib/rails/generators/active_record/model/USAGE +113 -0
- data/lib/rails/generators/active_record/model/model_generator.rb +15 -6
- data/lib/rails/generators/active_record/model/templates/abstract_base_class.rb.tt +1 -1
- data/lib/rails/generators/active_record/model/templates/model.rb.tt +1 -1
- data/lib/rails/generators/active_record/model/templates/module.rb.tt +2 -2
- data/lib/rails/generators/active_record/multi_db/multi_db_generator.rb +16 -0
- data/lib/rails/generators/active_record/multi_db/templates/multi_db.rb.tt +44 -0
- metadata +103 -17
- data/lib/active_record/connection_adapters/legacy_pool_manager.rb +0 -35
- data/lib/active_record/null_relation.rb +0 -67
@@ -2,19 +2,19 @@
|
|
2
2
|
|
3
3
|
module ActiveRecord
|
4
4
|
module Locking
|
5
|
-
# == What is Optimistic Locking
|
5
|
+
# == What is \Optimistic \Locking
|
6
6
|
#
|
7
7
|
# Optimistic locking allows multiple users to access the same record for edits, and assumes a minimum of
|
8
8
|
# conflicts with the data. It does this by checking whether another process has made changes to a record since
|
9
|
-
# it was opened, an
|
9
|
+
# it was opened, an ActiveRecord::StaleObjectError exception is thrown if that has occurred
|
10
10
|
# and the update is ignored.
|
11
11
|
#
|
12
|
-
# Check out
|
12
|
+
# Check out +ActiveRecord::Locking::Pessimistic+ for an alternative.
|
13
13
|
#
|
14
14
|
# == Usage
|
15
15
|
#
|
16
16
|
# Active Record supports optimistic locking if the +lock_version+ field is present. Each update to the
|
17
|
-
# record increments the +lock_version+
|
17
|
+
# record increments the integer column +lock_version+ and the locking facilities ensure that records instantiated twice
|
18
18
|
# will let the last one saved raise a +StaleObjectError+ if the first was also updated. Example:
|
19
19
|
#
|
20
20
|
# p1 = Person.find(1)
|
@@ -56,11 +56,11 @@ module ActiveRecord
|
|
56
56
|
class_attribute :lock_optimistically, instance_writer: false, default: true
|
57
57
|
end
|
58
58
|
|
59
|
-
def locking_enabled?
|
59
|
+
def locking_enabled? # :nodoc:
|
60
60
|
self.class.locking_enabled?
|
61
61
|
end
|
62
62
|
|
63
|
-
def increment!(*, **)
|
63
|
+
def increment!(*, **) # :nodoc:
|
64
64
|
super.tap do
|
65
65
|
if locking_enabled?
|
66
66
|
self[self.class.locking_column] += 1
|
@@ -69,6 +69,11 @@ module ActiveRecord
|
|
69
69
|
end
|
70
70
|
end
|
71
71
|
|
72
|
+
def initialize_dup(other) # :nodoc:
|
73
|
+
super
|
74
|
+
_clear_locking_column if locking_enabled?
|
75
|
+
end
|
76
|
+
|
72
77
|
private
|
73
78
|
def _create_record(attribute_names = self.attribute_names)
|
74
79
|
if locking_enabled?
|
@@ -90,7 +95,8 @@ module ActiveRecord
|
|
90
95
|
begin
|
91
96
|
locking_column = self.class.locking_column
|
92
97
|
lock_attribute_was = @attributes[locking_column]
|
93
|
-
|
98
|
+
|
99
|
+
update_constraints = _query_constraints_hash
|
94
100
|
|
95
101
|
attribute_names = attribute_names.dup if attribute_names.frozen?
|
96
102
|
attribute_names << locking_column
|
@@ -99,8 +105,7 @@ module ActiveRecord
|
|
99
105
|
|
100
106
|
affected_rows = self.class._update_record(
|
101
107
|
attributes_with_values(attribute_names),
|
102
|
-
|
103
|
-
locking_column => lock_value_for_database
|
108
|
+
update_constraints
|
104
109
|
)
|
105
110
|
|
106
111
|
if affected_rows != 1
|
@@ -117,16 +122,9 @@ module ActiveRecord
|
|
117
122
|
end
|
118
123
|
|
119
124
|
def destroy_row
|
120
|
-
|
121
|
-
|
122
|
-
locking_column = self.class.locking_column
|
123
|
-
|
124
|
-
affected_rows = self.class._delete_record(
|
125
|
-
@primary_key => id_in_database,
|
126
|
-
locking_column => _lock_value_for_database(locking_column)
|
127
|
-
)
|
125
|
+
affected_rows = super
|
128
126
|
|
129
|
-
if affected_rows != 1
|
127
|
+
if locking_enabled? && affected_rows != 1
|
130
128
|
raise ActiveRecord::StaleObjectError.new(self, "destroy")
|
131
129
|
end
|
132
130
|
|
@@ -141,6 +139,18 @@ module ActiveRecord
|
|
141
139
|
end
|
142
140
|
end
|
143
141
|
|
142
|
+
def _clear_locking_column
|
143
|
+
self[self.class.locking_column] = nil
|
144
|
+
clear_attribute_change(self.class.locking_column)
|
145
|
+
end
|
146
|
+
|
147
|
+
def _query_constraints_hash
|
148
|
+
return super unless locking_enabled?
|
149
|
+
|
150
|
+
locking_column = self.class.locking_column
|
151
|
+
super.merge(locking_column => _lock_value_for_database(locking_column))
|
152
|
+
end
|
153
|
+
|
144
154
|
module ClassMethods
|
145
155
|
DEFAULT_LOCKING_COLUMN = "lock_version"
|
146
156
|
|
@@ -158,10 +168,7 @@ module ActiveRecord
|
|
158
168
|
end
|
159
169
|
|
160
170
|
# The version column used for optimistic locking. Defaults to +lock_version+.
|
161
|
-
|
162
|
-
@locking_column = DEFAULT_LOCKING_COLUMN unless defined?(@locking_column)
|
163
|
-
@locking_column
|
164
|
-
end
|
171
|
+
attr_reader :locking_column
|
165
172
|
|
166
173
|
# Reset the column used for optimistic locking back to the +lock_version+ default.
|
167
174
|
def reset_locking_column
|
@@ -175,12 +182,21 @@ module ActiveRecord
|
|
175
182
|
super
|
176
183
|
end
|
177
184
|
|
178
|
-
|
179
|
-
|
180
|
-
|
185
|
+
private
|
186
|
+
def hook_attribute_type(name, cast_type)
|
187
|
+
if lock_optimistically && name == locking_column
|
188
|
+
cast_type = LockingType.new(cast_type)
|
189
|
+
end
|
190
|
+
|
191
|
+
super
|
192
|
+
end
|
193
|
+
|
194
|
+
def inherited(base)
|
195
|
+
super
|
196
|
+
base.class_eval do
|
197
|
+
@locking_column = DEFAULT_LOCKING_COLUMN
|
198
|
+
end
|
181
199
|
end
|
182
|
-
super
|
183
|
-
end
|
184
200
|
end
|
185
201
|
end
|
186
202
|
|
@@ -2,10 +2,12 @@
|
|
2
2
|
|
3
3
|
module ActiveRecord
|
4
4
|
module Locking
|
5
|
+
# = \Pessimistic \Locking
|
6
|
+
#
|
5
7
|
# Locking::Pessimistic provides support for row-level locking using
|
6
8
|
# SELECT ... FOR UPDATE and other lock types.
|
7
9
|
#
|
8
|
-
# Chain <tt>ActiveRecord::Base#find</tt> to
|
10
|
+
# Chain <tt>ActiveRecord::Base#find</tt> to ActiveRecord::QueryMethods#lock to obtain an exclusive
|
9
11
|
# lock on the selected rows:
|
10
12
|
# # select * from accounts where id=1 for update
|
11
13
|
# Account.lock.find(1)
|
@@ -71,6 +73,7 @@ module ActiveRecord
|
|
71
73
|
Locking a record with unpersisted changes is not supported. Use
|
72
74
|
`save` to persist the changes, or `reload` to discard them
|
73
75
|
explicitly.
|
76
|
+
Changed attributes: #{changed.map(&:inspect).join(', ')}.
|
74
77
|
MSG
|
75
78
|
end
|
76
79
|
|
@@ -79,11 +82,17 @@ module ActiveRecord
|
|
79
82
|
self
|
80
83
|
end
|
81
84
|
|
82
|
-
# Wraps the passed block in a transaction,
|
83
|
-
# before yielding. You can pass the SQL locking clause
|
84
|
-
# as argument (see
|
85
|
-
|
86
|
-
|
85
|
+
# Wraps the passed block in a transaction, reloading the object with a
|
86
|
+
# lock before yielding. You can pass the SQL locking clause
|
87
|
+
# as an optional argument (see #lock!).
|
88
|
+
#
|
89
|
+
# You can also pass options like <tt>requires_new:</tt>, <tt>isolation:</tt>,
|
90
|
+
# and <tt>joinable:</tt> to the wrapping transaction (see
|
91
|
+
# ActiveRecord::ConnectionAdapters::DatabaseStatements#transaction).
|
92
|
+
def with_lock(*args)
|
93
|
+
transaction_opts = args.extract_options!
|
94
|
+
lock = args.present? ? args.first : true
|
95
|
+
transaction(**transaction_opts) do
|
87
96
|
lock!(lock)
|
88
97
|
yield
|
89
98
|
end
|
@@ -6,38 +6,25 @@ module ActiveRecord
|
|
6
6
|
|
7
7
|
class_attribute :backtrace_cleaner, default: ActiveSupport::BacktraceCleaner.new
|
8
8
|
|
9
|
-
def self.runtime=(value)
|
10
|
-
ActiveRecord::RuntimeRegistry.sql_runtime = value
|
11
|
-
end
|
12
|
-
|
13
|
-
def self.runtime
|
14
|
-
ActiveRecord::RuntimeRegistry.sql_runtime ||= 0
|
15
|
-
end
|
16
|
-
|
17
|
-
def self.reset_runtime
|
18
|
-
rt, self.runtime = runtime, 0
|
19
|
-
rt
|
20
|
-
end
|
21
|
-
|
22
9
|
def strict_loading_violation(event)
|
23
10
|
debug do
|
24
11
|
owner = event.payload[:owner]
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
color("Strict loading violation: #{owner} is marked for strict loading. The #{association} association named :#{name} cannot be lazily loaded.", RED)
|
12
|
+
reflection = event.payload[:reflection]
|
13
|
+
color(reflection.strict_loading_violation_message(owner), RED)
|
29
14
|
end
|
30
15
|
end
|
16
|
+
subscribe_log_level :strict_loading_violation, :debug
|
31
17
|
|
32
18
|
def sql(event)
|
33
|
-
self.class.runtime += event.duration
|
34
|
-
return unless logger.debug?
|
35
|
-
|
36
19
|
payload = event.payload
|
37
20
|
|
38
21
|
return if IGNORE_PAYLOAD_NAMES.include?(payload[:name])
|
39
22
|
|
40
|
-
name
|
23
|
+
name = if payload[:async]
|
24
|
+
"ASYNC #{payload[:name]} (#{payload[:lock_wait].round(1)}ms) (db time #{event.duration.round(1)}ms)"
|
25
|
+
else
|
26
|
+
"#{payload[:name]} (#{event.duration.round(1)}ms)"
|
27
|
+
end
|
41
28
|
name = "CACHE #{name}" if payload[:cached]
|
42
29
|
sql = payload[:sql]
|
43
30
|
binds = nil
|
@@ -47,17 +34,28 @@ module ActiveRecord
|
|
47
34
|
|
48
35
|
binds = []
|
49
36
|
payload[:binds].each_with_index do |attr, i|
|
50
|
-
|
37
|
+
attribute_name = if attr.respond_to?(:name)
|
38
|
+
attr.name
|
39
|
+
elsif attr.respond_to?(:[]) && attr[i].respond_to?(:name)
|
40
|
+
attr[i].name
|
41
|
+
else
|
42
|
+
nil
|
43
|
+
end
|
44
|
+
|
45
|
+
filtered_params = filter(attribute_name, casted_params[i])
|
46
|
+
|
47
|
+
binds << render_bind(attr, filtered_params)
|
51
48
|
end
|
52
49
|
binds = binds.inspect
|
53
50
|
binds.prepend(" ")
|
54
51
|
end
|
55
52
|
|
56
53
|
name = colorize_payload_name(name, payload[:name])
|
57
|
-
sql = color(sql, sql_color(sql), true) if colorize_logging
|
54
|
+
sql = color(sql, sql_color(sql), bold: true) if colorize_logging
|
58
55
|
|
59
56
|
debug " #{name} #{sql}#{binds}"
|
60
57
|
end
|
58
|
+
subscribe_log_level :sql, :debug
|
61
59
|
|
62
60
|
private
|
63
61
|
def type_casted_binds(casted_binds)
|
@@ -81,9 +79,9 @@ module ActiveRecord
|
|
81
79
|
|
82
80
|
def colorize_payload_name(name, payload_name)
|
83
81
|
if payload_name.blank? || payload_name == "SQL" # SQL vs Model Load/Exists
|
84
|
-
color(name, MAGENTA, true)
|
82
|
+
color(name, MAGENTA, bold: true)
|
85
83
|
else
|
86
|
-
color(name, CYAN, true)
|
84
|
+
color(name, CYAN, bold: true)
|
87
85
|
end
|
88
86
|
end
|
89
87
|
|
@@ -115,21 +113,35 @@ module ActiveRecord
|
|
115
113
|
def debug(progname = nil, &block)
|
116
114
|
return unless super
|
117
115
|
|
118
|
-
if ActiveRecord
|
116
|
+
if ActiveRecord.verbose_query_logs
|
119
117
|
log_query_source
|
120
118
|
end
|
121
119
|
end
|
122
120
|
|
123
121
|
def log_query_source
|
124
|
-
source =
|
122
|
+
source = query_source_location
|
125
123
|
|
126
124
|
if source
|
127
125
|
logger.debug(" ↳ #{source}")
|
128
126
|
end
|
129
127
|
end
|
130
128
|
|
131
|
-
|
132
|
-
|
129
|
+
if Thread.respond_to?(:each_caller_location)
|
130
|
+
def query_source_location
|
131
|
+
Thread.each_caller_location do |location|
|
132
|
+
frame = backtrace_cleaner.clean_frame(location)
|
133
|
+
return frame if frame
|
134
|
+
end
|
135
|
+
nil
|
136
|
+
end
|
137
|
+
else
|
138
|
+
def query_source_location
|
139
|
+
backtrace_cleaner.clean(caller(1).lazy).first
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
143
|
+
def filter(name, value)
|
144
|
+
ActiveRecord::Base.inspection_filter.filter_param(name, value)
|
133
145
|
end
|
134
146
|
end
|
135
147
|
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActiveRecord
|
4
|
+
module Marshalling
|
5
|
+
@format_version = 6.1
|
6
|
+
|
7
|
+
class << self
|
8
|
+
attr_reader :format_version
|
9
|
+
|
10
|
+
def format_version=(version)
|
11
|
+
case version
|
12
|
+
when 6.1
|
13
|
+
Methods.remove_method(:marshal_dump) if Methods.method_defined?(:marshal_dump)
|
14
|
+
when 7.1
|
15
|
+
Methods.alias_method(:marshal_dump, :_marshal_dump_7_1)
|
16
|
+
else
|
17
|
+
raise ArgumentError, "Unknown marshalling format: #{version.inspect}"
|
18
|
+
end
|
19
|
+
@format_version = version
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
module Methods
|
24
|
+
def _marshal_dump_7_1
|
25
|
+
payload = [attributes_for_database, new_record?]
|
26
|
+
|
27
|
+
cached_associations = self.class.reflect_on_all_associations.select do |reflection|
|
28
|
+
if association_cached?(reflection.name)
|
29
|
+
association = association(reflection.name)
|
30
|
+
association.loaded? || association.target.present?
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
unless cached_associations.empty?
|
35
|
+
payload << cached_associations.map do |reflection|
|
36
|
+
[reflection.name, association(reflection.name).target]
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
payload
|
41
|
+
end
|
42
|
+
|
43
|
+
def marshal_load(state)
|
44
|
+
attributes_from_database, new_record, associations = state
|
45
|
+
|
46
|
+
attributes = self.class.attributes_builder.build_from_database(attributes_from_database)
|
47
|
+
init_with_attributes(attributes, new_record)
|
48
|
+
|
49
|
+
if associations
|
50
|
+
associations.each do |name, target|
|
51
|
+
association(name).target = target
|
52
|
+
rescue ActiveRecord::AssociationNotFoundError
|
53
|
+
# the association no longer exist, we can just skip it.
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
@@ -0,0 +1,124 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActiveRecord
|
4
|
+
module MessagePack # :nodoc:
|
5
|
+
FORMAT_VERSION = 1
|
6
|
+
|
7
|
+
class << self
|
8
|
+
def dump(input)
|
9
|
+
encoder = Encoder.new
|
10
|
+
[FORMAT_VERSION, encoder.encode(input), encoder.entries]
|
11
|
+
end
|
12
|
+
|
13
|
+
def load(dumped)
|
14
|
+
format_version, top_level, entries = dumped
|
15
|
+
unless format_version == FORMAT_VERSION
|
16
|
+
raise "Invalid format version: #{format_version.inspect}"
|
17
|
+
end
|
18
|
+
Decoder.new(entries).decode(top_level)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
module Extensions
|
23
|
+
extend self
|
24
|
+
|
25
|
+
def install(registry)
|
26
|
+
registry.register_type 119, ActiveModel::Type::Binary::Data,
|
27
|
+
packer: :to_s,
|
28
|
+
unpacker: :new
|
29
|
+
|
30
|
+
registry.register_type 120, ActiveRecord::Base,
|
31
|
+
packer: method(:write_record),
|
32
|
+
unpacker: method(:read_record),
|
33
|
+
recursive: true
|
34
|
+
end
|
35
|
+
|
36
|
+
def write_record(record, packer)
|
37
|
+
packer.write(ActiveRecord::MessagePack.dump(record))
|
38
|
+
end
|
39
|
+
|
40
|
+
def read_record(unpacker)
|
41
|
+
ActiveRecord::MessagePack.load(unpacker.read)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
class Encoder
|
46
|
+
attr_reader :entries
|
47
|
+
|
48
|
+
def initialize
|
49
|
+
@entries = []
|
50
|
+
@refs = {}.compare_by_identity
|
51
|
+
end
|
52
|
+
|
53
|
+
def encode(input)
|
54
|
+
if input.is_a?(Array)
|
55
|
+
input.map { |record| encode_record(record) }
|
56
|
+
elsif input
|
57
|
+
encode_record(input)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
def encode_record(record)
|
62
|
+
ref = @refs[record]
|
63
|
+
|
64
|
+
if !ref
|
65
|
+
ref = @refs[record] = @entries.size
|
66
|
+
@entries << build_entry(record)
|
67
|
+
add_cached_associations(record, @entries.last)
|
68
|
+
end
|
69
|
+
|
70
|
+
ref
|
71
|
+
end
|
72
|
+
|
73
|
+
def build_entry(record)
|
74
|
+
[
|
75
|
+
ActiveSupport::MessagePack::Extensions.dump_class(record.class),
|
76
|
+
record.attributes_for_database,
|
77
|
+
record.new_record?
|
78
|
+
]
|
79
|
+
end
|
80
|
+
|
81
|
+
def add_cached_associations(record, entry)
|
82
|
+
record.class.normalized_reflections.each_value do |reflection|
|
83
|
+
if record.association_cached?(reflection.name) && record.association(reflection.name).loaded?
|
84
|
+
entry << reflection.name << encode(record.association(reflection.name).target)
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
class Decoder
|
91
|
+
def initialize(entries)
|
92
|
+
@records = entries.map { |entry| build_record(entry) }
|
93
|
+
@records.zip(entries) { |record, entry| resolve_cached_associations(record, entry) }
|
94
|
+
end
|
95
|
+
|
96
|
+
def decode(ref)
|
97
|
+
if ref.is_a?(Array)
|
98
|
+
ref.map { |r| @records[r] }
|
99
|
+
elsif ref
|
100
|
+
@records[ref]
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
def build_record(entry)
|
105
|
+
class_name, attributes_hash, is_new_record, * = entry
|
106
|
+
klass = ActiveSupport::MessagePack::Extensions.load_class(class_name)
|
107
|
+
attributes = klass.attributes_builder.build_from_database(attributes_hash)
|
108
|
+
klass.allocate.init_with_attributes(attributes, is_new_record)
|
109
|
+
end
|
110
|
+
|
111
|
+
def resolve_cached_associations(record, entry)
|
112
|
+
i = 3 # entry == [class_name, attributes_hash, is_new_record, *associations]
|
113
|
+
while i < entry.length
|
114
|
+
begin
|
115
|
+
record.association(entry[i]).target = decode(entry[i + 1])
|
116
|
+
rescue ActiveRecord::AssociationNotFoundError
|
117
|
+
# The association no longer exists, so just skip it.
|
118
|
+
end
|
119
|
+
i += 2
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end
|
@@ -48,25 +48,25 @@ module ActiveRecord
|
|
48
48
|
context.save(response)
|
49
49
|
end
|
50
50
|
|
51
|
+
def reading_request?(request)
|
52
|
+
request.get? || request.head?
|
53
|
+
end
|
54
|
+
|
51
55
|
private
|
52
56
|
def read_from_primary(&blk)
|
53
|
-
ActiveRecord::Base.connected_to(role: ActiveRecord
|
54
|
-
instrumenter.instrument("database_selector.active_record.read_from_primary")
|
55
|
-
yield
|
56
|
-
end
|
57
|
+
ActiveRecord::Base.connected_to(role: ActiveRecord.writing_role, prevent_writes: true) do
|
58
|
+
instrumenter.instrument("database_selector.active_record.read_from_primary", &blk)
|
57
59
|
end
|
58
60
|
end
|
59
61
|
|
60
62
|
def read_from_replica(&blk)
|
61
|
-
ActiveRecord::Base.connected_to(role: ActiveRecord
|
62
|
-
instrumenter.instrument("database_selector.active_record.read_from_replica")
|
63
|
-
yield
|
64
|
-
end
|
63
|
+
ActiveRecord::Base.connected_to(role: ActiveRecord.reading_role, prevent_writes: true) do
|
64
|
+
instrumenter.instrument("database_selector.active_record.read_from_replica", &blk)
|
65
65
|
end
|
66
66
|
end
|
67
67
|
|
68
|
-
def write_to_primary
|
69
|
-
ActiveRecord::Base.connected_to(role: ActiveRecord
|
68
|
+
def write_to_primary
|
69
|
+
ActiveRecord::Base.connected_to(role: ActiveRecord.writing_role, prevent_writes: false) do
|
70
70
|
instrumenter.instrument("database_selector.active_record.wrote_to_primary") do
|
71
71
|
yield
|
72
72
|
ensure
|
@@ -4,8 +4,10 @@ require "active_record/middleware/database_selector/resolver"
|
|
4
4
|
|
5
5
|
module ActiveRecord
|
6
6
|
module Middleware
|
7
|
+
# = Database Selector \Middleware
|
8
|
+
#
|
7
9
|
# The DatabaseSelector Middleware provides a framework for automatically
|
8
|
-
# swapping from the primary to the replica database connection. Rails
|
10
|
+
# swapping from the primary to the replica database connection. \Rails
|
9
11
|
# provides a basic framework to determine when to swap and allows for
|
10
12
|
# applications to write custom strategy classes to override the default
|
11
13
|
# behavior.
|
@@ -15,18 +17,26 @@ module ActiveRecord
|
|
15
17
|
# resolver context class that sets a value that helps the resolver class
|
16
18
|
# decide when to switch.
|
17
19
|
#
|
18
|
-
# Rails default middleware uses the request's session to set a timestamp
|
20
|
+
# \Rails default middleware uses the request's session to set a timestamp
|
19
21
|
# that informs the application when to read from a primary or read from a
|
20
22
|
# replica.
|
21
23
|
#
|
22
|
-
# To use the DatabaseSelector in your application with default settings
|
23
|
-
# the
|
24
|
+
# To use the DatabaseSelector in your application with default settings,
|
25
|
+
# run the provided generator.
|
24
26
|
#
|
25
|
-
#
|
26
|
-
#
|
27
|
-
#
|
27
|
+
# $ bin/rails g active_record:multi_db
|
28
|
+
#
|
29
|
+
# This will create a file named +config/initializers/multi_db.rb+ with the
|
30
|
+
# following contents:
|
31
|
+
#
|
32
|
+
# Rails.application.configure do
|
33
|
+
# config.active_record.database_selector = { delay: 2.seconds }
|
34
|
+
# config.active_record.database_resolver = ActiveRecord::Middleware::DatabaseSelector::Resolver
|
35
|
+
# config.active_record.database_resolver_context = ActiveRecord::Middleware::DatabaseSelector::Resolver::Session
|
36
|
+
# end
|
28
37
|
#
|
29
|
-
#
|
38
|
+
# Alternatively you can set the options in your environment config or
|
39
|
+
# any other config file loaded on boot.
|
30
40
|
#
|
31
41
|
# The default behavior can be changed by setting the config options to a
|
32
42
|
# custom class:
|
@@ -34,6 +44,10 @@ module ActiveRecord
|
|
34
44
|
# config.active_record.database_selector = { delay: 2.seconds }
|
35
45
|
# config.active_record.database_resolver = MyResolver
|
36
46
|
# config.active_record.database_resolver_context = MyResolver::MySession
|
47
|
+
#
|
48
|
+
# Note: If you are using <tt>rails new my_app --minimal</tt> you will need
|
49
|
+
# to call <tt>require "active_support/core_ext/integer/time"</tt> to load
|
50
|
+
# the core extension in order to use +2.seconds+
|
37
51
|
class DatabaseSelector
|
38
52
|
def initialize(app, resolver_klass = nil, context_klass = nil, options = {})
|
39
53
|
@app = app
|
@@ -59,7 +73,7 @@ module ActiveRecord
|
|
59
73
|
context = context_klass.call(request)
|
60
74
|
resolver = resolver_klass.call(context, options)
|
61
75
|
|
62
|
-
response = if reading_request?(request)
|
76
|
+
response = if resolver.reading_request?(request)
|
63
77
|
resolver.read(&blk)
|
64
78
|
else
|
65
79
|
resolver.write(&blk)
|
@@ -68,10 +82,6 @@ module ActiveRecord
|
|
68
82
|
resolver.update_context(response)
|
69
83
|
response
|
70
84
|
end
|
71
|
-
|
72
|
-
def reading_request?(request)
|
73
|
-
request.get? || request.head?
|
74
|
-
end
|
75
85
|
end
|
76
86
|
end
|
77
87
|
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActiveRecord
|
4
|
+
module Middleware
|
5
|
+
# = Shard Selector \Middleware
|
6
|
+
#
|
7
|
+
# The ShardSelector Middleware provides a framework for automatically
|
8
|
+
# swapping shards. \Rails provides a basic framework to determine which
|
9
|
+
# shard to switch to and allows for applications to write custom strategies
|
10
|
+
# for swapping if needed.
|
11
|
+
#
|
12
|
+
# The ShardSelector takes a set of options (currently only +lock+ is supported)
|
13
|
+
# that can be used by the middleware to alter behavior. +lock+ is
|
14
|
+
# true by default and will prohibit the request from switching shards once
|
15
|
+
# inside the block. If +lock+ is false, then shard swapping will be allowed.
|
16
|
+
# For tenant based sharding, +lock+ should always be true to prevent application
|
17
|
+
# code from mistakenly switching between tenants.
|
18
|
+
#
|
19
|
+
# Options can be set in the config:
|
20
|
+
#
|
21
|
+
# config.active_record.shard_selector = { lock: true }
|
22
|
+
#
|
23
|
+
# Applications must also provide the code for the resolver as it depends on application
|
24
|
+
# specific models. An example resolver would look like this:
|
25
|
+
#
|
26
|
+
# config.active_record.shard_resolver = ->(request) {
|
27
|
+
# subdomain = request.subdomain
|
28
|
+
# tenant = Tenant.find_by_subdomain!(subdomain)
|
29
|
+
# tenant.shard
|
30
|
+
# }
|
31
|
+
class ShardSelector
|
32
|
+
def initialize(app, resolver, options = {})
|
33
|
+
@app = app
|
34
|
+
@resolver = resolver
|
35
|
+
@options = options
|
36
|
+
end
|
37
|
+
|
38
|
+
attr_reader :resolver, :options
|
39
|
+
|
40
|
+
def call(env)
|
41
|
+
request = ActionDispatch::Request.new(env)
|
42
|
+
|
43
|
+
shard = selected_shard(request)
|
44
|
+
|
45
|
+
set_shard(shard) do
|
46
|
+
@app.call(env)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
private
|
51
|
+
def selected_shard(request)
|
52
|
+
resolver.call(request)
|
53
|
+
end
|
54
|
+
|
55
|
+
def set_shard(shard, &block)
|
56
|
+
ActiveRecord::Base.connected_to(shard: shard.to_sym) do
|
57
|
+
ActiveRecord::Base.prohibit_shard_swapping(options.fetch(:lock, true), &block)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|