Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Significantly optimize #without_role #500

Merged
merged 3 commits into from
Feb 27, 2019

Conversation

sergey-alekseev
Copy link
Contributor

Imagine a big production application with millions of users.

The previous version generates 3 queries for User.without_role(:admin):

  1. For user.all in #all_except:
SELECT "users".* FROM "users"
  1. For self.with_role(role_name, resource) in #without_role:
SELECT "users".* FROM "users" INNER JOIN "users_roles" ON "users_roles"."user_id" = "users"."id" INNER JOIN "roles" ON "roles"."id" = "users_roles"."role_id" WHERE (((roles.name = 'admin') AND (roles.resource_type IS NULL) AND (roles.resource_id IS NULL)))
  1. For user.where(prime_key => (user.all - excluded_obj).map(&prime_key)) in #all_except:
SELECT "users".* FROM "users" WHERE "users"."id" IN (1, 2, ..., N)

where N is a number of users without the admin role.

While a number of values in the IN clause for the third query can be huge there is even a worse problem. First, user.all - excluded_obj creates an Array of users and then (user.all - excluded_obj).map(&prime_key) calls a primary key method on every object in this array.

From tests

Before (3 queries):

SELECT "customers".* FROM "customers"
SELECT "customers".* FROM "customers" INNER JOIN "customers_privileges" ON "customers_privileges"."customer_id" = "customers"."id" INNER JOIN "privileges" ON "privileges"."id" = "customers_privileges"."privilege_id" WHERE (((privileges.name = 'admin') AND (privileges.resource_type IS NULL) AND (privileges.resource_id IS NULL)))
SELECT "customers".* FROM "customers" WHERE "customers"."id" IN (2, 3, 4)

After (1 query with a subquery):

SELECT "customers".* FROM "customers" WHERE ("customers"."id" NOT IN (SELECT "customers"."id" FROM "customers" INNER JOIN "customers_privileges" ON "customers_privileges"."customer_id" = "customers"."id" INNER JOIN "privileges" ON "privileges"."id" = "customers_privileges"."privilege_id" WHERE (((privileges.name = 'admin') AND (privileges.resource_type IS NULL) AND (privileges.resource_id IS NULL)))))

since RolifyCommunity@522c0d7 Rails < 4 is no longer supported
Imagine a big production application with millions of users.

The previous version generates 3 queries for `User.without_role(:admin)`:
1) For `user.all` in [`#all_except`](https://github.com/RolifyCommunity/rolify/blob/522c0d7d84a70dd3576b569edf19f9f6cb118a61/lib/rolify/adapters/active_record/role_adapter.rb#L83):
```sql
SELECT "users".* FROM "users"
```
2) For `self.with_role(role_name, resource)` in [`#without_role`](https://github.com/RolifyCommunity/rolify/blob/522c0d7d84a70dd3576b569edf19f9f6cb118a61/lib/rolify/finders.rb#L8):
```sql
SELECT "users".* FROM "users" INNER JOIN "users_roles" ON "users_roles"."user_id" = "users"."id" INNER JOIN "roles" ON "roles"."id" = "users_roles"."role_id" WHERE (((roles.name = 'admin') AND (roles.resource_type IS NULL) AND (roles.resource_id IS NULL)))
```
3) For `user.where(prime_key => (user.all - excluded_obj).map(&prime_key))` in [`#all_except`](https://github.com/RolifyCommunity/rolify/blob/522c0d7d84a70dd3576b569edf19f9f6cb118a61/lib/rolify/adapters/active_record/role_adapter.rb#L83):
```sql
SELECT "users".* FROM "users" WHERE "users"."id" IN (1, 2, ..., N)
```
where `N` is a number of users without the `admin` role.

While a number of values in the `IN` clause for the third query can be huge there is even a worse problem. First, `user.all - excluded_obj` creates an `Array` of users and then `(user.all - excluded_obj).map(&prime_key)` calls a primary key method on every object in this array.

From tests
---

Before (3 queries):
```sql
SELECT "customers".* FROM "customers"
SELECT "customers".* FROM "customers" INNER JOIN "customers_privileges" ON "customers_privileges"."customer_id" = "customers"."id" INNER JOIN "privileges" ON "privileges"."id" = "customers_privileges"."privilege_id" WHERE (((privileges.name = 'admin') AND (privileges.resource_type IS NULL) AND (privileges.resource_id IS NULL)))
SELECT "customers".* FROM "customers" WHERE "customers"."id" IN (2, 3, 4)
```

After (1 query with a subquery):
```sql
SELECT "customers".* FROM "customers" WHERE ("customers"."id" NOT IN (SELECT "customers"."id" FROM "customers" INNER JOIN "customers_privileges" ON "customers_privileges"."customer_id" = "customers"."id" INNER JOIN "privileges" ON "privileges"."id" = "customers_privileges"."privilege_id" WHERE (((privileges.name = 'admin') AND (privileges.resource_type IS NULL) AND (privileges.resource_id IS NULL)))))
```
@coveralls
Copy link

Coverage Status

Coverage increased (+1.1%) to 93.393% when pulling 4694142 on sergey-alekseev:master into 522c0d7 on RolifyCommunity:master.

5 similar comments
@coveralls
Copy link

Coverage Status

Coverage increased (+1.1%) to 93.393% when pulling 4694142 on sergey-alekseev:master into 522c0d7 on RolifyCommunity:master.

@coveralls
Copy link

Coverage Status

Coverage increased (+1.1%) to 93.393% when pulling 4694142 on sergey-alekseev:master into 522c0d7 on RolifyCommunity:master.

@coveralls
Copy link

Coverage Status

Coverage increased (+1.1%) to 93.393% when pulling 4694142 on sergey-alekseev:master into 522c0d7 on RolifyCommunity:master.

@coveralls
Copy link

Coverage Status

Coverage increased (+1.1%) to 93.393% when pulling 4694142 on sergey-alekseev:master into 522c0d7 on RolifyCommunity:master.

@coveralls
Copy link

Coverage Status

Coverage increased (+1.1%) to 93.393% when pulling 4694142 on sergey-alekseev:master into 522c0d7 on RolifyCommunity:master.

@coveralls
Copy link

coveralls commented Dec 29, 2018

Coverage Status

Coverage increased (+0.2%) to 92.516% when pulling cbecced on sergey-alekseev:master into 51cad06 on RolifyCommunity:master.

@sergey-alekseev
Copy link
Contributor Author

@EppO some jobs in the Travis CI build mistakenly failed with The command "gem update --system" failed and exited with 1 during . Can you restart those?

BTW any reason for Rolify::Finders#with_role not accepting an array of role names while Rolify::Resource#with_role does accept an Array? (Hence the same for #without_role).
In my view the difference between these two modules and their methods is confusing in the current version of README. I've recently left a comment regarding this in another issue.

Copy link
Member

@EppO EppO left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@EppO
Copy link
Member

EppO commented Feb 27, 2019

BTW any reason for Rolify::Finders#with_role not accepting an array of role names while Rolify::Resource#with_role does accept an Array? (Hence the same for #without_role).
In my view the difference between these two modules and their methods is confusing in the current version of README. I've recently left a comment regarding this in another issue.

No there is no reason and I agree it's confusing. Any PR regarding that matter would be greatly appreciated

@EppO EppO merged commit 199ca1e into RolifyCommunity:master Feb 27, 2019
@EppO
Copy link
Member

EppO commented Feb 27, 2019

Thanks for your PR!

@EppO EppO added this to the rolify 5.3 milestone Feb 28, 2019
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants