:binary
:boolean
:date
:datetime
:decimal
:float
:integer
:primary_key
:string
:text
:time
:timestamp
# refer to rails generate model -h
# http://blog.ifyouseewendy.com/blog/2015/08/10/data-types-in-rails/
rails console> ActiveRecord::Base.connection.native_database_types.keys
ActiveRecord::Relation
object allows us to chain queries together and this Relation class is at the heart of the new query syntax.
Finder methods that return a collection, such as where and group, return an instance of ActiveRecord::Relation
. Methods that find a single entity, such as find
and first
, return a single instance of the model.
Product.find([1,2]).class
==> Array
Product.where(id:[1,2]).class
==>Product::ActiveRecord_Relation
Product.where(id:[1,2]).to_a.class
=> Array
When a table has a reference of other table(foreign key), the model has belongs_to
Association
create_table "groups", force: :cascade do |t|
t.string "name"
t.bigint "user_id", null: false
t.index ["user_id"], name: "index_groups_on_user_id"
end
create_table "users", force: :cascade do |t|
t.string "name", default: "", null: false
t.string "email", default: "", null: false
end
class Group < ApplicationRecord
belongs_to :user
end
class User < ApplicationRecord
has_many :groups, dependent: :destroy
end
The model class name should follow the Ruby conventions, using the CamelCase form, while the table name must contain the words separated by underscores
# model table
Article articles
LineItem line_items
Deer deers
Mouse mice
Person people
joins/includes
method, always use the exact same name as the relation- In the
where
clauses, always use the pluralized name of the relation (actually, the table's name, which is by default the model name in plural but can also be manually set)
User has_many :posts
Post belongs_to :user
# Usage of joins/includes & where:
User.includes(:posts).where(posts: { name: 'BlogPost #1' })
#^ ^
Post.joins(:user).where(users: { name: 'Little Boby Table' })
#^^ ^
joins
using INNER JOIN
# Multiple joins:
Article.joins(:category, :comments)
SELECT articles.* FROM articles
INNER JOIN categories ON categories.id = articles.category_id
INNER JOIN comments ON comments.article_id = articles.id
Article.joins(comments: :guest)
SELECT articles.* FROM articles
INNER JOIN comments ON comments.article_id = articles.id
INNER JOIN guests ON guests.comment_id = comments.id
Category.joins(articles: [{ comments: :guest }, :tags])
SELECT categories.* FROM categories
INNER JOIN articles ON articles.category_id = categories.id
INNER JOIN comments ON comments.article_id = articles.id
INNER JOIN guests ON guests.comment_id = comments.id
INNER JOIN tags ON tags.article_id = articles.id
User.joins("LEFT JOIN bookmarks ON bookmarks.bookmarkable_type = 'Post' AND bookmarks.user_id = users.id")
SELECT "users".* FROM "users" LEFT JOIN bookmarks ON bookmarks.bookmarkable_type = 'Post' AND bookmarks.user_id = users.id
includes will either use a separate query (like above) or a LEFT OUTER JOIN. If you have a where or order clause that references a relationship, a LEFT OUTER JOIN is used versus a separate query.
Person.
includes(:manager).
references(:manager).
where(
"managers_people.id != ? OR managers_people.id IS NULL",
Person.find_by!(name: "Eve")
)
=> SELECT "people"."id" AS t0_r0, "people"."name" AS t0_r1, "people"."role_id" AS t0_r2,
"people"."location_id" AS t0_r3, "people"."manager_id" AS t0_r4, "managers_people"."id" AS t1_r0,
"managers_people"."name" AS t1_r1, "managers_people".role_id AS t1_r2,
"managers_people"."location_id" AS t1_r3, "managers_people"."manager_id" AS t1_r4
FROM "people"
LEFT OUTER JOIN "people" "managers_people"
ON "managers_people"."id" = "people"."managers_id"
WHERE ("managers_people"."id" != 1 OR "managers_people"."id" IS NULL)
# Rails Joins Association Condition
Product.joins(:store).where(stores: {title: "Apple"})
Product.joins(:store).where("stores.title = 'Apple'")
Book.where("title ILIKE ?", "%#{params[:q]}%")
belongs_to
associations are now automatically required: true
All model's belongs_to associations are now treated as if the required: true option is present.
So if the association is optional, that must be declared, or validation will fail without the presence of the associated model record.
class Post < ApplicationRecord
belongs_to :user, optional: true
end
post = Post.create(title: 'Hi')
=> <Post id: 2, title: "Hi", user_id: nil>
Rails.application.config.active_record.belongs_to_required_by_default = false
class Post < ApplicationRecord
belongs_to :user
end
post = Post.create(title: 'Hi')
=> <Post id: 3, title: "Hi", user_id: nil, created_at: "2016-02-11 12:36:05", updated_at: "2016-02-11 12:36:05">
class Person < ActiveRecord::Base
belongs_to :role
end
# roles have a name and a boolean column called billable.
class Role < ActiveRecord::Base
has_many :people
end
# We're hitting the database potentially hundreds or thousands or millions of times (for each person, we're making a request to the roles table)
Person.all.select { |person| person.role.billable? }
Person.all.joins(:role).where(roles: { billable: true })
#------------------------------------------------------------
class Role < ActiveRecord::Base
def self.billable
where(billable: true)
end
end
Person.joins(:role).merge(Role.billable)
class Person < ActiveRecord::Base
def self.billable
joins(:role).merge(Role.billable)
end
end
Person.billable
SELECT "people".*
FROM "people"
INNER JOIN "roles"
ON "roles.id" = "people"."role_id"
WHERE "roles"."billable" = 't';
class Person < ActiveRecord::Base
belongs_to :location
belongs_to :role
end
class Role < ActiveRecord::Base
has_many :people
end
class Location < ActiveRecord::Base
has_many :people
belongs_to :region
end
class Region < ActiveRecord::Base
has_many :locations
end
# data could be duplicated because of has_many
Location.joins(people: :role).where(roles: { billable: true })
Location.joins(people: :role).where(roles: { billable: true }).distinct
# SELECT DISTINCT "locations".*
# FROM "locations"
# INNER JOIN "people"
# ON "people"."location_id" = "locations"."id"
# INNER JOIN "roles"
# ON "roles"."id" = "people"."role_id"
# WHERE "roles"."billable" = 't';
# order by region name, then by location name
Location.joins(:region).merge(Region.order(:name)).order(:name)
# SELECT "locations".*
# FROM "locations"
# INNER JOIN "regions"
# ON "regions"."id" = "locations"."region_id"
# ORDER BY "regions"."name" ASC, "locations"."name" ASC;
# --------------------------
class Location < ActiveRecord::Base
has_many :people
def self.billable
joins(people: :role).where(roles: { billable: true }).distinct
end
def self.by_region_and_location_name
joins(:region).merge(Region.order(:name)).order(:name)
end
end
# order the billable locations by region name, then by location name
Location.billable.by_region_and_location_name
# SELECT DISTINCT "locations".*
# FROM "locations"
# INNER JOIN "people"
# ON "people"."location_id" = "locations"."id"
# INNER JOIN "roles"
# ON "roles"."id" = "people"."role_id"
# INNER JOIN "regions"
# ON "regions"."id" = "locations"."region_id"
# WHERE "roles"."billable" = 't'
# ORDER BY "regions"."name" ASC, "locations"."name" ASC;
# PG::InvalidColumnReference: ERROR: for SELECT DISTINCT, ORDER BY expressions must
# appear in select list
# LINE 1: ...gion_id" WHERE "roles"."billable" = 't' ORDER BY "regions"
# it's an order of operations issue: we have to make sure that when we eliminate some rows with DISTINCT
# we don't lose important information that we needed for ordering.
# So we need to be explicit about how to end up with distinct things first, and then order them.
Location.from(Location.billable, :locations)
# SELECT "locations".*
# FROM (
# SELECT DISTINCT "locations".*
# FROM "locations"
# INNER JOIN "people"
# ON "people"."location_id" = "locations"."id"
# INNER JOIN "roles"
# ON "roles"."id" = "people"."role_id"
# WHERE "roles"."billable" = 't'
# ) locations;
Location.from(Location.billable, :locations).by_region_and_location_name
# SELECT "locations".*
# FROM (
# SELECT DISTINCT "locations".*
# FROM "locations"
# INNER JOIN "people"
# ON "people"."location_id" = "locations"."id"
# INNER JOIN "roles"
# ON "roles"."id" = "people"."role_id"
# WHERE "roles"."billable" = 't'
# ) locations
# INNER JOIN "regions"
# ON "regions"."id" = "locations"."region_id"
# ORDER BY "regions"."name" ASC, "locations"."name" ASC;
# optional: true because manager_id could be nil
# belongs_to will now trigger a validation error by default if the association is not present.
# We can turn this off on a per-association basis with optional: true
class Person < ApplicationRecord
belongs_to :manager, class_name: "Person", optional: true
has_many :employees, class_name: "Person", foreign_key: :manager_id
end
class CreatePeople < ActiveRecord::Migration[6.0]
def change
create_table :people do |t|
t.references :manager
t.string "name", null: false
t.timestamps
end
end
end
Person.joins(:manager)
# ActiveRecord made up the alias managers_people
# SELECT "people".*
# FROM "people"
# INNER JOIN "people" "managers_people"
# ON "managers_people"."id" = "people"."manager_id"
Person.joins(:manager).where.not(managers_people: { id: Person.find_by!(name: "Eve") })
Person.
joins(<<~SQL).
LEFT JOIN people managers
ON managers.id = people.manager_id
SQL
where.
not(managers: { id: Person.find_by!(name: "Eve") })
# find all distinct locations with at least one person who belongs to a billable role, ordered by region name, then location name
Location.
from(
Location.
joins(people: :role).
where(roles: { billable: true }).
distinct,
:locations
).
joins(:region).
merge(Region.order(:name)).
order(:name)
# SELECT "locations".*
# FROM (
# SELECT DISTINCT "locations".*
# FROM "locations"
# INNER JOIN "people"
# ON "people"."location_id" = "locations"."id"
# INNER JOIN "roles"
# ON "roles"."id" = "people"."role_id"
# WHERE "roles"."billable" = 't'
# ) locations
# INNER JOIN "regions"
# ON "regions"."id" = "locations"."region_id"
# ORDER BY "regions"."name" ASC, "locations"."name" ASC;
Location.
joins(
"INNER JOIN (" +
Location.
joins(people: :role).
where(roles: { billable: true }).
distinct.
to_sql +
") billable_locations " \
"ON locations.id = billable_locations.id"
).
joins(:region).
merge(Region.order(:name)).
order(:name)
# SELECT "locations".*
# FROM "locations"
# INNER JOIN "regions"
# ON "regions"."id" = "locations"."region_id"
# INNER JOIN (
# SELECT DISTINCT "locations".*
# FROM "locations"
# INNER JOIN "people"
# ON "people"."location_id" = "locations"."id"
# INNER JOIN "roles"
# ON "roles"."id" = "people"."role_id"
# WHERE "roles"."billable" = 't'
# ) billable_locations
# ON "locations"."id" = "billable_locations"."id"
# ORDER BY "regions"."name" ASC, "locations"."name" ASC;
It's important to realize that the return value when you use group is a hash, with the keys being the unique values you are grouping by, and the values are the aggregation. count, sum, average, etc just returned scalar values.
Product.select("stores.name, average(:price)").joins(:store).group("stores.name").average(:price)
# SELECT AVG("products"."price") AS average_price, stores.name AS stores_name
# FROM "products" INNER JOIN "stores" ON "stores"."id" = "products"."store_id" GROUP BY stores.name
=> {"Samsung"=>0.7e4, "Apple"=>0.3e4}
# since the :employees association is not named conventionally (i.e., it's not named after the model),
# ActiveRecord makes up an alias for the table that it can guarantee won't be ambiguous.
Person.joins(:employees).group("people.name").count("employees_people.id")
# SELECT people.name, COUNT(employees_people.id)
# FROM "people"
# INNER JOIN "people" "employees_people"
# ON "employees_people"."manager_id" = "people"."id"
# GROUP BY people.name;
Person.
joins("LEFT JOIN people employees ON employees.manager_id = people.id").
group("people.name").
count("employees.id")
# SELECT people.name, COUNT(employees.id)
# FROM "people"
# LEFT JOIN people employees
# ON employees.manager_id = people.id GROUP BY people.name;
# find people with lower than average salaries at their location:
Person.
joins(
"INNER JOIN (" +
Person.
select("location_id, AVG(salary) as average").
group("location_id").
to_sql
") salaries " \
"ON salaries.location_id = people.location_id"
).
where("people.salary < salaries.average")
# SELECT "people".*
# FROM "people"
# INNER JOIN (
# SELECT location_id, AVG(salary) as average
# FROM "people"
# GROUP BY "people"."location_id"
# ) salaries
# ON salaries.location_id = people.location_id
# WHERE (people.salary < salaries.average)
# integer Integer - store positive or negative integers
# float Float - store decimal places
# decimal BigDecimal - stores decimal places more precisely than float (e.g. for currency)
# money BigDecimal
# precision and scale only can be applied on decimal
# ArgumentError: Error adding decimal column: precision cannot be empty if scale is specified
# money is always with scale: 2, and user cannot apply scale and precision
# If scale is applied on a attribute, attribute is automatically round up.
# for example, price attritue is money type
proudct.price = 0.249
proudct.price
=>0.249
proudct.save!
proudct = Product.last
proudct.price
=>0.25e0
#decimal type with scale, round up instantly happen
# if decima_price is decimal type with scale: 2
proudct.decima_price = 0.249
proudct.decima_price
=>0.25e0
# do not save string directly with commma, $
Product.create(price:"10,000")
=>0.1e2
#find the highest price product, ordered by name
Product.order(price: :desc).limit(3).sort_by(&:name)
Product.order(price: :desc).limit(3).sort_by(&:name).reverse
# this is using ruby sort_by
Product.
joins(
"INNER JOIN (" +
Product.select("id, rank() OVER (ORDER BY price DESC)").to_sql +
") prices ON prices.id = products.id"
).where("prices.rank <= 3").order(:name)
# SELECT "products".* FROM "products"
# INNER JOIN (SELECT id, rank() OVER (ORDER BY price DESC) FROM "products") prices
# ON prices.id = products.id WHERE (prices.rank <= 3)
# ORDER BY "products"."name" ASC
Returns true if the record is persisted, i.e. it’s not a new record
u=User.new
u.new_record? => true
u.persisted? => false
u = User.findbyemail('[email protected]')
u.persisted? => true
# Create a single new object
User.create(:first_name => 'Jamie')
# Create an Array of new objects
User.create([{ :first_name => 'Jamie' }, { :first_name => 'Jeremy' }])
# Create a single object and pass it into a block to set other attributes.
User.create(:first_name => 'Jamie') do |u|
u.is_admin = false
end
# Creating an Array of new objects using a block, where the block is executed for each object:
User.create([{ :first_name => 'Jamie' }, { :first_name => 'Jeremy' }]) do |u|
u.is_admin = false
end
automatically save into database
# save product to database
st.products << Product.new(title:"test", price:23.54)
pluck
returns array, but select
returns ActiveRecord::Relation
Product.all.pluck(:id)
=> [1, 2]
Product.select(:id)
=> #<ActiveRecord::Relation [#<Product id: 1>, #<Product id: 2>]>
p = Product.select("price + price as dobule_price").last
p.dobule_price
=> 0.21e2
#it can be also used just like Array#select. the return is not Relation, but Array
Product.select { |p| p.title != nil }
=> [#<Product id: 1, price: nil, price_precision: nil...>]
Product.select(&:title)
Product.all.pluck(:id)
SELECT "products"."id" FROM "products"
=> [1, 2]
Product.all.map(&:id)
SELECT "products".* FROM "products"
=> [1, 2]
Product.all.map { |p| p.slice(:id,:title) }
SELECT "products".* FROM "products"
=> [{"id"=>123, "title"=>"Chicken"}, {"id"=>127, "title"=>"Chicken"},..]
Product.pluck(:id, :title).map { |id, title| {id: id, title: title}}
SELECT "products"."id", "products"."title" FROM "products"
=> [{"id"=>123, "title"=>"Chicken"}, {"id"=>127, "title"=>"Chicken"},..]
Product.all.as_json only:[:id, :title]
=> [{"id"=>123, "title"=>"Chicken"}, {"id"=>127, "title"=>"Chicken"},..]
Product.first.attributes
=> {"id"=>121,"price"=>0.3e4,...}
Product.first.as_json
=> {"id"=>121,"price"=>"3000.0",...}
Product.select(:price).group(:price).map(&:attributes)
=> [{"id"=>nil, "price"=>0.3e4}, {"id"=>nil, "price"=>0.8e4}, {"id"=>nil, "price"=>0.6e4}]
Refer : https://zaiste.net/rails_pluck_to_hash/
size
or count
causes database query in the the associated records.
counter_cache
prevents n+1 query.
# Bad smell
Article.all.each do |article|
article.responses.size ## => will lead to a SELECT COUNT(*) query
end
# Use counter_cache
class Response
belongs_to :article, counter_cache: true
end
$ rails generate migration AddResponsesCountToArticles responses_count:integer
article.responses.size ##=> There is no query
https://blog.appsignal.com/2018/06/19/activerecords-counter-cache.html
Product.where("title = ? AND price = ?", "car", 10)
Product.where("title = :name AND price = :value", name: "car", value: 10)
Product.where(title: "car", price:10)
product = Product.select("*", "CAST(money_price as decimal) + price as total_price").first
product.total_price
Store.select("products.id, products.title").joins(:products)
# SELECT products.id, products.title FROM "stores" INNER JOIN "products" ON "products"."store_id" = "stores"."id"
Automagical ?-methods for boolean attributes in ActiveRecord
create_table "users", :force => true do |t|
t.boolean "email_confirmed"
end
# you may now say:
User.email_confirmed?
gem 'rails-erd', group: :development
brew install graphviz
bundle exec erd
https://thoughtbot.com/upcase/videos/advanced-querying-custom-joins