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
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
4. 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
5. 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
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: "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
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 autosave and 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
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
44. 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
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