Skip to content

Commit

Permalink
fix: rails | AR queries | enums (#29099)
Browse files Browse the repository at this point in the history
implement changes suggested during review

* drop array model declaration
* rearrange lesson content
  * move up migrations
  * move down equivalent scopes
* use more pertinent example
  * light_switch => article (draft, published)
* unwrap lines
  * my editor defaulted to 80 char wrapping.
  * unwrapped all lines in enums/modified sections
* drop mention of/link to discrete values
* clarify enum definition
  * typically, not always, an int
  • Loading branch information
sean-garwood committed Dec 2, 2024
1 parent 5a66c96 commit d9af9ec
Showing 1 changed file with 41 additions and 61 deletions.
102 changes: 41 additions & 61 deletions ruby_on_rails/advanced_forms_and_activerecord/active_record_queries.md
Original file line number Diff line number Diff line change
Expand Up @@ -211,86 +211,73 @@ How much do you need to understand or care about scopes? In the early going, you

### Enums

Enums (short for "enumerations") map symbolic names to integer constants. They
make code more readable and maintainable, and they offer a performance boost
since queries involving integers are faster than those involving strings.
Enums (short for "enumerations") map a database column, typically an n integer, to a set of symbolic names. They make code more readable and maintainable, and they offer a performance boost since queries involving integers are faster than those involving strings.

Enums are perfect for representing the state of an attribute that has a [discrete
value](https://en.wikipedia.org/wiki/Continuous_or_discrete_variable). As an
example, consider a light switch: it is either in an on or an off state. It is
never between these two states, and it is never both on and off.
Enums are perfect for representing the state of an attribute that has a discrete value. As an example, suppose an article on a blog can be in a `draft` or `published` state. It is never between these two states, and it is never in more than one at any time.

#### How to use enums

To implement `enum`s, we need to declare them in the model and add a column to
store them in the database table that stores instances of the model.
To implement `enum`s, we need to declare them in the model and add a column to store them in the database table that stores instances of the model.

Enums are declared in the model's class as either an array or a hash. The hash flavor has a slight advantage
in that the integer values mapped to the `enum` are independent of their
position and the values stored in the database are explicitly stated.
Since enums are stored as integers in the database and there is no native `enum` type available in most database schemas, we set their data type to `integer`:

```ruby
# Array declaration
class LightSwitch < ApplicationRecord
belongs_to :room
enum :status, [:off, :on] # Value depends on index of symbol
end
```bash
# If building a new model, pass `column_name:integer` to the model generator
bin/rails g model Article status:integer

# If adding a column to an existing table:
bin/rails g migration AddStatusToLightSwitch status:integer
```

Enums are declared in the model's class as either an array or a hash. The hash flavor has a slight advantage in that the integer values mapped to the `enum` are independent of their position and the values stored in the database are explicitly stated.

```ruby
# Hash declaration (recommended)
class LightSwitch < ApplicationRecord
belongs_to :room
enum :status, { off: 0, on: 1 } # Value independent of position; explicit
class Article < ApplicationRecord
belongs_to :author
enum :status, { draft: 0, published: 1 }
end
```

These are functionally equivalent to the following [scopes](#scopes):
Articles can now be passed a status during creation:

```ruby
scope :on, -> { where(status: "on") }
scope :off, -> { where(status: "off") }
article = Article.create(status: :draft)
```

And, just like scopes, this exposes a number of class methods that return
collections:
Defining an enum on a model exposes a number of class methods that return collections:

```ruby
LightSwitch.on
LightSwitch.not_on
LightSwitch.off
LightSwitch.not_off
Article.draft
Article.not_draft
Article.published
Article.not_published
```

A number of handy instance methods are also exposed:
These are functionally equivalent to the following [scopes](#scopes):

```ruby
switchy = LightSwitch.new(status: :off)
scope :draft, -> { where(status: "draft") }
scope :not_draft, -> { where.not(status: "draft")}
scope :published, -> { where(status: "published") }
scope :not_published, -> { where.not(status: "published") }
```

A number of handy instance methods are also exposed:

```ruby
# Predicates
switchy.on? #=> false
switchy.off? #=> true
article.draft? #=> true
article.published? #=> false

# Getters
switchy.status #=> "off"
article.status #=> "draft"

# Setters
switchy.status = "on" #=> "on"
switchy.off! #=> "off"
```

Since enums are stored as integers in the database and there is no native `enum` type available in most database schemas, we set their data type to `integer`:

```bash
# If building a new model, pass `column_name:integer`
# to the model generator
bin/rails g model LightSwitch status:integer

# If adding a column to an existing table:
bin/rails g migration AddToLightSwitch status:integer
article.status = "published" #=> "published"
article.draft! #=> "draft"
```

Importantly, changing the model declaration **will not change** values that have
already been stored in the database; you must update them yourself.

### Bare-metal SQL

Sometimes, you just can't get ActiveRecord to do what you want it to. In that case, it gives you an interface to the bare metal SQL so you can just type in your query as desired. This should really be a last resort -- it's basically hard-coding your application code. Use the `#find_by_sql` method for this.
Expand All @@ -314,14 +301,8 @@ This was a lot of material, but you should have a healthy appreciation for the b

#### Advanced querying

1. Read section 14 in the [Rails Guide on
Querying](https://guides.rubyonrails.org/active_record_querying.html#scopes)
for a look at scopes. Again, you don't necessarily need to memorize all the
details of scopes, but you should understand the concept and when it might be
useful.
1. Read [How to Use Enums in
Rails](https://blog.saeloun.com/2022/01/05/how-to-use-enums-in-rails/).
1. Read the [docs for ActiveRecord::Enum](https://api.rubyonrails.org/classes/ActiveRecord/Enum.html).
1. Read section 14 in the [Rails Guide on Querying](https://guides.rubyonrails.org/active_record_querying.html#scopes) for a look at scopes. Again, you don't necessarily need to memorize all the details of scopes, but you should understand the concept and when it might be useful.
1. Read [How to Use Enums in Rails](https://blog.saeloun.com/2022/01/05/how-to-use-enums-in-rails/).
1. Read section 19 of the same Rails guide for a look at [using SQL directly to query](http://guides.rubyonrails.org/active_record_querying.html#finding-by-sql).

</div>
Expand Down Expand Up @@ -351,5 +332,4 @@ This section contains helpful links to related content. It isn't required, so co
- [N+1 Problem: Optimized Counts with Joins and Custom Select](https://www.youtube.com/watch?v=rJg3I-leoo4)
- [Speed up ActiveRecord with a little tweaking](https://blog.codeship.com/speed-up-activerecord/)
- [A useful gem that identifies N+1 queries](https://github.com/flyerhzm/bullet)
- [ActiveRecord::Enum API
docs](https://api.rubyonrails.org/classes/ActiveRecord/Enum.html)
- [ActiveRecord::Enum API docs](https://api.rubyonrails.org/classes/ActiveRecord/Enum.html)

0 comments on commit d9af9ec

Please sign in to comment.