Successfully reported this slideshow.
We use your LinkedIn profile and activity data to personalize ads and to show you more relevant ads. You can change your ad preferences anytime.

Effective ActiveRecord

1,494 views

Published on

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

Published in: Technology, Business
  • Login to see the comments

  • Be the first to like this

Effective ActiveRecord

  1. 1. Effective ActiveRecord Sam Goldman @nontrivialzeros http://github.com/samwgoldman sam@smartlogic.io Wednesday, December 18, 13
  2. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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. 26. Summary • Use autosave and inverse associations • Inspect the generated SQL for sanity • Avoid explicit transactions Wednesday, December 18, 13
  27. 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. 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. 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. 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. 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. 32. Authorization Couldn't find Project with id=1 (ActiveRecord::RecordNotFound) Wednesday, December 18, 13
  33. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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. 54. Questions? http://smartlogic.io http://twitter.com/smartlogic http://github.com/smartlogic http://facebook.com/smartlogic Wednesday, December 18, 13

×