Effective ActiveRecord
Sam Goldman
@nontrivialzeros
http://github.com/samwgoldman
sam@smartlogic.io

Wednesday, December 18, 13
Review: Models
class User < ActiveRecord::Base
end
foo = User.find(1)
foo.name # "Foo"
foo.email # "foo@example.com"
bar = User.find(2)
bar.name # "Bar
bar.email # "bar@example.com"

Wednesday, December 18, 13

users
id

email

name

1 foo@example.com Foo
2 bar@example.com Bar
Review: Has Many
class Project < AR::Base
has_many :members
end
class Member < AR::Base
belongs_to :project
end
foo_project = Project.find(1)
foo_project.name
# "Foo project"
foo_project.members.
map(&:email)
# ["foo@example.com",
# "bar@example.com"]

Wednesday, December 18, 13

projects
id

name

1 Foo project
2 Bar project

members
id project_id

email

1

1

foo@example.com

2

1

bar@example.com

3

2

baz@example.com

4

2

quux@example.com
Review: Belongs To
class Project < AR::Base
has_many :members
end
class Member < AR::Base
belongs_to :project
end
foo = Member.find(1)
foo.email
# "foo@example.com"
foo.project.name
# "Foo project"

Wednesday, December 18, 13

projects
id

name

1 Foo project
2 Bar project

members
id project_id

email

1

1

foo@example.com

2

1

bar@example.com

3

2

baz@example.com

4

2

quux@example.com
Review: Has Many Through
class User < AR::Base
has_many :members
has_many :projects,
through: :members
end
class Project < AR::Base
has_many :members
end
class Member < AR::Base
belongs_to :user
belongs_to :project
end
foo = User.find(1)
foo.projects.map(&:name)
# ["Foo project",
# "Bar project"]

Wednesday, December 18, 13

users
id

email

name

1 foo@example.com Foo

projects
id

name

1 Foo project
2 Bar project

members
id project_id

user_id

1

1

1

3

2

1
Creating Records
project = Project.create(name: "Project")
(0.3ms) BEGIN
SQL (1.5ms) INSERT INTO "projects" ("name") VALUES ($1)
RETURNING "id"
[["name", "Project"]]
(0.4ms) COMMIT

user = User.create(name: "User", email: "user@example.com")
(0.3ms) BEGIN
SQL (1.3ms) INSERT INTO "users" ("email", "name") VALUES ($1, $2)
RETURNING "id"
[["email", "user@example.com"], ["name", "User"]]
(0.4ms) COMMIT

member = Member.create(user: user, project: project)
(0.5ms) BEGIN
SQL (3.7ms) INSERT INTO "members" ("project_id", "user_id") VALUES ($1, $2)
RETURNING "id"
[["project_id", 1], ["user_id", 1]]
(0.3ms) COMMIT

Wednesday, December 18, 13
Updating Records
project.update_attributes(name: "Updated Project")
(0.2ms) BEGIN
SQL (0.9ms) UPDATE "projects" SET "name" = $1
WHERE "projects"."id" = 1
[["name", "Updated Project"]]
(0.4ms) COMMIT

user.update_attributes(name: "Updated User")
(0.1ms) BEGIN
SQL (0.9ms) UPDATE "users" SET "name" = $1
WHERE "users"."id" = 1
[["name", "Updated User"]]
(0.4ms) COMMIT

Wednesday, December 18, 13
Autosave
class Member < ActiveRecord::Base
belongs_to :user
belongs_to :project
end
project = Project.new(name: "Project")
user = User.new(name: "User", email: "user@example.com")
member = Member.create(user: user, project: project)
Guess the result.

Wednesday, December 18, 13
Autosave
class Member < ActiveRecord::Base
belongs_to :user
belongs_to :project
end
project = Project.new(name: "Project")
user = User.new(name: "User", email: "user@example.com")
member = Member.create(user: user, project: project)
(0.4ms) BEGIN
SQL (2.7ms) INSERT INTO "users" ("email", "name") VALUES ($1, $2)
RETURNING "id"
[["email", "user@example.com"], ["name", "User"]]
SQL (1.2ms) INSERT INTO "projects" ("name") VALUES ($1)
RETURNING "id"
[["name", "Project"]]
SQL (3.5ms) INSERT INTO "members" ("project_id", "user_id") VALUES ($1, $2)
RETURNING "id"
[["project_id", 1], ["user_id", 1]]
(0.5ms) COMMIT

Wednesday, December 18, 13
Autosave
class Member < ActiveRecord::Base
belongs_to :user
belongs_to :project
end
member = Member.new
member.build_user(name: "User", email: "user@example.com")
member.build_project(name: "Project")
member.save
(0.4ms) BEGIN
SQL (2.7ms) INSERT INTO "users" ("email", "name") VALUES ($1, $2)
RETURNING "id"
[["email", "user@example.com"], ["name", "User"]]
SQL (1.2ms) INSERT INTO "projects" ("name") VALUES ($1)
RETURNING "id"
[["name", "Project"]]
SQL (3.5ms) INSERT INTO "members" ("project_id", "user_id") VALUES ($1, $2)
RETURNING "id"
[["project_id", 1], ["user_id", 1]]
(0.5ms) COMMIT

Wednesday, December 18, 13
Autosave
class Member < ActiveRecord::Base
belongs_to :user, autosave: false
belongs_to :project, autosave: false
end
member = Member.new
member.build_user(name: "User", email: "user@example.com")
member.build_project(name: "Project")
member.save
Guess the result.

Wednesday, December 18, 13
Autosave
class Member < ActiveRecord::Base
belongs_to :user, autosave: false
belongs_to :project, autosave: false
end
member = Member.new
member.build_user(name: "User", email: "user@example.com")
member.build_project(name: "Project")
member.save
PG::NotNullViolation:
ERROR: null value in column "user_id" violates not-null constraint
(ActiveRecord::StatementInvalid)
DETAIL: Failing row contains (1, null, null).
: INSERT INTO "members" DEFAULT VALUES RETURNING "id"

Wednesday, December 18, 13
Autosave
class Member < ActiveRecord::Base
belongs_to :user
belongs_to :project
end
member.user.name = "Updated User"
member.project.name = "Updated Project"
member.save
Guess the result.

Wednesday, December 18, 13
Autosave
class Member < ActiveRecord::Base
belongs_to :user
belongs_to :project
end
member.user.name = "Updated User"
member.project.name = "Updated Project"
member.save
(0.2ms) BEGIN
(0.2ms) COMMIT

Wednesday, December 18, 13
Autosave
class Member < ActiveRecord::Base
belongs_to :user, autosave: true
belongs_to :project, autosave: true
end
member.user.name = "Updated User"
member.project.name = "Updated Project"
member.save
Guess the result.

Wednesday, December 18, 13
Autosave
class Member < ActiveRecord::Base
belongs_to :user, autosave: true
belongs_to :project, autosave: true
end
member.user.name = "Updated User"
member.project.name = "Updated Project"
member.save
(0.2ms) BEGIN
SQL (1.1ms) UPDATE "users" SET "name" = $1
WHERE "users"."id" = 1
[["name", "Updated User"]]
SQL (1.2ms) UPDATE "projects" SET "name" = $1
WHERE "projects"."id" = 1
[["name", "Updated Project"]]
(0.4ms) COMMIT

Wednesday, December 18, 13
Autosave
class Project < ActiveRecord::Base
has_many :members
end
user = User.new(name: "User", email: "user@example.com")
project = Project.new(name: "Project")
project.members << Member.new(user: user)
project.save
Guess the result.

Wednesday, December 18, 13
Autosave
class Project < ActiveRecord::Base
has_many :members
end
user = User.new(name: "User", email: "user@example.com")
project = Project.new(name: "Project")
project.members << Member.new(user: user)
project.save
(0.7ms) BEGIN
SQL (1.6ms) INSERT INTO "projects" ("name") VALUES ($1)
RETURNING "id"
[["name", "Project"]]
SQL (1.2ms) INSERT INTO "users" ("email", "name") VALUES ($1, $2)
RETURNING "id"
[["email", "user@example.com"], ["name", "User"]]
SQL (3.4ms) INSERT INTO "members" ("project_id", "user_id") VALUES ($1, $2)
RETURNING "id"
[["project_id", 1], ["user_id", 1]]
(0.5ms) COMMIT

Wednesday, December 18, 13
Autosave
class Project < ActiveRecord::Base
has_many :members
end
user = User.new(name: "User", email: "user@example.com")
project = Project.new(name: "Project")
project.members.build(user: user)
project.save
(0.7ms) BEGIN
SQL (1.6ms) INSERT INTO "projects" ("name") VALUES ($1)
RETURNING "id"
[["name", "Project"]]
SQL (1.2ms) INSERT INTO "users" ("email", "name") VALUES ($1, $2)
RETURNING "id"
[["email", "user@example.com"], ["name", "User"]]
SQL (3.4ms) INSERT INTO "members" ("project_id", "user_id") VALUES ($1, $2)
RETURNING "id"
[["project_id", 1], ["user_id", 1]]
(0.5ms) COMMIT

Wednesday, December 18, 13
Autosave
class Project < ActiveRecord::Base
has_many :members, autosave: false
end
user = User.new(name: "User", email: "user@example.com")
project = Project.new(name: "Project")
project.members.build(user: user)
project.save
Guess the result.

Wednesday, December 18, 13
Autosave
class Project < ActiveRecord::Base
has_many :members, autosave: false
end
user = User.new(name: "User", email: "user@example.com")
project = Project.new(name: "Project")
project.members.build(user: user)
project.save
(0.4ms) BEGIN
SQL (1.6ms) INSERT INTO "projects" ("name") VALUES ($1)
RETURNING "id"
[["name", "Project"]]
(0.4ms) COMMIT

Wednesday, December 18, 13
Inverses
class Project < ActiveRecord::Base
has_many :tasks
end
class Task < ActiveRecord::Base
belongs_to :project
end
project = Project.new(name: "Project")
task = project.tasks.build
project.save
p project.object_id
p task.project.object_id
Guess the result.

Wednesday, December 18, 13
Inverses
class Project < ActiveRecord::Base
has_many :tasks
end
class Task < ActiveRecord::Base
belongs_to :project
end
project = Project.new(name: "Project")
task = project.tasks.build
project.save
p project.object_id
p task.project.object_id

Not just an
extra query. Split
brain!

70236648295560
Project Load (1.4ms) SELECT "projects".* FROM "projects"
WHERE "projects"."id" = $1
ORDER BY "projects"."id" ASC
LIMIT 1
[["id", 1]]
70236645304160

