# ActiveModelCachers

[![Gem Version](https://img.shields.io/gem/v/active_model_cachers.svg?style=flat)](http://rubygems.org/gems/active_model_cachers)
[![Build Status](https://travis-ci.org/khiav223577/active_model_cachers.svg?branch=master)](https://travis-ci.org/khiav223577/active_model_cachers)
[![RubyGems](http://img.shields.io/gem/dt/active_model_cachers.svg?style=flat)](http://rubygems.org/gems/active_model_cachers)
[![Code Climate](https://codeclimate.com/github/khiav223577/active_model_cachers/badges/gpa.svg)](https://codeclimate.com/github/khiav223577/active_model_cachers)
[![Test Coverage](https://codeclimate.com/github/khiav223577/active_model_cachers/badges/coverage.svg)](https://codeclimate.com/github/khiav223577/active_model_cachers/coverage)

Provide cachers to the model so that you could specify which you want to cache. Data will be cached at `Rails.cache` and also at application level via `RequestStore` to cache values between requests. Cachers will maintain cached objects and expire them when they are changed (by create, update, destroy, and even delete).

- [Multi-level Cache](#multi-level-cache)
- Do not pollute original ActiveModel API.
- Support ActiveRecord 3, 4, 5.
- High test coverage


## Installation

Add this line to your application's Gemfile:

```ruby
gem 'active_model_cachers'
```

And then execute:

    $ bundle

Or install it yourself as:

    $ gem install active_model_cachers

Add an initializer with this code to your project:

```rb
ActiveModelCachers.config do |config|
  config.store = Rails.cache # specify where the cache will be stored
end
```

## Usage

`cache_at(name, query = nil, options = {})`

Specifie a cache on the model.
 - name: the attribute name.
 - query: how to get data on cache miss. It will be set automatically if the name match an association or an attribute.
 - options: see [here](#options)

## Cache whatever you want

### Example 1: Cache the number of active user

After specifying the name as `active_count` and how to get data when cache miss by lambda `User.active.count`.
You could access the cached data by calling `active_count` method on the cacher, `User.cacher`.

```rb
class User < ActiveRecord::Base
  scope :active, ->{ where('last_login_at > ?', 7.days.ago) }
  cache_at :active_count, ->{ User.active.count }, expire_by: 'User#last_login_at'
end

@count = User.cacher.active_count
```

You may want to flush cache on the number of active user changed. It can be done by simply setting [`expire_by`](#expire_by). In this case, `User#last_login_at` means flushing the cache when a user's `last_login_at` is changed (whenever by save, update, create, destroy or delete).

### Example 2: Cache the number of user

In this example, the cache should be cleaned on user `destroyed`, or new user `created`, but not on user `updated`. You could specify the cleaning callback to only fire on certain events by [`on`](#on).

```rb
class User < ActiveRecord::Base
  cache_at :count, ->{ User.count }, expire_by: 'User', on: [:create, :destroy]
end

@count = User.cacher.count
```

### Example 3: Access the cacher from a model instance

You could use the cacher from instance scope, e.g. `user.cacher`, instead of `User.cacher`. The difference is that the `binding` of query lambda is changed. In this example, you could write the query as `posts.exists?` in that it's in instance scope, and the binding of the lambda is `user`, not `User`. So that it accesses `posts` method of `user`.

```rb
class User < ActiveRecord::Base
  has_many :posts
  cache_at :has_post?, ->{ posts.exists? }, expire_by: :posts
end

do_something if current_user.cacher.has_post?
```

In this example, the cache should be cleaned when the `posts` of the user changed. You could just set `expire_by` to the association: `:posts`, and then it will do all the works for you magically. (If you want know more details, it actually set [`expire_by`](#expire_by) to `Post#user_id` and [`foreign_key`](#foreign_key), which is needed for backtracing the user id from post, to `:user_id`)


### Example 4: Pass an argument to the query lambda.

You could cache not only the query result of database but also the result of outer service. Becasue `email_valid?` doesn't match an association or an attribute, by default, the cache will not be cleaned by any changes.

```rb
class User < ActiveRecord::Base
  cache_at :email_valid?, ->(email){ ValidEmail2::Address.new(email).valid_mx? }
end

render_error if not User.cacher_at('pearl@example.com').email_valid?
```

The query lambda can have one parameter, you could pass variable to it by using `cacher_at`. For example, `User.cacher_at(email)`.

```rb
class User < ActiveRecord::Base
  cache_at :email_valid?, ->(email){ ValidEmail2::Address.new(email).valid_mx? }, primary_key: :email
end

render_error if not current_user.cacher.email_valid?
```

It can also be accessed from instance cacher. But you have to set [`primary_key`](#primary_key), which is needed to know which attribute should be passed to the parameter.

### Example 5: Clean the cache manually

Sometimes it needs to maintain the cache manually. For example, after calling `update_all`, `delete_all` or `import` records without calling callbacks.

```rb
class User < ActiveRecord::Base
  has_one :profile
  cache_at :profile
end

# clean the cache by name
current_user.cacher.clean(:profile)

# Or calling the clean_* method
current_user.cacher.clean_profile

# clean the cache without loading model
User.cacher_at(user_id).clean_profile
```

## Smart Caching

### Multi-level Cache
There is multi-level cache in order to make the speed of data access go faster.

1. RequestStore
2. Rails.cache
3. Association Cache
4. Database

`RequestStore` is used to make sure same object will not loaded from cache twice, since the data transfer between `Cache` and `Application` still consumes time.

`Association Cache` will be used to prevent preloaded objects being loaded again.

For example:
```rb
user = User.includes(:posts).take
user.cacher.posts # => no query will be made even on cache miss.
```

## Convenient syntax sugar for caching ActiveRecord

### Caching Associations
```rb
class User < ActiveRecord::Base
  has_one :profile
  cache_at :profile
end

@profile = current_user.cacher.profile

# directly get profile without loading user.
@profile = User.cacher_at(user_id).profile
```

### Caching Polymorphic Associations

TODO

### Caching Self

Cache self by id.
```rb
class User < ActiveRecord::Base
  cache_self
end

@user = User.cacher_at(user_id).self
```

Also support caching self by other columns.
```rb
class User < ActiveRecord::Base
  cache_self, by: :account
end

@user = User.cacher_at('khiav').self_by_account
```

### Caching Attributes

```rb
class Profile < ActiveRecord::Base
  cache_at :point
end

@point = Profile.cacher_at(profile_id).point
```

## Options

### :expire_by

Monitor on the specific model. Clean the cached objects if target are changed.

  - if empty, e.g. `nil` or `''`: Monitoring nothing.

  - if string, e.g. `User`: Monitoring all attributes of `User`.

  - if string with keyword `#`, e.g. `User#last_login_in_at`: Monitoring only the specific attribute.

  - if symbol, e.g. `:posts`: Monitoring on the association. It will trying to do all the things for you, including monitoring all attributes of `Post` and set the `foreign_key`.

  - Default value depends on the `name`. If is an association, monitoring the association klass. If is an attribute, monitoring current klass and the attrribute name. If others, monitoring nothing.

### :on

 Fire changes only by a certain action with the `on` option. Like the same option of [after_commit](https://apidock.com/rails/ActiveRecord/Transactions/ClassMethods/after_commit).

  - if `:create`: Clean the cache only on new record is created, e.g. `Model.create`.

  - if `:update`: Clean the cache only on the record is updated, e.g. `model.update`.

  - if `:destroy`: Clean the cache only on the record id destroyed, e.g. `model.destroy`, `model.delete`.

  - if `array`, e.g. `[:create, :update]`: Clean the cache by any of specified actions.

  - Default value is `[:create, :update, :destroy]`

### :foreign_key

This option is needed only for caching assoication and need not to set if [`expire_by`](#expire_by) is set to monitor association. Used for backtracing the cache key from cached objects. For examle, if `user` has_many `posts`, and cached the `posts` by user.id. When a post is changed, it needs to know which column to use (in this example, `user_id`) to clean the cache at user.

  - Default value is `:id`

  - Will be automatically determined if [`expire_by`](#expire_by) is symbol.

### :primary_key

This option is needed to know which attribute should be passed to the parameter when you are using instance cacher. For example, if a query, named `email_valid?`, uses `user.email` as parameter, and you call it from instance: `user.cacher.email_valid?`. You need to tell it to pass `user.email` instead of `user.id` as the argument.

  - Default value is `:id`