cancancan icon indicating copy to clipboard operation
cancancan copied to clipboard

Working with large collections

Open dmke opened this issue 3 years ago • 1 comments

Steps to reproduce

The following ability conditions differ in behaviour:

class Ability
  include CanCan::Ability

  def initialize(user)
    can :manage, Customer, id: user.customer_ids
    can :manage, Customer, id: user.customers.select(:id)
  end
end

https://gist.github.com/dmke/937a7d0323a6b5907c178da103df850f

ruby main.rb --name test_via_ids
-- create_table(:customers, {:force=>true})
D, [2021-08-23T19:27:19.549694 #12265] DEBUG -- :    (0.5ms)  SELECT sqlite_version(*)
D, [2021-08-23T19:27:19.549991 #12265] DEBUG -- :    (0.0ms)  DROP TABLE IF EXISTS "customers"
D, [2021-08-23T19:27:19.550245 #12265] DEBUG -- :    (0.1ms)  CREATE TABLE "customers" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "name" varchar)
   -> 0.0042s
-- create_table(:users, {:force=>true})
D, [2021-08-23T19:27:19.550440 #12265] DEBUG -- :    (0.0ms)  DROP TABLE IF EXISTS "users"
D, [2021-08-23T19:27:19.550602 #12265] DEBUG -- :    (0.1ms)  CREATE TABLE "users" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "name" varchar)
   -> 0.0003s
-- create_join_table(:customers, :users, {:force=>true})
D, [2021-08-23T19:27:19.550869 #12265] DEBUG -- :    (0.0ms)  DROP TABLE IF EXISTS "customers_users"
D, [2021-08-23T19:27:19.551037 #12265] DEBUG -- :    (0.1ms)  CREATE TABLE "customers_users" ("customer_id" integer NOT NULL, "user_id" integer NOT NULL)
   -> 0.0004s
D, [2021-08-23T19:27:19.568794 #12265] DEBUG -- :    (0.1ms)  CREATE TABLE "ar_internal_metadata" ("key" varchar NOT NULL PRIMARY KEY, "value" varchar, "created_at" datetime(6) NOT NULL, "updated_at" datetime(6) NOT NULL)
D, [2021-08-23T19:27:19.574234 #12265] DEBUG -- :   ActiveRecord::InternalMetadata Load (0.5ms)  SELECT "ar_internal_metadata".* FROM "ar_internal_metadata" WHERE "ar_internal_metadata"."key" = ? LIMIT ?  [["key", "environment"], ["LIMIT", 1]]
D, [2021-08-23T19:27:19.577575 #12265] DEBUG -- :   TRANSACTION (0.0ms)  begin transaction
D, [2021-08-23T19:27:19.577766 #12265] DEBUG -- :   ActiveRecord::InternalMetadata Create (0.1ms)  INSERT INTO "ar_internal_metadata" ("key", "value", "created_at", "updated_at") VALUES (?, ?, ?, ?)  [["key", "environment"], ["value", "development"], ["created_at", "2021-08-23 17:27:19.577235"], ["updated_at", "2021-08-23 17:27:19.577235"]]
D, [2021-08-23T19:27:19.577901 #12265] DEBUG -- :   TRANSACTION (0.0ms)  commit transaction
Run options: -n test_via_ids --seed 48407

# Running:

D, [2021-08-23T19:27:19.598178 #12265] DEBUG -- :   TRANSACTION (0.0ms)  begin transaction
D, [2021-08-23T19:27:19.598352 #12265] DEBUG -- :   Customer Create (0.1ms)  INSERT INTO "customers" ("name") VALUES (?)  [["name", "UPS"]]
D, [2021-08-23T19:27:19.598477 #12265] DEBUG -- :   TRANSACTION (0.0ms)  commit transaction
D, [2021-08-23T19:27:19.598727 #12265] DEBUG -- :   TRANSACTION (0.0ms)  begin transaction
D, [2021-08-23T19:27:19.598880 #12265] DEBUG -- :   Customer Create (0.0ms)  INSERT INTO "customers" ("name") VALUES (?)  [["name", "DHL"]]
D, [2021-08-23T19:27:19.598975 #12265] DEBUG -- :   TRANSACTION (0.0ms)  commit transaction
D, [2021-08-23T19:27:19.599221 #12265] DEBUG -- :   TRANSACTION (0.0ms)  begin transaction
D, [2021-08-23T19:27:19.599327 #12265] DEBUG -- :   Customer Create (0.0ms)  INSERT INTO "customers" ("name") VALUES (?)  [["name", "FedEx"]]
D, [2021-08-23T19:27:19.599667 #12265] DEBUG -- :   TRANSACTION (0.0ms)  commit transaction
D, [2021-08-23T19:27:19.599974 #12265] DEBUG -- :   TRANSACTION (0.0ms)  begin transaction
D, [2021-08-23T19:27:19.600081 #12265] DEBUG -- :   Customer Create (0.0ms)  INSERT INTO "customers" ("name") VALUES (?)  [["name", "Other"]]
D, [2021-08-23T19:27:19.600171 #12265] DEBUG -- :   TRANSACTION (0.0ms)  commit transaction
D, [2021-08-23T19:27:19.601971 #12265] DEBUG -- :   TRANSACTION (0.0ms)  begin transaction
D, [2021-08-23T19:27:19.602101 #12265] DEBUG -- :   User Create (0.1ms)  INSERT INTO "users" ("name") VALUES (?)  [["name", "manager"]]
D, [2021-08-23T19:27:19.602223 #12265] DEBUG -- :   TRANSACTION (0.0ms)  commit transaction
D, [2021-08-23T19:27:19.609885 #12265] DEBUG -- :   TRANSACTION (0.0ms)  begin transaction
D, [2021-08-23T19:27:19.613449 #12265] DEBUG -- :   User::HABTM_Customers Create (0.1ms)  INSERT INTO "customers_users" ("customer_id", "user_id") VALUES (?, ?)  [["customer_id", 1], ["user_id", 1]]
D, [2021-08-23T19:27:19.614338 #12265] DEBUG -- :   User::HABTM_Customers Create (0.0ms)  INSERT INTO "customers_users" ("customer_id", "user_id") VALUES (?, ?)  [["customer_id", 2], ["user_id", 1]]
D, [2021-08-23T19:27:19.614933 #12265] DEBUG -- :   User::HABTM_Customers Create (0.1ms)  INSERT INTO "customers_users" ("customer_id", "user_id") VALUES (?, ?)  [["customer_id", 3], ["user_id", 1]]
D, [2021-08-23T19:27:19.615074 #12265] DEBUG -- :   TRANSACTION (0.0ms)  commit transaction
D, [2021-08-23T19:27:19.615933 #12265] DEBUG -- :   Customer Load (0.1ms)  SELECT "customers".* FROM "customers" INNER JOIN "customers_users" ON "customers"."id" = "customers_users"."customer_id" WHERE "customers_users"."user_id" = ?  [["user_id", 1]]
D, [2021-08-23T19:27:19.616830 #12265] DEBUG -- :    (0.1ms)  SELECT COUNT(*) FROM "customers" WHERE "customers"."id" IN (?, ?, ?)  [["id", 1], ["id", 2], ["id", 3]]
.

Finished in 0.021713s, 46.0559 runs/s, 138.1676 assertions/s.
1 runs, 3 assertions, 0 failures, 0 errors, 0 skips
ruby main.rb --name test_via_select
-- create_table(:customers, {:force=>true})
D, [2021-08-23T19:27:56.680072 #12577] DEBUG -- :    (0.6ms)  SELECT sqlite_version(*)
D, [2021-08-23T19:27:56.680494 #12577] DEBUG -- :    (0.0ms)  DROP TABLE IF EXISTS "customers"
D, [2021-08-23T19:27:56.680750 #12577] DEBUG -- :    (0.1ms)  CREATE TABLE "customers" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "name" varchar)
   -> 0.0042s
-- create_table(:users, {:force=>true})
D, [2021-08-23T19:27:56.680947 #12577] DEBUG -- :    (0.0ms)  DROP TABLE IF EXISTS "users"
D, [2021-08-23T19:27:56.681106 #12577] DEBUG -- :    (0.1ms)  CREATE TABLE "users" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "name" varchar)
   -> 0.0003s
-- create_join_table(:customers, :users, {:force=>true})
D, [2021-08-23T19:27:56.681361 #12577] DEBUG -- :    (0.0ms)  DROP TABLE IF EXISTS "customers_users"
D, [2021-08-23T19:27:56.681527 #12577] DEBUG -- :    (0.1ms)  CREATE TABLE "customers_users" ("customer_id" integer NOT NULL, "user_id" integer NOT NULL)
   -> 0.0004s
D, [2021-08-23T19:27:56.698463 #12577] DEBUG -- :    (0.1ms)  CREATE TABLE "ar_internal_metadata" ("key" varchar NOT NULL PRIMARY KEY, "value" varchar, "created_at" datetime(6) NOT NULL, "updated_at" datetime(6) NOT NULL)
D, [2021-08-23T19:27:56.704510 #12577] DEBUG -- :   ActiveRecord::InternalMetadata Load (0.6ms)  SELECT "ar_internal_metadata".* FROM "ar_internal_metadata" WHERE "ar_internal_metadata"."key" = ? LIMIT ?  [["key", "environment"], ["LIMIT", 1]]
D, [2021-08-23T19:27:56.707978 #12577] DEBUG -- :   TRANSACTION (0.0ms)  begin transaction
D, [2021-08-23T19:27:56.708180 #12577] DEBUG -- :   ActiveRecord::InternalMetadata Create (0.1ms)  INSERT INTO "ar_internal_metadata" ("key", "value", "created_at", "updated_at") VALUES (?, ?, ?, ?)  [["key", "environment"], ["value", "development"], ["created_at", "2021-08-23 17:27:56.707614"], ["updated_at", "2021-08-23 17:27:56.707614"]]
D, [2021-08-23T19:27:56.708315 #12577] DEBUG -- :   TRANSACTION (0.0ms)  commit transaction
Run options: -n test_via_select --seed 30378

# Running:

D, [2021-08-23T19:27:56.728509 #12577] DEBUG -- :   TRANSACTION (0.0ms)  begin transaction
D, [2021-08-23T19:27:56.728686 #12577] DEBUG -- :   Customer Create (0.1ms)  INSERT INTO "customers" ("name") VALUES (?)  [["name", "UPS"]]
D, [2021-08-23T19:27:56.728810 #12577] DEBUG -- :   TRANSACTION (0.0ms)  commit transaction
D, [2021-08-23T19:27:56.729062 #12577] DEBUG -- :   TRANSACTION (0.0ms)  begin transaction
D, [2021-08-23T19:27:56.729168 #12577] DEBUG -- :   Customer Create (0.0ms)  INSERT INTO "customers" ("name") VALUES (?)  [["name", "DHL"]]
D, [2021-08-23T19:27:56.729282 #12577] DEBUG -- :   TRANSACTION (0.0ms)  commit transaction
D, [2021-08-23T19:27:56.729509 #12577] DEBUG -- :   TRANSACTION (0.0ms)  begin transaction
D, [2021-08-23T19:27:56.729613 #12577] DEBUG -- :   Customer Create (0.0ms)  INSERT INTO "customers" ("name") VALUES (?)  [["name", "FedEx"]]
D, [2021-08-23T19:27:56.729703 #12577] DEBUG -- :   TRANSACTION (0.0ms)  commit transaction
D, [2021-08-23T19:27:56.729928 #12577] DEBUG -- :   TRANSACTION (0.0ms)  begin transaction
D, [2021-08-23T19:27:56.730032 #12577] DEBUG -- :   Customer Create (0.0ms)  INSERT INTO "customers" ("name") VALUES (?)  [["name", "Other"]]
D, [2021-08-23T19:27:56.730121 #12577] DEBUG -- :   TRANSACTION (0.0ms)  commit transaction
D, [2021-08-23T19:27:56.732036 #12577] DEBUG -- :   TRANSACTION (0.0ms)  begin transaction
D, [2021-08-23T19:27:56.732146 #12577] DEBUG -- :   User Create (0.0ms)  INSERT INTO "users" ("name") VALUES (?)  [["name", "manager"]]
D, [2021-08-23T19:27:56.732268 #12577] DEBUG -- :   TRANSACTION (0.0ms)  commit transaction
D, [2021-08-23T19:27:56.740462 #12577] DEBUG -- :   TRANSACTION (0.1ms)  begin transaction
D, [2021-08-23T19:27:56.744168 #12577] DEBUG -- :   User::HABTM_Customers Create (0.1ms)  INSERT INTO "customers_users" ("customer_id", "user_id") VALUES (?, ?)  [["customer_id", 1], ["user_id", 1]]
D, [2021-08-23T19:27:56.744737 #12577] DEBUG -- :   User::HABTM_Customers Create (0.0ms)  INSERT INTO "customers_users" ("customer_id", "user_id") VALUES (?, ?)  [["customer_id", 2], ["user_id", 1]]
D, [2021-08-23T19:27:56.745251 #12577] DEBUG -- :   User::HABTM_Customers Create (0.0ms)  INSERT INTO "customers_users" ("customer_id", "user_id") VALUES (?, ?)  [["customer_id", 3], ["user_id", 1]]
D, [2021-08-23T19:27:56.745390 #12577] DEBUG -- :   TRANSACTION (0.0ms)  commit transaction
D, [2021-08-23T19:27:56.746408 #12577] DEBUG -- :    (0.1ms)  SELECT COUNT(*) FROM "customers" WHERE "customers"."id" IN (SELECT "customers"."id" FROM "customers" INNER JOIN "customers_users" ON "customers"."id" = "customers_users"."customer_id" WHERE "customers_users"."user_id" = ?)  [["user_id", 1]]
F

Failure:
BugTest#test_via_select [main.rb:92]:
Expected false to be truthy.


rails test main.rb:82



Finished in 0.020929s, 47.7799 runs/s, 95.5597 assertions/s.
1 runs, 2 assertions, 1 failures, 0 errors, 0 skips

Expected behaviour

The definitions should behave the same.

Actual behaviour

It matters whether I use an array of IDs or a sub-query:

ability.can? :read, Customer.first

With can :manage, Customer, user.customer_ids this works, but can :manage, Customer, user.customers.select(:id) it fails (see test cases in the Gist above).

Related issue

The user.customer_ids variant generates a potentially huge query (I've seen a prepared statement in production with 17k placeholders).

Compare the SQL query of Customer.accessible_by(ability).count for:

  1. can :manage, Customer, user.customer_ids:
    -- user.customer_ids
    SELECT "customers".*
    FROM "customers"
    INNER JOIN "customers_users" ON "customers"."id" = "customers_users"."customer_id"
    WHERE "customers_users"."user_id" = ?  
    
    -- Customer.accessible_by
    SELECT COUNT(*)
    FROM "customers"
    WHERE "customers"."id" IN (?, ?, ?)
    
  2. can :manage, Customer, user.customers.select(:id):
    -- Customer.accessible_by
    SELECT COUNT(*)
    FROM "customers"
    WHERE "customers"."id" IN (
      -- user.customers.select(:id)
      SELECT "customers"."id"
      FROM "customers"
      INNER JOIN "customers_users" ON "customers"."id" = "customers_users"."customer_id"
      WHERE "customers_users"."user_id" = ?
    )
    

System configuration

Rails version: 6.1

Ruby version: 2.7

CanCanCan version: 3.3.0

dmke avatar Aug 23 '21 11:08 dmke

I'd refactor this to query the customers based on either a scope or, as this is a simple association here, using the foreign keys:


class Customer
  belongs_to :user, inverse_of: :customers
  scope :for_user, scope :for_user, ->(user) { where(user_id: user.id) }
end

class Ability
  include CanCan::Ability

  def initialize(user)
    can :manage, Customer, user_id: user.id
    can :manage, Customer.for_user(user)
  end
end

milgner avatar Oct 21 '21 12:10 milgner