Download - Effective ActiveRecord
Effective ActiveRecordSam Goldman
@nontrivialzeroshttp://github.com/samwgoldman
Wednesday, December 18, 13
Review: Models
id email name1 [email protected] Foo2 [email protected] Bar
usersclass User < ActiveRecord::Baseend
foo = User.find(1)foo.name # "Foo"foo.email # "[email protected]"
bar = User.find(2)bar.name # "Barbar.email # "[email protected]"
Wednesday, December 18, 13
Review: Has Many
id name1 Foo project2 Bar project
projectsclass Project < AR::Base has_many :membersend
class Member < AR::Base belongs_to :projectend
foo_project = Project.find(1)
foo_project.name# "Foo project"
foo_project.members. map(&:email)# ["[email protected]",# "[email protected]"]
id project_id email1 1 [email protected] 1 [email protected] 2 [email protected] 2 [email protected]
members
Wednesday, December 18, 13
Review: Belongs To
id name1 Foo project2 Bar project
projectsclass Project < AR::Base has_many :membersend
class Member < AR::Base belongs_to :projectend
foo = Member.find(1)
foo.email# "[email protected]"
foo.project.name# "Foo project"
id project_id email1 1 [email protected] 1 [email protected] 2 [email protected] 2 [email protected]
members
Wednesday, December 18, 13
Review: Has Many Through
id name1 Foo project2 Bar project
projects
class User < AR::Base has_many :members has_many :projects, through: :membersend
class Project < AR::Base has_many :membersend
class Member < AR::Base belongs_to :user belongs_to :projectend
foo = User.find(1)foo.projects.map(&:name)# ["Foo project",# "Bar project"]
id project_id user_id1 1 13 2 1
members
id email name1 [email protected] Foo
users
Wednesday, December 18, 13
Creating Recordsproject = Project.create(name: "Project") (0.3ms) BEGINSQL (1.5ms) INSERT INTO "projects" ("name") VALUES ($1) RETURNING "id" [["name", "Project"]] (0.4ms) COMMIT
user = User.create(name: "User", email: "[email protected]") (0.3ms) BEGINSQL (1.3ms) INSERT INTO "users" ("email", "name") VALUES ($1, $2) RETURNING "id" [["email", "[email protected]"], ["name", "User"]] (0.4ms) COMMIT
member = Member.create(user: user, project: project) (0.5ms) BEGINSQL (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 Recordsproject.update_attributes(name: "Updated Project") (0.2ms) BEGINSQL (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) BEGINSQL (0.9ms) UPDATE "users" SET "name" = $1 WHERE "users"."id" = 1 [["name", "Updated User"]] (0.4ms) COMMIT
Wednesday, December 18, 13
Autosaveclass Member < ActiveRecord::Base belongs_to :user belongs_to :projectend
project = Project.new(name: "Project")user = User.new(name: "User", email: "[email protected]")member = Member.create(user: user, project: project)Guess the result.
Wednesday, December 18, 13
Autosaveclass Member < ActiveRecord::Base belongs_to :user belongs_to :projectend
project = Project.new(name: "Project")user = User.new(name: "User", email: "[email protected]")member = Member.create(user: user, project: project) (0.4ms) BEGINSQL (2.7ms) INSERT INTO "users" ("email", "name") VALUES ($1, $2) RETURNING "id" [["email", "[email protected]"], ["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
class Member < ActiveRecord::Base belongs_to :user belongs_to :projectend
member = Member.newmember.build_user(name: "User", email: "[email protected]")member.build_project(name: "Project")member.save (0.4ms) BEGINSQL (2.7ms) INSERT INTO "users" ("email", "name") VALUES ($1, $2) RETURNING "id" [["email", "[email protected]"], ["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
Autosave
Wednesday, December 18, 13
class Member < ActiveRecord::Base belongs_to :user, autosave: false belongs_to :project, autosave: falseend
member = Member.newmember.build_user(name: "User", email: "[email protected]")member.build_project(name: "Project")member.saveGuess the result.
Autosave
Wednesday, December 18, 13
Autosaveclass Member < ActiveRecord::Base belongs_to :user, autosave: false belongs_to :project, autosave: falseend
member = Member.newmember.build_user(name: "User", email: "[email protected]")member.build_project(name: "Project")member.savePG::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
Autosaveclass Member < ActiveRecord::Base belongs_to :user belongs_to :projectend
member.user.name = "Updated User"member.project.name = "Updated Project"member.saveGuess the result.
Wednesday, December 18, 13
Autosaveclass Member < ActiveRecord::Base belongs_to :user belongs_to :projectend
member.user.name = "Updated User"member.project.name = "Updated Project"member.save(0.2ms) BEGIN(0.2ms) COMMIT
Wednesday, December 18, 13
class Member < ActiveRecord::Base belongs_to :user, autosave: true belongs_to :project, autosave: trueend
member.user.name = "Updated User"member.project.name = "Updated Project"member.saveGuess the result.
Autosave
Wednesday, December 18, 13
Autosaveclass Member < ActiveRecord::Base belongs_to :user, autosave: true belongs_to :project, autosave: trueend
member.user.name = "Updated User"member.project.name = "Updated Project"member.save (0.2ms) BEGINSQL (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
Autosaveclass Project < ActiveRecord::Base has_many :membersend
user = User.new(name: "User", email: "[email protected]")project = Project.new(name: "Project")project.members << Member.new(user: user)project.saveGuess the result.
Wednesday, December 18, 13
Autosaveclass Project < ActiveRecord::Base has_many :membersend
user = User.new(name: "User", email: "[email protected]")project = Project.new(name: "Project")project.members << Member.new(user: user)project.save (0.7ms) BEGINSQL (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", "[email protected]"], ["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
class Project < ActiveRecord::Base has_many :membersend
user = User.new(name: "User", email: "[email protected]")project = Project.new(name: "Project")project.members.build(user: user)project.save (0.7ms) BEGINSQL (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", "[email protected]"], ["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
Autosave
Wednesday, December 18, 13
class Project < ActiveRecord::Base has_many :members, autosave: falseend
user = User.new(name: "User", email: "[email protected]")project = Project.new(name: "Project")project.members.build(user: user)project.saveGuess the result.
Autosave
Wednesday, December 18, 13
Autosaveclass Project < ActiveRecord::Base has_many :members, autosave: falseend
user = User.new(name: "User", email: "[email protected]")project = Project.new(name: "Project")project.members.build(user: user)project.save (0.4ms) BEGINSQL (1.6ms) INSERT INTO "projects" ("name") VALUES ($1) RETURNING "id" [["name", "Project"]] (0.4ms) COMMIT
Wednesday, December 18, 13
Inversesclass Project < ActiveRecord::Base has_many :tasksend
class Task < ActiveRecord::Base belongs_to :projectend
project = Project.new(name: "Project")task = project.tasks.buildproject.save
p project.object_idp task.project.object_idGuess the result.
Wednesday, December 18, 13
Inversesclass Project < ActiveRecord::Base has_many :tasksend
class Task < ActiveRecord::Base belongs_to :projectend
project = Project.new(name: "Project")task = project.tasks.buildproject.save
p project.object_idp task.project.object_id70236648295560Project Load (1.4ms) SELECT "projects".* FROM "projects" WHERE "projects"."id" = $1 ORDER BY "projects"."id" ASC LIMIT 1 [["id", 1]]70236645304160
Not just an extra query. Split
brain!
Wednesday, December 18, 13
class Project < ActiveRecord::Base has_many :tasks, inverse_of: :projectend
class Task < ActiveRecord::Base belongs_to :projectend
project = Project.new(name: "Project")task = project.tasks.buildproject.save
p project.object_idp task.project.object_idGuess the result.
Inverses
Wednesday, December 18, 13
Inversesclass Project < ActiveRecord::Base has_many :tasks, inverse_of: :projectend
class Task < ActiveRecord::Base belongs_to :projectend
project = Project.new(name: "Project")task = project.tasks.buildproject.save
p project.object_idp task.project.object_id7025951560814070259515608140
Wednesday, December 18, 13
Summary
• Use autosave and inverse associations
• Inspect the generated SQL for sanity
• Avoid explicit transactions
Wednesday, December 18, 13
Authorizationclass 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) endend
Wednesday, December 18, 13
Authorizationclass 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) endend
Anyone can add any user to any
project!
Wednesday, December 18, 13
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) endend
class User < ActiveRecord::Base has_many :members has_many :projects, through: :members
def create_project_member(project_id, member_params) project = projects.find(project_id) project.members.create(member_params) endend
Authorization
May only add members to my own
projects.
Wednesday, December 18, 13
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) endend
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) endend
Authorization
Separate build vs. create
Wednesday, December 18, 13
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) endend
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) endend
Authorization
What if I am not a member of this
project?
Wednesday, December 18, 13
Authorization
Couldn't find Project with id=1 (ActiveRecord::RecordNotFound)
Wednesday, December 18, 13
Authorizationclass 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 endend
Wednesday, December 18, 13
Authorizationclass 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 endend
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) endend
Wednesday, December 18, 13
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 endend
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 endend
Authorization
Wednesday, December 18, 13
Authorizationclass 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) endend
Wednesday, December 18, 13
Authorizationclass 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) endend
Which error happened?
Wednesday, December 18, 13
AuthorizationFailure = Struct.new(:error) do def success? false endend
Success = Struct.new(:value) do def success? true endend
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 endend
Wednesday, December 18, 13
Authorizationclass 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 endend
Wednesday, December 18, 13
Authorizationclass 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) endend
Wednesday, December 18, 13
Authorizationproject = Project.create(name: "Project")alice = User.create(name: "Alice", email: "[email protected]")bob = User.create(name: "Bob", email: "[email protected]")
p alice.build_project_member(project.id, { user_id: bob.id, role: "member"})#<struct Failure error=:member_not_found>
Wednesday, December 18, 13
project = Project.create(name: "Project")alice = User.create(name: "Alice", email: "[email protected]")bob = User.create(name: "Bob", email: "[email protected]")
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>
Authorization
Wednesday, December 18, 13
project = Project.create(name: "Project")alice = User.create(name: "Alice", email: "[email protected]")bob = User.create(name: "Bob", email: "[email protected]")
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">>
Authorization
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
Refactoringclass 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 endend
Wednesday, December 18, 13
Refactoringclass 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 endend
We need a way to combine results.
Wednesday, December 18, 13
RefactoringFailure = Struct.new(:error) do def success? false end
def map self end
def bind self endend
Success = Struct.new(:value) do def success? true end
def map Success.new(yield value) end
def bind yield value endend
Wednesday, December 18, 13
Refactoringclass 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) member(project_id).bind do |member| member.build_project_member(member_params) end endend
Build compound results.
Wednesday, December 18, 13
Serializeclass 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 endend
Wednesday, December 18, 13
Serializeclass 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) endend
Wednesday, December 18, 13
Serializeclass 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 endend
Wednesday, December 18, 13
Serializeclass 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) endend
Wednesday, December 18, 13
Authorizationclass 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) endend
Wednesday, December 18, 13
Questions?
http://smartlogic.io
http://twitter.com/smartlogic
http://github.com/smartlogic
http://facebook.com/smartlogic
Wednesday, December 18, 13