Wednesday, December 18, 13
Inverses
class Project < ActiveRecord::Base
has_many :tasks, inverse_of: :project
end
class Task < ActiveRecord::Base
belongs_to :project
end
project = Project.new(name: "Project")
task = project.tasks.build
project.save
p project.object_id
p task.project.object_id
Guess the result.

Wednesday, December 18, 13
Inverses
class Project < ActiveRecord::Base
has_many :tasks, inverse_of: :project
end
class Task < ActiveRecord::Base
belongs_to :project
end
project = Project.new(name: "Project")
task = project.tasks.build
project.save
p project.object_id
p task.project.object_id
70259515608140
70259515608140

Wednesday, December 18, 13
Summary
• Use autosave and inverse associations
• Inspect the generated SQL for sanity
• Avoid explicit transactions

Wednesday, December 18, 13
Authorization
class ProjectMembersController < ApplicationController
# POST /project/:project_id/members
def create
member = Member.create(member_params.merge(project_id: project_id))
respond_with member
end
private
def project_id
params.require(:project_id)
end
def member_params
params.require(:member).permit(:user_id)
end
end

Wednesday, December 18, 13
Authorization
class ProjectMembersController < ApplicationController
# POST /project/:project_id/members
def create
member = Member.create(member_params.merge(project_id: project_id))
respond_with member
end

Anyone can
def project_id
add any user to any
params.require(:project_id)
end
project!
private

def member_params
params.require(:member).permit(:user_id)
end
end

Wednesday, December 18, 13
Authorization
class ProjectMembersController < ApplicationController
# POST /project/:project_id/members
def create
member = current_user.create_project_member(project_id, member_params)
respond_with member
end
private
def project_id
params.require(:project_id)
end
def member_params
params.require(:member).permit(:user_id)
end
end
class User < ActiveRecord::Base
has_many :members
has_many :projects, through: :members

May only add
members to my own
projects.

def create_project_member(project_id, member_params)
project = projects.find(project_id)
project.members.create(member_params)
end
end

Wednesday, December 18, 13
Authorization
class ProjectMembersController < ApplicationController
# POST /project/:project_id/members
def create
member = current_user.build_project_member(project_id, member_params)
member.save
respond_with member
end
private
def project_id
params.require(:project_id)
end

Separate build vs.
create

def member_params
params.require(:member).permit(:user_id)
end
end
class User < ActiveRecord::Base
has_many :members
has_many :projects, through: :members
def build_project_member(project_id, member_params)
project = projects.find(project_id)
project.members.build(member_params)
end
end
Wednesday, December 18, 13
Authorization
class ProjectMembersController < ApplicationController
# POST /project/:project_id/members
def create
member = current_user.build_project_member(project_id, member_params)
member.save
respond_with member
end
private
def project_id
params.require(:project_id)
end
def member_params
params.require(:member).permit(:user_id)
end
end

What if I am
not a member of this
class User < ActiveRecord::Base
has_many :members
has_many :projects, through: :members
project?
def build_project_member(project_id, member_params)
project = projects.find(project_id)
project.members.build(member_params)
end
end
Wednesday, December 18, 13
Authorization
Couldn't find Project with id=1
(ActiveRecord::RecordNotFound)

Wednesday, December 18, 13
Authorization
class User < ActiveRecord::Base
has_many :members, inverse_of: :user
has_many :projects, through: :members
def build_project_member(project_id, member_params)
project = projects.find_one(project_id)
if project
project.members.build(member_params)
end
end
end

Wednesday, December 18, 13
Authorization
class User < ActiveRecord::Base
has_many :members, inverse_of: :user
has_many :projects, through: :members
def member(project_id)
members.find_by(project_id: project_id)
end
def build_project_member(project_id, member_params)
member = member(project_id)
if member
member.build_project_member(member_params)
end
end
end
class Member < ActiveRecord::Base
belongs_to :user, inverse_of: :members
belongs_to :project, inverse_of: :members
def build_project_member(member_params)
project.members.build(member_params)
end
end

Wednesday, December 18, 13
Authorization
class User < ActiveRecord::Base
has_many :members, inverse_of: :user
has_many :projects, through: :members
def member(project_id)
members.find_by(project_id: project_id)
end
def build_project_member(project_id, member_params)
member = member(project_id)
if member
member.build_project_member(member_params)
end
end
end
class Member < ActiveRecord::Base
belongs_to :user, inverse_of: :members
belongs_to :project, inverse_of: :members
def build_project_member(member_params)
if role == "admin"
project.members.build(member_params)
end
end
end

Wednesday, December 18, 13
Authorization
class ProjectMembersController < ApplicationController
# POST /project/:project_id/members
def create
member = current_user.build_project_member(project_id, member_params)
if member.nil?
# handle error
else
member.save
respond_with member
end
end
private
def project_id
params.require(:project_id)
end
def member_params
params.require(:member).permit(:user_id)
end
end

Wednesday, December 18, 13
Authorization
class ProjectMembersController < ApplicationController
# POST /project/:project_id/members
def create
member = current_user.build_project_member(project_id, member_params)
if member.nil?
# handle error
else
member.save
respond_with member
end
end
private

Which error
happened?

def project_id
params.require(:project_id)
end
def member_params
params.require(:member).permit(:user_id)
end
end

