12/08/2018, 13:27

Viết scope bằng arel

Đôi khi trong công việc, việc viết scope cần thiết phải đưa ít các string nhằm tránh sql injection, có thể thử bằng cách dùng arel. Link về arel: https://github.com/rails/arel Ví dụ về model lấy trong bài viết trước: https://viblo.asia/pham.huy.cuong/posts/ZabG9z9ovzY6 Như vậy ta có các bảng: ...

Đôi khi trong công việc, việc viết scope cần thiết phải đưa ít các string nhằm tránh sql injection, có thể thử bằng cách dùng arel.

Link về arel: https://github.com/rails/arel

Ví dụ về model lấy trong bài viết trước: https://viblo.asia/pham.huy.cuong/posts/ZabG9z9ovzY6

Như vậy ta có các bảng: User, House, HousesUser.

Thử viết 1 query đơn gỉan sử dụng arel cho bảng House.

irb(main):016:0> houses = House.arel_table
=> #<Arel::Table:0x007ff5ed1793d8 @name="houses", @engine=House(id: integer, name: string, created_at: datetime, updated_at: datetime), @columns=nil, @aliases=[], @table_alias=nil, @primary_key=nil>
irb(main):017:0> House.where(houses[:id].eq(1))
  House Load (0.2ms)  SELECT "houses".* FROM "houses" WHERE "houses"."id" = 1
=> #<ActiveRecord::Relation [#<House id: 1, name: "House_0", created_at: "2016-04-01 04:15:06", updated_at: "2016-04-12 04:16:20">]>

Thử gõ houses[:id] và houses[:id].eq(1) ta được:

irb(main):003:0> houses[:id]
=> #<struct Arel::Attributes::Attribute relation=#<Arel::Table:0x007ff4536d54d0 @name="houses", @engine=House(id: integer, name: string, created_at: datetime, updated_at: datetime), @columns=nil, @aliases=[], @table_alias=nil, @primary_key=nil>, name=:id>

irb(main):004:0> houses[:id].eq(1)
=> #<Arel::Nodes::Equality:0x007ff4535464e8 @left=#<struct Arel::Attributes::Attribute relation=#<Arel::Table:0x007ff4536d54d0 @name="houses", @engine=House(id: integer, name: string, created_at: datetime, updated_at: datetime), @columns=nil, @aliases=[], @table_alias=nil, @primary_key=nil>, name=:id>, @right=#<Arel::Nodes::Casted:0x007ff453546510 @val=1, @attribute=#<struct Arel::Attributes::Attribute relation=#<Arel::Table:0x007ff4536d54d0 @name="houses", @engine=House(id: integer, name: string, created_at: datetime, updated_at: datetime), @columns=nil, @aliases=[], @table_alias=nil, @primary_key=nil>, name=:id>>>

houses[:id] là Arel::Attributes::Attribute, houses[:id].eq(1) là Arel::Nodes::Equality class giữ phép so sánh tương đương giữa 2 Arel::Attributes::Attribute với name=:id và val=1.

2 Arel::Attributes::Attribute được lưu ở @left và @right, ta có thể gọi houses[:id].eq(1).right để lấy ra 1 trong 2 Arel::Attributes::Attribute.

Arel::Nodes là module chứa các class tạo ra các phép toán đối với Arel::Attributes::Attribute. Ví dụ houses[:id].lt(1) sẽ tạo ra class Arel::Nodes::LessThan.

Có thể nhìn hình vẽ sau để hình dung ra việc build câu sql của arel:

arel_ast_graph.png

Sơ qua về cách render câu query của arel là như vậy, chi tiết mình xin phép trình bày ở các bài viết tiếp theo, giờ thử viết các câu query dùng arel.

Chọn các bản ghi ở bảng House có id = 2 hoặc name = 'House_8'

irb(main):003:0> houses = House.arel_table
irb(main):004:0> House.where(houses[:id].eq(2).or(houses[:name].eq('House_8')))
  House Load (0.2ms)  SELECT "houses".* FROM "houses" WHERE ("houses"."id" = 2 OR "houses"."name" = 'House_8')
=> #<ActiveRecord::Relation [#<House id: 2, name: "House_1", created_at: "2016-04-01 04:15:06", updated_at: "2016-04-12 04:16:20">, #<House id: 9, name: "House_8", created_at: "2016-04-01 04:15:08", updated_at: "2016-04-01 04:15:08">]>

Bình thường viết câu query có condition là or khá lằng nhằng (nếu như không muốn bị string injection) thường ta dùng ransack, còn bây giờ câu query khá đơn gỉan .or(...)

Query để Tìm bản ghi ở model User có id là số lớn nhất bên bảng House :

irb(main):010:0> User.where(users[:id].eq(houses.project(houses[:id].maximum)))
  User Load (0.2ms)  SELECT "users".* FROM "users" WHERE "users"."id" = (SELECT MAX("houses"."id") FROM "houses")
=> #<ActiveRecord::Relation [#<User id: 9, name: "Name_8", created_at: "2016-04-01 04:15:08", updated_at: "2016-04-01 04:15:08">]>

Query sắp xêp record của User theo bản ghi tạo mới nhất của HousesUser

irb(main):054:0> User.joins(:houses_users).select("users.*").select(houses_users.where(houses_users[:user_id].eq(users[:id])).project(houses_users[:created_at].maximum).as("last_created")).distinct.order("last_created DESC").map &:attributes
  User Load (0.2ms)  SELECT DISTINCT users.*, (SELECT MAX("houses_users"."created_at") FROM "houses_users" WHERE "houses_users"."user_id" = "users"."id") last_created FROM "users" INNER JOIN "houses_users" ON "houses_users"."user_id" = "users"."id"  ORDER BY last_created DESC
=> [{"id"=>2, "name"=>"Name_1", "created_at"=>Fri, 01 Apr 2016 04:15:06 UTC +00:00, "updated_at"=>Fri, 01 Apr 2016 04:15:06 UTC +00:00, "last_created"=>"2016-05-23 11:08:01.367001"}, {"id"=>1, "name"=>"new_name", "created_at"=>Fri, 01 Apr 2016 04:15:05 UTC +00:00, "updated_at"=>Wed, 13 Apr 2016 01:39:28 UTC +00:00, "last_created"=>"2016-04-13 01:39:28.612710"}]

Ta có thể join bảng khá đơn gỉan

irb(main):018:0> houses.join(users).on(houses[:id].eq(users[:id])).to_sql
=> "SELECT FROM "houses" INNER JOIN "users" ON "houses"."id" = "users"."id""

limit và offset tương ứng với take va skip

irb(main):020:0> users.take(5).to_sql
=> "SELECT  FROM "users" LIMIT 5"
irb(main):022:0> users.skip(5).to_sql
=> "SELECT FROM "users" LIMIT -1 OFFSET 5"

Cảm ơn bạn đã đọc và hi vọng bài viết gíup ích phần nào trong công việc của bạn.

0