{"id":13483171,"url":"https://github.com/orslumen/record-cache","last_synced_at":"2025-03-27T13:33:37.346Z","repository":{"id":1728580,"uuid":"2466338","full_name":"orslumen/record-cache","owner":"orslumen","description":"Cache Active Model Records in Rails 3","archived":false,"fork":false,"pushed_at":"2022-07-21T22:28:34.000Z","size":293,"stargazers_count":145,"open_issues_count":8,"forks_count":39,"subscribers_count":6,"default_branch":"master","last_synced_at":"2024-10-30T16:41:35.665Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":"","language":"Ruby","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":"mscdex/node-oscar","license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/orslumen.png","metadata":{"files":{"readme":"README.markdown","changelog":null,"contributing":null,"funding":null,"license":"MIT-LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2011-09-27T08:19:49.000Z","updated_at":"2023-07-10T17:08:37.000Z","dependencies_parsed_at":"2022-07-22T05:18:15.508Z","dependency_job_id":null,"html_url":"https://github.com/orslumen/record-cache","commit_stats":null,"previous_names":[],"tags_count":7,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/orslumen%2Frecord-cache","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/orslumen%2Frecord-cache/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/orslumen%2Frecord-cache/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/orslumen%2Frecord-cache/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/orslumen","download_url":"https://codeload.github.com/orslumen/record-cache/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":245854850,"owners_count":20683424,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2024-07-31T17:01:08.776Z","updated_at":"2025-03-27T13:33:37.035Z","avatar_url":"https://github.com/orslumen.png","language":"Ruby","readme":"Record Cache\n============\n\n[![Build Status](http://img.shields.io/travis/orslumen/record-cache.svg?style=flat)](https://travis-ci.org/orslumen/record-cache)\n[![Gem Version](http://img.shields.io/gem/v/record-cache.svg?style=flat)](https://rubygems.org/gems/record-cache)\n\n*Cache Active Model Records in Rails 3 and Rails 4*\n\nRecord Cache transparently stores Records in a Cache Store to retrieve those Records from the store when queried using Active Model.\nCache invalidation is performed automatically when Records are created, updated or destroyed. Currently only Active Record is supported, but more\ndata stores may be added in the future.\n\nUsage\n-----\n\n#### Installation\n\nAdd the following line to your Gemfile:\n\n    gem 'record-cache'\n\n\n#### Initializer\n\nIn /config/initializers/record_cache.rb:\n\n    # --- Version Store\n    # All Workers that use the Record Cache should point to the same Version Store\n    # E.g. a MemCached cluster or a Redis Store (defaults to Rails.cache)\n    RecordCache::Base.version_store = Rails.cache\n\n    # --- Record Stores\n    # Register Cache Stores for the Records themselves\n    # Note: A different Cache Store could be used per Model, but in most configurations the following 2 stores will suffice:\n\n    # The :local store is used to keep records in Worker memory\n    RecordCache::Base.register_store(:local, ActiveSupport::Cache.lookup_store(:memory_store))\n\n    # The :shared store is used to share Records between multiple Workers\n    RecordCache::Base.register_store(:shared, Rails.cache)\n\n    # Different logger\n    # RecordCache::Base.logger = Logger.new(STDOUT)\n\n\n#### Models\n\nDefine the Caching Strategy in your models.\n\nTypical Example: /app/models/person.rb:\n\n    class Person \u003c ActiveRecord::Base\n      cache_records :store =\u003e :shared, :key =\u003e \"pers\"\n    end\n\nExample with Index Cache: /app/models/permission.rb:\n\n    class Permission \u003c ActiveRecord::Base\n      cache_records :store =\u003e :shared, :key =\u003e \"perm\", :index =\u003e [:person_id]\n\n      belongs_to :person\n    end\n\nExample with Full Table Cache: /app/models/priority.rb:\n\n    class Priority \u003c ActiveRecord::Base\n      cache_records :store =\u003e :local, :key =\u003e \"prio\", :full_table =\u003e true\n    end\n\nThe following options are available:\n\n- \u003ca name=\"store\" /\u003e`:store`: The name of the Cache Store for the Records (default: `Rails.cache`)\n\n    _@see Initializer section above how to define named Cache Stores_\n\n- \u003ca name=\"key\" /\u003e`:key`: Provide a short (unique) name to be used in the cache keys (default: `\u003cmodel\u003e.name`)\n\n    _Using shorter cache keys will improve performance as less data is sent to the Cache Stores_\n\n- \u003ca name=\"unique_index\" /\u003e`:unique_index`: The name(s) of the unique index column (default: `id`)\n\n    _Choose a different column as the unqiue index column in case it is not `id`_\n\n- \u003ca name=\"index\" /\u003e`:index`: An array of `:belongs_to` attributes to cache `:has_many` relations (default: `[]`)\n\n    _`has_many` relations will lead to queries like: `SELECT * FROM permissions WHERE permission.person_id = 10`\n      As Record Cache only caches records by ID, this query would always hit the DB. If an index is set\n      on person_id (like in the example above), Record Cache will keep track of the Permission IDs per\n      Person ID.\n      Using that information the query will be translated to: `SELECT * FROM permissions WHERE permission.id IN (14,15,...)`\n      and the permissions can be retrieved from cache.\n      Note: The administration overhead for the Permission IDs per Person ID leads to more calls to the Version Store and the Record\n      Store. Whether or not it is profitable to add specific indexes for has_many relations will differ per use-case._\n\n- \u003ca name=\"full_table\" /\u003e`:full_table`: Whether the whole table should be stored as a single block in the cache (default: `false`)\n\n    _Use this option in case this table is small, is only rarely updated and needs to be retrieved as a whole in most cases.\n     For example to fill a Language or Country drop-down._\n\n- \u003ca name=\"ttl\" /\u003e`:ttl`: Time to live (default: `infinitely`)\n\n    _In case not all updates go through Rails (not a recommended design) this option makes it possible to specify a TTL for the cached\n     records._\n\nIt is also possible to listen to write failures on the Version Store that could lead to stale results: \n\n    RecordCache::Base.version_store.on_write_failure{ |key| clear_this_key_after_2_seconds(key) }\n\n\n#### Tests\n\nTo switch off Record Cache during the tests, add the following line to /config/environments/test.rb:\n\n    RecordCache::Base.disable!\n\nBut it is also possible (and preferable during Integration Tests) to keep the Record Cache switched on.\nTo make sure the cache is invalidated for all updated Records after each test/scenario, require the\nresettable_version_store and reset the Version Store after each test/scenario.\n\nRSpec 2 example, in spec/spec_helper.rb:\n\n    require 'record_cache/test/resettable_version_store'\n\n    RSpec.configure do |config|\n      config.after(:each) do\n        RecordCache::Base.version_store.reset!\n      end\n    end\n\nCucumber example, in features/support/env.rb:\n\n    require 'record_cache/test/resettable_version_store'\n\n    After do |scenario|\n      RecordCache::Base.version_store.reset!\n    end\n\n\nRestrictions\n------------\n\n1. This gem is dependent on Rails 3 or Rails 4\n\n2. Only Active Record is supported as a data store.\n\n3. All servers that host Workers should be time-synchronized (otherwise the Version Store may return stale results).\n\n#### Caveats\n\n1. Record Cache sorting mimics the MySQL sort order being case-insensitive and using collation.\n   _If you need a different sort order, check out the code in `\u003cgem\u003e/lib/record_cache/strategy/util.rb`._\n\n1. Using `update_all` to modify attributes used in the [:index option](#index) will lead to stale results.\n\n1. (Uncommon) If you have a model (A) with a `has_many :autosave =\u003e true` relation to another model (B) that defines a\n   `:counter_cache` back to model A, the `\u003cmodel B\u003e_count` attribute will contain stale results. To solve this, add an\n   after_save hook to model A and update the `\u003cmodel B\u003e_count` attribute there in case the `has_many` relation was loaded.\n\n1. The combination of Mongrel (Rack) and the Dalli `:threadsafe =\u003e false` option will lead to the following errors in\n   your log file: `undefined method `constantize’ for 0:Fixnum`. This is because Mongrel creates multiple threads.\n   To overcome this, set thread_save to true, or consider using a different webserver like Unicorn.\n\n1. Nested transactions: When using nested transactions, Rails will also call the after_commit hook of records that were\n   updated within a nested transaction that was rolled back. This will cause the cache to contain updates that are not\n   in the database.\n   To overcome this, skip using nested transactions, or disable record cache and manually invalidate all records that were\n   possibly updated within the nested transactions.\n   \n1. Flapping version store. Due to network hiccups the version store may not always be accessible to read/write the current\n   version of a record. This may lead to stale results. The `on_write_failure` hook can be used to be informed when the\n   communication to the version store fails and to take appropriate action, e.g. resetting the version store for that\n   record some time later.\n\nExplain\n-------\n\n#### Retrieval\n\nEach query is parsed and sent to Record Cache before it is executed to check if the query is cacheable.\nA query is cacheable if:\n\n- it contains at least one `where(:id =\u003e ...)` or `where(\u003cindexed attribute\u003e =\u003e ...)` clause, and\n\n- it contains zero or more `where(\u003cattribute\u003e =\u003e \u003csingle value\u003e)` clauses on attributes in the same model, and\n\n- it has no `limit(...)` defined, or is limited to 1 record and has exactly one id in the `where(:id =\u003e ...)` clause, and\n\n- it has no `order(...)` clause, or it is sorted on single attributes using ASC and DESC only\n\n- it has no joins, calculations, group by, etc. clauses\n\nWhen the query is accepted by Record Cache, all requested records will be retrieved and cached as follows:\n\nID queries:\n\n1. The Version Store is called to retrieve the current version for each ID using a `multi_read` (keys `rc/\u003cmodel-name\u003e/\u003cid\u003e`).\n\n2. A new version will be generated (using the current timestamp) for each ID unknown to the Version Store.\n\n3. The Record Store is called to retrieve the latest data for each ID using a `multi_read` (keys `rc/\u003cmodel-name\u003e/\u003cid\u003ev\u003ccurrent-version\u003e`).\n\n4. The data of the missing records is retrieved directly from the Data Store (single query) and are subsequently cached in the Record Store.\n\n5. The data of all records is deserialized to Active Model records.\n\n6. The other (simple) `where(\u003cattribute\u003e =\u003e \u003csingle value\u003e)` clauses are applied, if applicable.\n\n7. The (simple) `order(...)` clause is applied, if applicable.\n\nIndex queries:\n\n1. The Version Store is called to retrieve the current version for the group (key `rc/\u003cmodel-name\u003e/\u003cindex\u003e/\u003cid\u003e`).\n\n2. A new version will be generated (using the current timestamp) in case the current version is unknown to the Version Store.\n\n3. The Record Store is called to retrieve the latest set of IDs in this group (key `rc/\u003cmodel-name\u003e/\u003cindex\u003e/\u003cid\u003ev\u003ccurrent-version\u003e`).\n\n4. In case the IDs are missing, the IDs (only) will be retrieved from the Data Store (single query) and subsequently cached in the Record Store.\n\n5. The IDs are passed as an ID query to the id-based-cache (see above).\n\n\n#### Invalidation\n\nThe `after_commit, :on =\u003e :create/:update/:destroy` hooks are used to inform the Record Cache of changes to the cached records.\n\nID cache:\n\n- `:create`: add a new version to the Version Store and cache the record in the Records Store\n\n- `:update`: similar to :create\n\n- `:destroy`: remove the record from the Version Store\n\nIndex cache:\n\n- `:create`: increment Version Store for each index that contains the indexed attribute value of this record.\n             In case the IDs in this group are cached and fresh, add the ID of the new record to the group and store\n             the updated list of IDs in the Records Store.\n\n- `:update`: For each index that is included in the changed attribute, apply the :destoy logic to the old value\n             and the :create logic to the new value.\n\n- `:destroy`: increment Version Store for each index that contains the indexed attribute value of this record.\n              In case the IDs in this group are current cached and fresh, remove the ID of the record from the group and store\n              the updated list of IDs in the Records Store.\n\nThe `update_all` method of Active Record Relation is also overridden to make sure that mass-updates are processed correctly, e.g. used by the\n:counter_cache. As the details of the change are not known, all records that match the IDs mentioned in the update_all statement are invalidated by\nremoving them from the Version Store.\n\nFinally for `has_many` relations, the `after_commit` hooks are not triggered on add and remove. Whether this is a bug or feature I do not know, but\nfor Active Record the Has Many Association is patched to invalidate the Index Cache of the referenced (reflection) Record in case it has\nan [:index](#index) on the reverse `belongs_to` relation.\n\n\nDevelopment\n-----------\n\n    $ bundle\n    $ appraisal\n\n    # run the specs (requires ruby 1.9.3)\n    $ appraisal rake\n\n    # run the specs for a particular version (supported are rails-30, rails-31, rails-32, rails-40)\n    $ appraisal rails-32 rake\n\n    # run a single spec\n    $ appraisal rails-40 rspec ./spec/lib/strategy/base_spec.rb:61\n\nDeploying the gem:\n\n    # Don't forget to update the version in lib/record_cache/version.rb\n    $ git tag -a v0.1.1 -m 'version 0.1.1'\n    $ git push origin master --tags\n    $ gem update --system\n    $ gem build record-cache.gemspec\n    $ gem push record-cache-0.1.1.gem\n\nDebugging the gem:\n\nSwitch on DEBUG logging (`config.log_level = :debug` in development.rb) to get more information on cache hits and misses.\n\n\nRelease Notes\n-------------\n\n#### Version 0.1.5 (next version)\n\n1. On-write-failure hook on the version store\n1. \n\n#### Version 0.1.4\n\n1. Case insensitive filtering\n1. to_sql no longer destroying the sql binds (John Morales)\n1. Rails 4.0 support (Robin Roestenburg \u0026 Pitr https://github.com/orslumen/record-cache/pull/44)\n1. Rails 4.1 support (Pitr https://github.com/orslumen/record-cache/pull/45)\n1. Fix for +select('distinct ...')+ construct\n\n\n#### Version 0.1.3\n\nFixed Bugs:\n\n1. \"\\u0000\" is also used by Arel as a parameter query binding marker.\n1. https://github.com/orslumen/record-cache/issues/2: bypassing record_cache when selecting rows with lock\n\nAdded:\n\n1. Release Notes ;)\n1. Ruby 1.9 fixes, has_one support, Remove Freeze for Dalli encoding (Bryan Mundie https://github.com/orslumen/record-cache/pull/3)\n1. :unique_index option\n1. :full_table option\n1. [Appraisal](https://github.com/thoughtbot/appraisal) - working with different Rails versions\n1. [Travis CI](https://travis-ci.org/orslumen/record-cache) - continuous integration service (Robin Roestenburg https://github.com/orslumen/record-cache/pull/33)\n1. Rails 3.1 and 3.2 support\n1. Replace request_cache in favor of ActiveRecord::QueryCache (Lawrence Pit https://github.com/orslumen/record-cache/pull/11)\n1. Possibility to set a custom logger\n1. Select queries within a transaction will automatically bypass the cache\n1. No more increment calls to the Version Store (only set and delete)\n1. Support for Dalli's +multi+ method to pipeline multiple cache writes (when storing multiple fresh records in the cache, or outdating multiple records after update_all)\n1. Updated tests to RSpec 3\n1. Fix deserialization of records with serialized attributes, see https://github.com/orslumen/record-cache/issues/19\n1. Ruby 2 fix\n\n\n#### Version 0.1.2\n\nRefactoring: Moved Serialization, Sorting and Filtering to separate Util class.\n\nNow it is possible to re-use MySQL style sorting (with collation) in your own app, e.g. by calling `RecordCache::Strategy::Util.sort!(Apple.all, :name)`.\n\n\n#### Version 0.1.1\n\nAdded support for Rails 3.1\n\n\n#### Version 0.1.0\n\nFirst version, with the following Strategies:\n\n1. Request Cache\n1. ID Cache\n1. Index Cache\n\n----\nCopyright (c) 2011-2015 Orslumen, released under the MIT license\n","funding_links":[],"categories":["Ruby","Caching"],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Forslumen%2Frecord-cache","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Forslumen%2Frecord-cache","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Forslumen%2Frecord-cache/lists"}