54
Effective ActiveRecord Sam Goldman @nontrivialzeros http://github.com/samwgoldman [email protected] Wednesday, December 18, 13

Effective ActiveRecord

Embed Size (px)

DESCRIPTION

Check out how to use Effective ActiveRecord, as presented by software developer Sam Goldman.

Citation preview

Page 1: Effective ActiveRecord

Effective ActiveRecordSam Goldman

@nontrivialzeroshttp://github.com/samwgoldman

[email protected]

Wednesday, December 18, 13

Page 2: Effective ActiveRecord

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

Page 3: Effective ActiveRecord

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

Page 4: Effective ActiveRecord

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

Page 5: Effective ActiveRecord

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

Page 6: Effective ActiveRecord

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

Page 7: Effective ActiveRecord

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

Page 8: Effective ActiveRecord

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

Page 9: Effective ActiveRecord

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

Page 10: Effective ActiveRecord

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

Page 11: Effective ActiveRecord

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

Page 12: Effective ActiveRecord

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

Page 13: Effective ActiveRecord

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

Page 14: Effective ActiveRecord

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

Page 15: Effective ActiveRecord

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

Page 16: Effective ActiveRecord

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

Page 17: Effective ActiveRecord

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

Page 18: Effective ActiveRecord

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

Page 19: Effective ActiveRecord

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

Page 20: Effective ActiveRecord

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

Page 21: Effective ActiveRecord

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

Page 22: Effective ActiveRecord

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

Page 23: Effective ActiveRecord

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

Page 24: Effective ActiveRecord

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

Page 25: Effective ActiveRecord

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

Page 26: Effective ActiveRecord

Summary

• Use autosave and inverse associations

• Inspect the generated SQL for sanity

• Avoid explicit transactions

Wednesday, December 18, 13

Page 27: Effective ActiveRecord

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

Page 28: Effective ActiveRecord

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

Page 29: Effective ActiveRecord

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

Page 30: Effective ActiveRecord

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

Page 31: Effective ActiveRecord

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

Page 32: Effective ActiveRecord

Authorization

Couldn't find Project with id=1 (ActiveRecord::RecordNotFound)

Wednesday, December 18, 13

Page 33: Effective ActiveRecord

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

Page 34: Effective ActiveRecord

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

Page 35: Effective ActiveRecord

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

Page 36: Effective ActiveRecord

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

Page 37: Effective ActiveRecord

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

Page 38: Effective ActiveRecord

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

Page 39: Effective ActiveRecord

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

Page 40: Effective ActiveRecord

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

Page 41: Effective ActiveRecord

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

Page 42: Effective ActiveRecord

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

Page 43: Effective ActiveRecord

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

Page 44: Effective ActiveRecord

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

Page 45: Effective ActiveRecord

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

Page 46: Effective ActiveRecord

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

Page 47: Effective ActiveRecord

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

Page 48: Effective ActiveRecord

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

Page 49: Effective ActiveRecord

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

Page 50: Effective ActiveRecord

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

Page 51: Effective ActiveRecord

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

Page 52: Effective ActiveRecord

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

Page 53: Effective ActiveRecord

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

Page 54: Effective ActiveRecord

Questions?

http://smartlogic.io

http://twitter.com/smartlogic

http://github.com/smartlogic

http://facebook.com/smartlogic

Wednesday, December 18, 13