Wednesday, December 18, 13
Authorization
Failure = Struct.new(:error) do
def success?
false
end
end
Success = Struct.new(:value) do
def success?
true
end
end
class Member < ActiveRecord::Base
belongs_to :user, inverse_of: :members
belongs_to :project, inverse_of: :members
def build_project_member(member_params)
if role == "admin"
Success.new(project.members.build(member_params))
else
Failure.new(:not_authorized)
end
end
end

Wednesday, December 18, 13
Authorization
class User < ActiveRecord::Base
has_many :members, inverse_of: :user
has_many :projects, through: :members
def member(project_id)
member = members.find_by(project_id: project_id)
if member
Success.new(member)
else
Failure.new(:member_not_found)
end
end
def build_project_member(project_id, member_params)
result = member(project_id)
if result.success?
result.value.build_project_member(member_params)
else
result
end
end
end

Wednesday, December 18, 13
Authorization
class ProjectMembersController < ApplicationController
# POST /project/:project_id/members
def create
result = current_user.build_project_member(project_id, member_params)
if result.success?
member = result.value
member.save
respond_with member
else
result.error # handle error
end
end
private
def project_id
params.require(:project_id)
end
def member_params
params.require(:member).permit(:user_id)
end
end

Wednesday, December 18, 13
Authorization
project = Project.create(name: "Project")
alice = User.create(name: "Alice", email: "alice@example.com")
bob = User.create(name: "Bob", email: "bob@example.com")
p alice.build_project_member(project.id, {
user_id: bob.id,
role: "member"
})
#<struct Failure error=:member_not_found>

Wednesday, December 18, 13
Authorization
project = Project.create(name: "Project")
alice = User.create(name: "Alice", email: "alice@example.com")
bob = User.create(name: "Bob", email: "bob@example.com")
alice.members.create(project: project, role: "member")
p alice.build_project_member(project.id, {
user_id: bob.id,
role: "member"
})
#<struct Failure error=:not_authorized>

Wednesday, December 18, 13
Authorization
project = Project.create(name: "Project")
alice = User.create(name: "Alice", email: "alice@example.com")
bob = User.create(name: "Bob", email: "bob@example.com")
alice.members.create(project: project, role: "admin")
p alice.build_project_member(project.id, {
user_id: bob.id,
role: "member"
})
#<struct Success value=#<Member user_id: 2, project_id: 1, role: "member">>

Wednesday, December 18, 13
Summary
• Use the relations
• Move beyond ActiveRecord’s API
• Use result objects to represent possible
failures

• Separate building vs. creating APIs
Wednesday, December 18, 13
Refactoring
class User < ActiveRecord::Base
has_many :members
has_many :projects, through: :members
def member(project_id)
member = members.find_by(project_id: project_id)
if member
Success.new(member)
else
Failure.new(:member_not_found)
end
end
def build_project_member(project_id, member_params)
result = member(project_id)
if result.success?
result.value.build_project_member(member_params)
else
result
end
end
end

Wednesday, December 18, 13
Refactoring
class User < ActiveRecord::Base
has_many :members
has_many :projects, through: :members
def member(project_id)
member = members.find_by(project_id: project_id)
if member
Success.new(member)
else
Failure.new(:member_not_found)
end
end
def build_project_member(project_id, member_params)
result = member(project_id)
if result.success?
result.value.build_project_member(member_params)
else
result
end
end
end

Wednesday, December 18, 13

We need a way to
combine results.
Refactoring
Failure = Struct.new(:error) do
def success?
false
end
def map
self
end
def bind
self
end
end

Wednesday, December 18, 13

Success = Struct.new(:value) do
def success?
true
end
def map
Success.new(yield value)
end
def bind
yield value
end
end
Refactoring
class User < ActiveRecord::Base
has_many :members
has_many :projects, through: :members
def member(project_id)
member = members.find_by(project_id: project_id)
if member
Success.new(member)
else
Failure.new(:member_not_found)
end
end

Build compound
results.

def build_project_member(project_id, member_params)
member(project_id).bind do |member|
member.build_project_member(member_params)
end
end
end

Wednesday, December 18, 13
Serialize
class Member < ActiveRecord::Base
belongs_to :user, inverse_of: :members
belongs_to :project, inverse_of: :members
def build_project_member(member_params)
if role == "admin"
Success.new(project.members.build(member_params))
else
Failure.new(:not_authorized)
end
end
end

Wednesday, December 18, 13
Serialize
class Member < ActiveRecord::Base
belongs_to :user, inverse_of: :members
belongs_to :project, inverse_of: :members
serialize :role, Role
def build_project_member(member_params)
role.build_project_member(project, member_params)
end
end

Wednesday, December 18, 13
Serialize
class Role
Unknown = Object.new
def Unknown.name
nil
end
MAP = {}
MAP.default = Unknown
def self.load(name)
MAP[name]
end
def self.dump(role)
role.name
end
attr_reader :name
def initialize(name, &block)
@name = name
instance_eval(&block)
MAP[name] = self
end
end

Wednesday, December 18, 13
Serialize
class Role
Admin = Role.new("admin") do
def build_project_member(project, member_params)
Success.new(project.members.build(member_params))
end
end
Member = Role.new("member") do
def build_project_member(project, member_params)
Failure.new(:not_authorized)
end
end
Null = Role.new(nil) do
def build_project_member(project, member_params)
Failure.new(:missing_role)
end
end
def Unknown.build_project_member(project, member_params)
Failure.new(:unknown_role)
end
end

