Viết scope bằng arel
Bài đăng này đã không được cập nhật trong 3 năm
Đô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
:
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.
All rights reserved