Home > other >  What does this syntax using "on:" mean in Ruby on Rails?
What does this syntax using "on:" mean in Ruby on Rails?

Time:12-22

This is really hard to do a google search about because I have no idea if it's a Ruby thing or a Rails thing and google does not do a good job searching for "on"

In a file that looks like so

# app/models/concerns/searchable.rb
module Searchable
  extend ActiveSupport::Concern

  included do
    include Elasticsearch::Model
    include Elasticsearch::Model::Callbacks

    # Every time our entry is created, updated, or deleted, we update the index accordingly.
    after_commit on: %i[create update] do
      __elasticsearch__.index_document
    end

    after_commit on: %i[destroy] do
      __elasticsearch__.delete_document
    end

    # We serialize our model's attributes to JSON, including only the title and category fields.
    def as_indexed_json(_options = {})
      as_json(only: %i[title category])
    end

    # Here we define the index configuration
    settings settings_attributes do
      # We apply mappings to the title and category fields.
      mappings dynamic: false do
        # for the title we use our own autocomplete analyzer that we defined below in the settings_attributes method.
        indexes :title, type: :text, analyzer: :autocomplete
        # the category must be of the keyword type since we're only going to use it to filter articles.
        indexes :category, type: :keyword
      end
    end

    def self.search(query, filters)
      # lambda function adds conditions to the search definition.
      set_filters = lambda do |context_type, filter|
        @search_definition[:query][:bool][context_type] |= [filter]
      end

      @search_definition = {
        # we indicate that there should be no more than 5 documents to return.
        size: 5,
        # we define an empty query with the ability to dynamically change the definition
        # Query DSL https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl.html
        query: {
          bool: {
            must: [],
            should: [],
            filter: []
          }
        }
      }

      # match all documents if the query is blank.
      if query.blank?
        set_filters.call(:must, match_all: {})
      else
        set_filters.call(
          :must,
          match: {
            title: {
              query: query,
              # fuzziness means you can make one typo and still match your document.
              fuzziness: 1
            }
          }
        )
      end

      # the system will return only those documents that pass this filter
      set_filters.call(:filter, term: { category: filters[:category] }) if filters[:category].present?

      # and finally we pass the search query to Elasticsearch.
      __elasticsearch__.search(@search_definition)
    end
  end

  class_methods do
    def settings_attributes
      {
        index: {
          analysis: {
            analyzer: {
              # we define a custom analyzer with the name autocomplete.
              autocomplete: {
                # type should be custom for custom analyzers.
                type: :custom,
                # we use a standard tokenizer.
                tokenizer: :standard,
                # we apply two token filters.
                # autocomplete filter is a custom filter that we defined above.
                # and lowercase is a built-in filter.
                filter: %i[lowercase autocomplete]
              }
            },
            filter: {
              # we define a custom token filter with the name autocomplete.

              # Autocomplete filter is of edge_ngram type. The edge_ngram tokenizer divides the text into smaller parts (grams).
              # For example, the word “ruby” will be split into [“ru”, “rub”, “ruby”].

              # edge_ngram is useful when we need to implement autocomplete functionality. However, the so-called "completion suggester" - is another way to integrate the necessary options.
              autocomplete: {
                type: :edge_ngram,
                min_gram: 2,
                max_gram: 25
              }
            }
          }
        }
      }
    end
  end
end

I am not sure what after_commit on: %i[create update] do is supposed to mean. I managed to find this information https://apidock.com/rails/ActiveRecord/Transactions/ClassMethods/after_commit which gives me an idea of how to use this sytax. But I'm still not sure how this syntax "on:" is created. It doesn't seem like a Ruby thing. It seems like a Rails shorthand for something but what exactly is it?

On a separate note, is there any source that lists all the shorthands that Rails provides? It's such a pain to figure out if something is a Rails shorthand or if it's a Ruby syntax.

CodePudding user response:

Let's dissect the various parts of the syntax in

after_commit on: %i[create update] do
  # ...
end

At first, about the array at the end. It uses Ruby's %-syntax to create an object, in this case an array of Symbols. %i[create update] is thus equivalent to [:create, :update]

There are various options to use this %-syntax to create various objects, e.g. %[foo] is equivalent to "foo" and %w[foo bar] is equivalent to ["foo", "bar"]. The delimiter characters are arbitrary here. Instead of [ and ], you can also use { and }, or even something like %i|foo bar| or %i.foo bar`. See the syntax documentation for details.

Second, the on:. Here, you are passing the keyword argument on to the after_commit method and pass the array of Symbols to it. Keyword arguments are kind-of similar to regular positional arguments you can pass to methods, you just pass the argument values along with the names, similar to a Hash.

Historically, Ruby supported passing a Hash as the last argument to a method and allowed omitting the braces there (so that you could use after_commit(on: [:create, :update]) rather than after_commit({:on => [:create, :update]}). Ruby's keyword arguments (which were first introduced in Ruby 2.0) build upon this syntax and refined the semantics a bit along the way. For the most part, it still works the same as when passing a Hash with Symbol keys to a method though. Note that different to e.g. Python. positional arguments and keyword arguments can not be arbitrarily mixed.

You can learn more about keyword arguments (and regular positional arguments) at the respective documentation.

The method call is thus equivalent to:

after_commit(on: [:create, :update]) do
  # ...
end

The on ... end part is a block which is passed to the after_commit method, another Ruby syntax. This allows to pass a block of code to a method which can do something with this, similar to how you can pass anonymous functions around in Javascript. Blocks are used extensively in Ruby so it's important to learn about them.

Finally, the after_commit method and its associated behavior is defined by Rails. It is used to register callbacks which are run on certain events (in this case, to run some code after the database transaction successfully was committed in which the current ActiveRecord model was created or updated).

This is described in documentation about ActiveRecord callbacks.

CodePudding user response:

after_commit is triggered on create, update, destroy

If you want to run the callback only on specific actions you can make use of :on

Example:

after_commit :calculate_total, on: [:create, :update]

private 

def calculate_total
  update(total: subtotal   tax)
end

Given the above snippet, it makes sense to calculate the total only on create and update but not on destroy

Read more here - https://apidock.com/rails/ActiveRecord/Transactions/ClassMethods/after_commit

CodePudding user response:

With "on:" you can specify the ActiveRecord actions which will trigger this callback method.

Callbacks can also be registered to only fire on certain life cycle events

class User < ApplicationRecord
  before_validation :normalize_name, on: :create

  # :on takes an array as well
  after_validation :set_location, on: [ :create, :update ]

  private
    def normalize_name
      self.name = name.downcase.titleize
    end

    def set_location
      self.location = LocationService.query(self)
    end
end

You can check out more from here

CodePudding user response:

Try the following code in the console if you can:

def fn_args(*args, &block)
  puts 'args: '   args.inspect
  puts 'block: '   block.inspect
  block.call
end

fn_args on: %i[create update] do
  puts 'Heya from passed block'
end

I get:

args: [{:on=>[:create, :update]}]
block: #<Proc:0x0000020bc91c6a98@(irb):21>
Heya from passed block
=> nil

What does this mean?

fn_args is created with the same function signature as after_commit, we can see (I think) that on: %i[create update] is getting treated as a generic hash as part of the function arguments. It's not a special language feature (Ruby or Rails), though the interpretation of on: %i[create update] as a hash (or part of a hash) is a special feature of Ruby and the way it handles function arguments.

We could rewrite our call to:

fn_args({ on: %i[create update] }) do
  puts 'Heya from passed block'
end

and it should work the same way.

  • Related