Wednesday, December 18, 13
Authorization
class ProjectMembersController < ApplicationController
# POST /project/:project_id/members
def create
result = current_user.build_project_member(project_id, member_params)
if result.success?
member = result.value
member.save
respond_with member
else
result.error # handle error
end
end
private
def project_id
params.require(:project_id)
end
def member_params
member_params = params.require(:member).permit(:user_id, :role)
role = Role.load(member_params[:role].presence)
member_params.merge(:role => role)
end
end

Wednesday, December 18, 13
Questions?
http://smartlogic.io
http://twitter.com/smartlogic
http://github.com/smartlogic
http://facebook.com/smartlogic

Wednesday, December 18, 13

Effective ActiveRecord

  • 1.
  • 2.
    Review: Models class User< ActiveRecord::Base end foo = User.find(1) foo.name # "Foo" foo.email # "foo@example.com" bar = User.find(2) bar.name # "Bar bar.email # "bar@example.com" Wednesday, December 18, 13 users id email name 1 foo@example.com Foo 2 bar@example.com Bar
  • 3.
    Review: Has Many classProject < AR::Base has_many :members end class Member < AR::Base belongs_to :project end foo_project = Project.find(1) foo_project.name # "Foo project" foo_project.members. map(&:email) # ["foo@example.com", # "bar@example.com"] Wednesday, December 18, 13 projects id name 1 Foo project 2 Bar project members id project_id email 1 1 foo@example.com 2 1 bar@example.com 3 2 baz@example.com 4 2 quux@example.com
  • 4.
    Review: Belongs To classProject < AR::Base has_many :members end class Member < AR::Base belongs_to :project end foo = Member.find(1) foo.email # "foo@example.com" foo.project.name # "Foo project" Wednesday, December 18, 13 projects id name 1 Foo project 2 Bar project members id project_id email 1 1 foo@example.com 2 1 bar@example.com 3 2 baz@example.com 4 2 quux@example.com
  • 5.
    Review: Has ManyThrough class User < AR::Base has_many :members has_many :projects, through: :members end class Project < AR::Base has_many :members end class Member < AR::Base belongs_to :user belongs_to :project end foo = User.find(1) foo.projects.map(&:name) # ["Foo project", # "Bar project"] Wednesday, December 18, 13 users id email name 1 foo@example.com Foo projects id name 1 Foo project 2 Bar project members id project_id user_id 1 1 1 3 2 1
  • 6.
    Creating Records project =Project.create(name: "Project") (0.3ms) BEGIN SQL (1.5ms) INSERT INTO "projects" ("name") VALUES ($1) RETURNING "id" [["name", "Project"]] (0.4ms) COMMIT user = User.create(name: "User", email: "user@example.com") (0.3ms) BEGIN SQL (1.3ms) INSERT INTO "users" ("email", "name") VALUES ($1, $2) RETURNING "id" [["email", "user@example.com"], ["name", "User"]] (0.4ms) COMMIT member = Member.create(user: user, project: project) (0.5ms) BEGIN SQL (3.7ms) INSERT INTO "members" ("project_id", "user_id") VALUES ($1, $2) RETURNING "id" [["project_id", 1], ["user_id", 1]] (0.3ms) COMMIT Wednesday, December 18, 13
  • 7.
    Updating Records project.update_attributes(name: "UpdatedProject") (0.2ms) BEGIN SQL (0.9ms) UPDATE "projects" SET "name" = $1 WHERE "projects"."id" = 1 [["name", "Updated Project"]] (0.4ms) COMMIT user.update_attributes(name: "Updated User") (0.1ms) BEGIN SQL (0.9ms) UPDATE "users" SET "name" = $1 WHERE "users"."id" = 1 [["name", "Updated User"]] (0.4ms) COMMIT Wednesday, December 18, 13
  • 8.
    Autosave class Member <ActiveRecord::Base belongs_to :user belongs_to :project end project = Project.new(name: "Project") user = User.new(name: "User", email: "user@example.com") member = Member.create(user: user, project: project) Guess the result. Wednesday, December 18, 13
  • 9.
    Autosave class Member <ActiveRecord::Base belongs_to :user belongs_to :project end project = Project.new(name: "Project") user = User.new(name: "User", email: "user@example.com") member = Member.create(user: user, project: project) (0.4ms) BEGIN SQL (2.7ms) INSERT INTO "users" ("email", "name") VALUES ($1, $2) RETURNING "id" [["email", "user@example.com"], ["name", "User"]] SQL (1.2ms) INSERT INTO "projects" ("name") VALUES ($1) RETURNING "id" [["name", "Project"]] SQL (3.5ms) INSERT INTO "members" ("project_id", "user_id") VALUES ($1, $2) RETURNING "id" [["project_id", 1], ["user_id", 1]] (0.5ms) COMMIT Wednesday, December 18, 13
  • 10.
    Autosave class Member <ActiveRecord::Base belongs_to :user belongs_to :project end member = Member.new member.build_user(name: "User", email: "user@example.com") member.build_project(name: "Project") member.save (0.4ms) BEGIN SQL (2.7ms) INSERT INTO "users" ("email", "name") VALUES ($1, $2) RETURNING "id" [["email", "user@example.com"], ["name", "User"]] SQL (1.2ms) INSERT INTO "projects" ("name") VALUES ($1) RETURNING "id" [["name", "Project"]] SQL (3.5ms) INSERT INTO "members" ("project_id", "user_id") VALUES ($1, $2) RETURNING "id" [["project_id", 1], ["user_id", 1]] (0.5ms) COMMIT Wednesday, December 18, 13
  • 11.
    Autosave class Member <ActiveRecord::Base belongs_to :user, autosave: false belongs_to :project, autosave: false end member = Member.new member.build_user(name: "User", email: "user@example.com") member.build_project(name: "Project") member.save Guess the result. Wednesday, December 18, 13
  • 12.
    Autosave class Member <ActiveRecord::Base belongs_to :user, autosave: false belongs_to :project, autosave: false end member = Member.new member.build_user(name: "User", email: "user@example.com") member.build_project(name: "Project") member.save PG::NotNullViolation: ERROR: null value in column "user_id" violates not-null constraint (ActiveRecord::StatementInvalid) DETAIL: Failing row contains (1, null, null). : INSERT INTO "members" DEFAULT VALUES RETURNING "id" Wednesday, December 18, 13
  • 13.
    Autosave class Member <ActiveRecord::Base belongs_to :user belongs_to :project end member.user.name = "Updated User" member.project.name = "Updated Project" member.save Guess the result. Wednesday, December 18, 13
  • 14.
    Autosave class Member <ActiveRecord::Base belongs_to :user belongs_to :project end member.user.name = "Updated User" member.project.name = "Updated Project" member.save (0.2ms) BEGIN (0.2ms) COMMIT Wednesday, December 18, 13
  • 15.
    Autosave class Member <ActiveRecord::Base belongs_to :user, autosave: true belongs_to :project, autosave: true end member.user.name = "Updated User" member.project.name = "Updated Project" member.save Guess the result. Wednesday, December 18, 13
  • 16.
    Autosave class Member <ActiveRecord::Base belongs_to :user, autosave: true belongs_to :project, autosave: true end member.user.name = "Updated User" member.project.name = "Updated Project" member.save (0.2ms) BEGIN SQL (1.1ms) UPDATE "users" SET "name" = $1 WHERE "users"."id" = 1 [["name", "Updated User"]] SQL (1.2ms) UPDATE "projects" SET "name" = $1 WHERE "projects"."id" = 1 [["name", "Updated Project"]] (0.4ms) COMMIT Wednesday, December 18, 13
  • 17.
    Autosave class Project <ActiveRecord::Base has_many :members end user = User.new(name: "User", email: "user@example.com") project = Project.new(name: "Project") project.members << Member.new(user: user) project.save Guess the result. Wednesday, December 18, 13
  • 18.
    Autosave class Project <ActiveRecord::Base has_many :members end user = User.new(name: "User", email: "user@example.com") project = Project.new(name: "Project") project.members << Member.new(user: user) project.save (0.7ms) BEGIN SQL (1.6ms) INSERT INTO "projects" ("name") VALUES ($1) RETURNING "id" [["name", "Project"]] SQL (1.2ms) INSERT INTO "users" ("email", "name") VALUES ($1, $2) RETURNING "id" [["email", "user@example.com"], ["name", "User"]] SQL (3.4ms) INSERT INTO "members" ("project_id", "user_id") VALUES ($1, $2) RETURNING "id" [["project_id", 1], ["user_id", 1]] (0.5ms) COMMIT Wednesday, December 18, 13
  • 19.
    Autosave class Project <ActiveRecord::Base has_many :members end user = User.new(name: "User", email: "user@example.com") project = Project.new(name: "Project") project.members.build(user: user) project.save (0.7ms) BEGIN SQL (1.6ms) INSERT INTO "projects" ("name") VALUES ($1) RETURNING "id" [["name", "Project"]] SQL (1.2ms) INSERT INTO "users" ("email", "name") VALUES ($1, $2) RETURNING "id" [["email", "user@example.com"], ["name", "User"]] SQL (3.4ms) INSERT INTO "members" ("project_id", "user_id") VALUES ($1, $2) RETURNING "id" [["project_id", 1], ["user_id", 1]] (0.5ms) COMMIT Wednesday, December 18, 13
  • 20.
    Autosave class Project <ActiveRecord::Base has_many :members, autosave: false end user = User.new(name: "User", email: "user@example.com") project = Project.new(name: "Project") project.members.build(user: user) project.save Guess the result. Wednesday, December 18, 13
  • 21.
    Autosave class Project <ActiveRecord::Base has_many :members, autosave: false end user = User.new(name: "User", email: "user@example.com") project = Project.new(name: "Project") project.members.build(user: user) project.save (0.4ms) BEGIN SQL (1.6ms) INSERT INTO "projects" ("name") VALUES ($1) RETURNING "id" [["name", "Project"]] (0.4ms) COMMIT Wednesday, December 18, 13
  • 22.
    Inverses class Project <ActiveRecord::Base has_many :tasks end class Task < ActiveRecord::Base belongs_to :project end project = Project.new(name: "Project") task = project.tasks.build project.save p project.object_id p task.project.object_id Guess the result. Wednesday, December 18, 13
  • 23.
    Inverses class Project <ActiveRecord::Base has_many :tasks end class Task < ActiveRecord::Base belongs_to :project end project = Project.new(name: "Project") task = project.tasks.build project.save p project.object_id p task.project.object_id Not just an extra query. Split brain! 70236648295560 Project Load (1.4ms) SELECT "projects".* FROM "projects" WHERE "projects"."id" = $1 ORDER BY "projects"."id" ASC LIMIT 1 [["id", 1]] 70236645304160 Wednesday, December 18, 13
  • 24.
    Inverses class Project <ActiveRecord::Base has_many :tasks, inverse_of: :project end class Task < ActiveRecord::Base belongs_to :project end project = Project.new(name: "Project") task = project.tasks.build project.save p project.object_id p task.project.object_id Guess the result. Wednesday, December 18, 13
  • 25.
    Inverses class Project <ActiveRecord::Base has_many :tasks, inverse_of: :project end class Task < ActiveRecord::Base belongs_to :project end project = Project.new(name: "Project") task = project.tasks.build project.save p project.object_id p task.project.object_id 70259515608140 70259515608140 Wednesday, December 18, 13
  • 26.
    Summary • Use autosaveand inverse associations • Inspect the generated SQL for sanity • Avoid explicit transactions Wednesday, December 18, 13
  • 27.
    Authorization class ProjectMembersController <ApplicationController # POST /project/:project_id/members def create member = Member.create(member_params.merge(project_id: project_id)) respond_with member end private def project_id params.require(:project_id) end def member_params params.require(:member).permit(:user_id) end end Wednesday, December 18, 13
  • 28.
    Authorization class ProjectMembersController <ApplicationController # POST /project/:project_id/members def create member = Member.create(member_params.merge(project_id: project_id)) respond_with member end Anyone can def project_id add any user to any params.require(:project_id) end project! private def member_params params.require(:member).permit(:user_id) end end Wednesday, December 18, 13
  • 29.
    Authorization class ProjectMembersController <ApplicationController # POST /project/:project_id/members def create member = current_user.create_project_member(project_id, member_params) respond_with member end private def project_id params.require(:project_id) end def member_params params.require(:member).permit(:user_id) end end class User < ActiveRecord::Base has_many :members has_many :projects, through: :members May only add members to my own projects. def create_project_member(project_id, member_params) project = projects.find(project_id) project.members.create(member_params) end end Wednesday, December 18, 13
  • 30.
    Authorization class ProjectMembersController <ApplicationController # POST /project/:project_id/members def create member = current_user.build_project_member(project_id, member_params) member.save respond_with member end private def project_id params.require(:project_id) end Separate build vs. create def member_params params.require(:member).permit(:user_id) end end class User < ActiveRecord::Base has_many :members has_many :projects, through: :members def build_project_member(project_id, member_params) project = projects.find(project_id) project.members.build(member_params) end end Wednesday, December 18, 13
  • 31.
    Authorization class ProjectMembersController <ApplicationController # POST /project/:project_id/members def create member = current_user.build_project_member(project_id, member_params) member.save respond_with member end private def project_id params.require(:project_id) end def member_params params.require(:member).permit(:user_id) end end What if I am not a member of this class User < ActiveRecord::Base has_many :members has_many :projects, through: :members project? def build_project_member(project_id, member_params) project = projects.find(project_id) project.members.build(member_params) end end Wednesday, December 18, 13
  • 32.
    Authorization Couldn't find Projectwith id=1 (ActiveRecord::RecordNotFound) Wednesday, December 18, 13
  • 33.
    Authorization class User <ActiveRecord::Base has_many :members, inverse_of: :user has_many :projects, through: :members def build_project_member(project_id, member_params) project = projects.find_one(project_id) if project project.members.build(member_params) end end end Wednesday, December 18, 13
  • 34.
    Authorization class User <ActiveRecord::Base has_many :members, inverse_of: :user has_many :projects, through: :members def member(project_id) members.find_by(project_id: project_id) end def build_project_member(project_id, member_params) member = member(project_id) if member member.build_project_member(member_params) end end end class Member < ActiveRecord::Base belongs_to :user, inverse_of: :members belongs_to :project, inverse_of: :members def build_project_member(member_params) project.members.build(member_params) end end Wednesday, December 18, 13
  • 35.
    Authorization class User <ActiveRecord::Base has_many :members, inverse_of: :user has_many :projects, through: :members def member(project_id) members.find_by(project_id: project_id) end def build_project_member(project_id, member_params) member = member(project_id) if member member.build_project_member(member_params) end end end class Member < ActiveRecord::Base belongs_to :user, inverse_of: :members belongs_to :project, inverse_of: :members def build_project_member(member_params) if role == "admin" project.members.build(member_params) end end end Wednesday, December 18, 13
  • 36.
    Authorization class ProjectMembersController <ApplicationController # POST /project/:project_id/members def create member = current_user.build_project_member(project_id, member_params) if member.nil? # handle error else member.save respond_with member end end private def project_id params.require(:project_id) end def member_params params.require(:member).permit(:user_id) end end Wednesday, December 18, 13
  • 37.
    Authorization class ProjectMembersController <ApplicationController # POST /project/:project_id/members def create member = current_user.build_project_member(project_id, member_params) if member.nil? # handle error else member.save respond_with member end end private Which error happened? def project_id params.require(:project_id) end def member_params params.require(:member).permit(:user_id) end end Wednesday, December 18, 13
  • 38.
    Authorization Failure = Struct.new(:error)do def success? false end end Success = Struct.new(:value) do def success? true end end class Member < ActiveRecord::Base belongs_to :user, inverse_of: :members belongs_to :project, inverse_of: :members def build_project_member(member_params) if role == "admin" Success.new(project.members.build(member_params)) else Failure.new(:not_authorized) end end end Wednesday, December 18, 13
  • 39.
    Authorization class User <ActiveRecord::Base has_many :members, inverse_of: :user has_many :projects, through: :members def member(project_id) member = members.find_by(project_id: project_id) if member Success.new(member) else Failure.new(:member_not_found) end end def build_project_member(project_id, member_params) result = member(project_id) if result.success? result.value.build_project_member(member_params) else result end end end Wednesday, December 18, 13
  • 40.
    Authorization class ProjectMembersController <ApplicationController # POST /project/:project_id/members def create result = current_user.build_project_member(project_id, member_params) if result.success? member = result.value member.save respond_with member else result.error # handle error end end private def project_id params.require(:project_id) end def member_params params.require(:member).permit(:user_id) end end Wednesday, December 18, 13
  • 41.
    Authorization project = Project.create(name:"Project") alice = User.create(name: "Alice", email: "alice@example.com") bob = User.create(name: "Bob", email: "bob@example.com") p alice.build_project_member(project.id, { user_id: bob.id, role: "member" }) #<struct Failure error=:member_not_found> Wednesday, December 18, 13
  • 42.
    Authorization project = Project.create(name:"Project") alice = User.create(name: "Alice", email: "alice@example.com") bob = User.create(name: "Bob", email: "bob@example.com") alice.members.create(project: project, role: "member") p alice.build_project_member(project.id, { user_id: bob.id, role: "member" }) #<struct Failure error=:not_authorized> Wednesday, December 18, 13
  • 43.
    Authorization project = Project.create(name:"Project") alice = User.create(name: "Alice", email: "alice@example.com") bob = User.create(name: "Bob", email: "bob@example.com") alice.members.create(project: project, role: "admin") p alice.build_project_member(project.id, { user_id: bob.id, role: "member" }) #<struct Success value=#<Member user_id: 2, project_id: 1, role: "member">> Wednesday, December 18, 13
  • 44.
    Summary • Use therelations • Move beyond ActiveRecord’s API • Use result objects to represent possible failures • Separate building vs. creating APIs Wednesday, December 18, 13
  • 45.
    Refactoring class User <ActiveRecord::Base has_many :members has_many :projects, through: :members def member(project_id) member = members.find_by(project_id: project_id) if member Success.new(member) else Failure.new(:member_not_found) end end def build_project_member(project_id, member_params) result = member(project_id) if result.success? result.value.build_project_member(member_params) else result end end end Wednesday, December 18, 13
  • 46.
    Refactoring class User <ActiveRecord::Base has_many :members has_many :projects, through: :members def member(project_id) member = members.find_by(project_id: project_id) if member Success.new(member) else Failure.new(:member_not_found) end end def build_project_member(project_id, member_params) result = member(project_id) if result.success? result.value.build_project_member(member_params) else result end end end Wednesday, December 18, 13 We need a way to combine results.
  • 47.
    Refactoring Failure = Struct.new(:error)do def success? false end def map self end def bind self end end Wednesday, December 18, 13 Success = Struct.new(:value) do def success? true end def map Success.new(yield value) end def bind yield value end end
  • 48.
    Refactoring class User <ActiveRecord::Base has_many :members has_many :projects, through: :members def member(project_id) member = members.find_by(project_id: project_id) if member Success.new(member) else Failure.new(:member_not_found) end end Build compound results. def build_project_member(project_id, member_params) member(project_id).bind do |member| member.build_project_member(member_params) end end end Wednesday, December 18, 13
  • 49.
    Serialize class Member <ActiveRecord::Base belongs_to :user, inverse_of: :members belongs_to :project, inverse_of: :members def build_project_member(member_params) if role == "admin" Success.new(project.members.build(member_params)) else Failure.new(:not_authorized) end end end Wednesday, December 18, 13
  • 50.
    Serialize class Member <ActiveRecord::Base belongs_to :user, inverse_of: :members belongs_to :project, inverse_of: :members serialize :role, Role def build_project_member(member_params) role.build_project_member(project, member_params) end end Wednesday, December 18, 13
  • 51.
    Serialize class Role Unknown =Object.new def Unknown.name nil end MAP = {} MAP.default = Unknown def self.load(name) MAP[name] end def self.dump(role) role.name end attr_reader :name def initialize(name, &block) @name = name instance_eval(&block) MAP[name] = self end end Wednesday, December 18, 13
  • 52.
    Serialize class Role Admin =Role.new("admin") do def build_project_member(project, member_params) Success.new(project.members.build(member_params)) end end Member = Role.new("member") do def build_project_member(project, member_params) Failure.new(:not_authorized) end end Null = Role.new(nil) do def build_project_member(project, member_params) Failure.new(:missing_role) end end def Unknown.build_project_member(project, member_params) Failure.new(:unknown_role) end end Wednesday, December 18, 13
  • 53.
    Authorization class ProjectMembersController <ApplicationController # POST /project/:project_id/members def create result = current_user.build_project_member(project_id, member_params) if result.success? member = result.value member.save respond_with member else result.error # handle error end end private def project_id params.require(:project_id) end def member_params member_params = params.require(:member).permit(:user_id, :role) role = Role.load(member_params[:role].presence) member_params.merge(:role => role) end end Wednesday, December 18, 13
  • 54.