Effective ActiveRecord

1,308 views

Published on

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

Published in: Technology, Business
0 Comments
0 Likes
Statistics
Notes
  • Be the first to comment

  • Be the first to like this

No Downloads
Views
Total views
1,308
On SlideShare
0
From Embeds
0
Number of Embeds
1
Actions
Shares
0
Downloads
8
Comments
0
Likes
0
Embeds 0
No embeds

No notes for slide

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

×