{"id":14955427,"url":"https://github.com/maxlap/activerecord_where_assoc","last_synced_at":"2025-04-14T01:52:26.160Z","repository":{"id":23312206,"uuid":"98692435","full_name":"MaxLap/activerecord_where_assoc","owner":"MaxLap","description":"Make ActiveRecord do conditions on your associations","archived":false,"fork":false,"pushed_at":"2025-04-07T11:53:14.000Z","size":973,"stargazers_count":230,"open_issues_count":1,"forks_count":12,"subscribers_count":5,"default_branch":"master","last_synced_at":"2025-04-14T01:52:22.043Z","etag":null,"topics":["activerecord","rails","rails4","rails5","rails6","ruby","sql"],"latest_commit_sha":null,"homepage":null,"language":"Ruby","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/MaxLap.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":".github/FUNDING.yml","license":"LICENSE.txt","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":"SECURITY.md","support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null},"funding":{"github":"MaxLap","ko_fi":"maxlap","tidelift":"rubygems/activerecord_where_assoc"}},"created_at":"2017-07-28T22:21:28.000Z","updated_at":"2025-04-10T05:12:57.000Z","dependencies_parsed_at":"2024-05-18T14:38:49.813Z","dependency_job_id":"0816da7e-6e80-4f57-8e0a-e528590de188","html_url":"https://github.com/MaxLap/activerecord_where_assoc","commit_stats":{"total_commits":331,"total_committers":7,"mean_commits":"47.285714285714285","dds":0.05740181268882172,"last_synced_commit":"11f3e0940fe9d3f26667bd1e0c8701c692db45dd"},"previous_names":[],"tags_count":14,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/MaxLap%2Factiverecord_where_assoc","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/MaxLap%2Factiverecord_where_assoc/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/MaxLap%2Factiverecord_where_assoc/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/MaxLap%2Factiverecord_where_assoc/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/MaxLap","download_url":"https://codeload.github.com/MaxLap/activerecord_where_assoc/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":248809032,"owners_count":21164895,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["activerecord","rails","rails4","rails5","rails6","ruby","sql"],"created_at":"2024-09-24T13:11:08.534Z","updated_at":"2025-04-14T01:52:26.136Z","avatar_url":"https://github.com/MaxLap.png","language":"Ruby","funding_links":["https://github.com/sponsors/MaxLap","https://ko-fi.com/maxlap","https://tidelift.com/funding/github/rubygems/activerecord_where_assoc"],"categories":[],"sub_categories":[],"readme":"# ActiveRecord Where Assoc\n\n![Test supported versions](https://github.com/MaxLap/activerecord_where_assoc/workflows/Test%20supported%20versions/badge.svg)\n[![Code Climate](https://codeclimate.com/github/MaxLap/activerecord_where_assoc/badges/gpa.svg)](https://codeclimate.com/github/MaxLap/activerecord_where_assoc)\n\nThis gem makes it easy to do conditions based on the associations of your records in ActiveRecord (Rails). (Using SQL's `EXISTS` operator)\n\n```ruby\n# Find my_post's comments that were not made by an admin\nmy_post.comments.where_assoc_not_exists(:author, is_admin: true).where(...)\n\n# Find every posts that have comments by an admin\nPost.where_assoc_exists([:comments, :author], \u0026:admins).where(...)\n\n# Find my_user's posts that have at least 5 non-spam comments (not_spam is a scope on comments)\nmy_user.posts.where_assoc_count(:comments, :\u003e=, 5) { |comments| comments.not_spam }.where(...)\n```\n\nThese allow for powerful, chainable, clear and easy to reuse queries. (Great for scopes)\n\nHere is an [introduction to this gem](INTRODUCTION.md).\n\nYou avoid many [problems with the alternative options](ALTERNATIVES_PROBLEMS.md).\n\nHere are [many examples](EXAMPLES.md), including the generated SQL queries.\n\n## Advantages\n\nThese methods have many advantages over the alternative ways of achieving the similar results:\n* Avoids the [problems with the alternative ways](ALTERNATIVES_PROBLEMS.md)\n* Can be chained and nested with regular ActiveRecord methods (`where`, `merge`, `scope`, etc).\n* Adds a single condition in the `WHERE` of the query instead of complex things like joins.\n  So it's easy to have multiple conditions on the same association\n* Handles `has_one` correctly: only testing the \"first\" record of the association that matches the default_scope and the scope on the association itself.\n* Handles recursive associations (such as parent/children) seemlessly.\n* Can be used to quickly generate a SQL query that you can edit/use manually.\n\n## Installation\n\nRails 4.1 to 7.0 are supported with Ruby 2.1 to 3.1. Tested against SQLite3, PostgreSQL and MySQL. The gem\nonly depends on the `activerecord` gem.\n\nAdd this line to your application's Gemfile:\n\n```ruby\ngem 'activerecord_where_assoc', '~\u003e 1.0'\n```\n\nAnd then execute:\n\n    $ bundle install\n\nOr install it yourself with:\n\n    $ gem install activerecord_where_assoc\n\n## Development state\n\nThis gem is feature complete and production ready.\u003cbr\u003e\nOther than rare tweaks as new versions of Rails and Ruby are released, there shouldn't be much activity on this repository.\n\n## Documentation\n\nThe [documentation is nicely structured](https://maxlap.github.io/activerecord_where_assoc/ActiveRecordWhereAssoc/RelationReturningMethods.html)\n\nIf you prefer to see it in the code, the main methods are in [this file](https://github.com/MaxLap/activerecord_where_assoc/blob/master/lib/active_record_where_assoc/relation_returning_methods.rb)\nand the ones that return SQL parts are in [this one](https://github.com/MaxLap/activerecord_where_assoc/blob/master/lib/active_record_where_assoc/sql_returning_methods.rb)\n\nHere are some [usage tips](#usage-tips)\n\n## Usage\n\nYou can view [many examples](EXAMPLES.md).\n\nOtherwise, here is a short explanation of the main methods provided by this gem:\n\n```ruby\nwhere_assoc_exists(association_name, conditions, options, \u0026block)\nwhere_assoc_not_exists(association_name, conditions, options, \u0026block)\nwhere_assoc_count(left_assoc_or_value, operator, right_assoc_or_value, conditions, options, \u0026block)\n```\n\n* These methods add a condition (a `#where`) that checks if the association exists (or not)\n* You can specify condition on the association, so you could check only for comments that are made by an admin.\n* Each method returns a new relation, meaning you can chain `#where`, `#order`, `limit`, etc.\n* common arguments:\n  * association_name: the association we are doing the condition on.\n  * conditions: (optional) the condition to apply on the association. It can be anything that `#where` can receive, so: Hash, String and Array (string with binds).\n  * options: [available options](https://maxlap.github.io/activerecord_where_assoc/ActiveRecordWhereAssoc/RelationReturningMethods#module-ActiveRecordWhereAssoc::RelationReturningMethods-label-Options) to alter some behaviors. (rarely necessary)\n  * block: adds more complex conditions by receiving a relation on the association. Can use `#where`, `#where_assoc_*`, scopes, and other scoping methods.\u003cbr\u003e\n    Must return a relation.\u003cbr\u003e\n    The block either:\n    * receives no argument, in which case `self` is set to the relation, so you can do `{ where(id: 123) }`\n    * receives arguments, in which case the block is called with the relation as first parameter.\n\n    The block should return the new relation to use or `nil` to do as if there were no blocks.\u003cbr\u003e\n    It's common to use `where_assoc_*(..., \u0026:scope_name)` to use a single scope.\n* `#where_assoc_count` is a generalization of `#where_assoc_exists` and `#where_assoc_not_exists`. It behaves the same way, but is more powerful, as it allows you to specify how many matches there should be.\n    ```ruby\n    # These are equivalent:\n    Post.where_assoc_exists(:comments, is_spam: true)\n    Post.where_assoc_count(:comments, :\u003e=, 1, is_spam: true)\n\n    Post.where_assoc_not_exists(:comments, is_spam: true)\n    Post.where_assoc_count(:comments, :==, 0, is_spam: true)\n\n    # This has no equivalent (Posts with at least 5 spam comments)\n    Post.where_assoc_count(:comments, :\u003e=, 5, is_spam: true)\n    ```\n\n## Intuition\n\nHere is the basic intuition for the methods:\n\n`#where_assoc_exists` filters the models, returning those *where* a record for the *association* matching a condition (by default any record in the association) *exists*.\n\n`#where_assoc_not_exists` is the exact opposite of `#where_assoc_exists`. Filters the models, returning those *where* a record for the *association* matching a condition (by default any record in the association) do *not exists*\n\n`#where_assoc_count` the more specific version of `#where_assoc_exists`. Filters the models, returning those *where* a record for the *association* matching a condition (by default any record in the association) do *not exists*\n\nThe condition that you may need on the record can be quite complicated. For this reason, you can pass a block to these methods.\nThe block will receive a relation on records of the association. Your job is then to call `where` and scopes to specify what you want to exist (or to not exist if using `#where_assoc_not_exists`).\n\nSo if you have `User.where_assoc_exists(:comments) {|rel| rel.where(\"content ilike '%github.com%'\") }`, `rel` is a relation is on `Comment`, and you are specifying what you want to exist. So now we are looking for users that made a comment containing 'github.com'.\n\n## Usage tips\n\n### Nested associations\n\nSometimes, there isn't a single association that goes deep enough. In that situation, you can simply nest the scopes:\n\n```ruby\n# Find users that have a post that has a comment that was made by an admin.\n# Using \u0026:admins to use the admins scope (or any other class method of comments)\nUser.where_assoc_exists(:posts) { |posts|\n    posts.where_assoc_exists(:comments) { |comments|\n        comments.where_assoc_exists(:author, \u0026:admins)\n    }\n}\n```\n\nIf you don't need special conditions on any of the intermediary associations, then you can an array as shortcut for multiple steps:\n\n```ruby\n# Same as above\nUser.where_assoc_exists([:posts, :comments, :author], \u0026:admins)\n```\n\nThis shortcut can be used for every `where_assoc_*` methods. The conditions and the block will only be applied to the last association of the chain.\n\n\n### Beware of spreading conditions on multiple calls\n\nThe following have different meanings:\n\n```ruby\nmy_user.posts.where_assoc_exists(:comments_authors, is_admin: true, is_honest: true)\n\nmy_user.posts.where_assoc_exists(:comments_authors, is_admin: true)\n             .where_assoc_exists(:comments_authors, is_honest: true)\n```\n\nThe first is the posts of `my_user` that have a comment made by an honest admin. It requires a single comment to match every conditions.\n\nThe second is the posts of `my_user` that have a comment made by an admin and a comment made by someone honest. It could be the same comment (like the first query) but it could also be 2 different comments.\n\n### Inter-table conditions\n\nIt's possible, with string conditions, to refer to all the tables that are used before the association, including the source model.\n\n```ruby\n# Find posts where the author also commented on the post.\nPost.where_assoc_exists(:comments, \"posts.author_id = comments.author_id\")\n```\n\nNote that some database systems limit how far up you can refer to tables in nested queries. Meaning it's possible that the following query may get refused because of those limits:\n\n```ruby\n# it's hard to come up with a good example...\nPost.where_assoc_exists([:comments, :author, :address], \"addresses.country = posts.database_country\")\n```\n\nDoing the same thing but with less associations between `address` and `posts` would not be an issue.\n\n### Getting SQL strings\n\nSometimes, you may need only the SQL of the condition instead of a whole relation, such as when writing your own complex SQL. There are methods available for this use case: `assoc_exists_sql`, `assoc_not_exists_sql`, `compare_assoc_count_sql`, `only_assoc_count_sql`.\n\nYou can read some more about them in [their documentation](https://maxlap.github.io/activerecord_where_assoc/ActiveRecordWhereAssoc/SqlReturningMethods.html)\n\nHere is a simple example of they use. Note that they should always be called on the class.\n\n```ruby\n    # Users with a post or a comment\n    User.where(\"#{User.assoc_exists_sql(:posts)} OR #{User.assoc_exists_sql(:comments)}\")\n    my_users.where(\"#{User.assoc_exists_sql(:posts)} OR #{User.assoc_exists_sql(:comments)}\")\n    # Note that this could be achieved in Rails 5 using the #or method and #where_assoc_exists\n```\n\n### The opposite of multiple nested EXISTS...\n\n... is a single `NOT EXISTS` with the nested ones still using `EXISTS`.\n\nAll the methods always chain nested associations using an `EXISTS` when they have to go through multiple hoops. Only the outer-most, or first, association will have a `NOT EXISTS` when using `#where_assoc_not_exists` or a `COUNT` when using `#where_assoc_count`. This is the logical way of doing it.\n\n### Using `#from` in scope\n\nIf you want to use a scope / condition which uses `#from`, then you need to use the [:never_alias_limit](https://maxlap.github.io/activerecord_where_assoc/ActiveRecordWhereAssoc/RelationReturningMethods.html#module-ActiveRecordWhereAssoc::RelationReturningMethods-label-3Anever_alias_limit+option) option to avoid `#where_assoc_*` being overwritten by your scope and getting a weird exception / wrong result.\n\n## Known issues/limitations\n\n### MySQL doesn't support sub-limit\nOn MySQL databases, it is not possible to use `has_one` associations and associations with a scope that apply either a limit or an offset.\n\nI do not know of a way to do a SQL query that can deal with all the specifics of `has_one` for MySQL. If you have one, then please suggest it in an issue/pull request.\n\nIn order to work around this, you must use the [ignore_limit](https://maxlap.github.io/activerecord_where_assoc/ActiveRecordWhereAssoc/RelationReturningMethods.html#module-ActiveRecordWhereAssoc::RelationReturningMethods-label-3Aignore_limit+option) option. The behavior is less correct, but better than being unable to use the gem.\n\n### has_* :through vs limit/offset\nFor `has_many` and `has_one` with the `:through` option, `#limit` and `#offset` are ignored. Note that `#limit` and `#offset` of the `:source` and of the `:through` side are applied correctly.\n\nThis is the opposite of what `ActiveRecord` does when you fetch the result of such an association. `ActiveRecord` will ignore the limits of the part `:source` and of the `:through` and only use the one of the `has_* :through`.\n\nIt is pretty complicated to support `#limit` and `#offset` of the `has_* :through` and would require quite a bit of refactoring. PR welcome\n\nNote that the support of `#limit` and `#offset` for the `:source` and `:through` parts is a feature. I consider `ActiveRecord` wrong for not handling them correctly.\n\n## Another recommended gem\n\nIf you feel a need for this gem's feature, you may also be interested in another gem I made: [activerecord_follow_assoc](https://github.com/MaxLap/activerecord_follow_assoc).\n\nIt allows you to follow an association of your choice while building a query (a scope). You start querying posts, and then you change to querying the authors of those posts. For simple cases, it's possible that both `where_assoc` and `follow_assoc` can build the query your need, but each can handle different situations. Here is an example:\n\n```ruby\n# Find every posts that have comments by an admin\nPost.where_assoc_exists([:comments, :author], \u0026:admins)\n```\n\nThis could be done with `follow_assoc`: `User.admins.follow_assoc(:comments, :post)`. But if you wanted conditions on a second association, then `follow_assoc` wouldn't work. On the other hand, if you received a scope on users and wanted their posts, then `follow_assoc` would be a nicer tool for the job. It all depends on the context where you need to do the query and what starting point you have.\n\n## Development\n\nAfter checking out the repo, run `bundle install` to install dependencies.\n\nRun `rake test` to run the tests for the latest version of rails. If you want SQL queries printed when you have failures, use `SQL_WITH_FAILURES=1 rake test`.\n\nRun `bin/console` for an interactive prompt that will allow you to experiment in the same environment as the tests.\n\nRun `bin/fixcop` to fix a lot of common styling mistake from your changes and then display the remaining rubocop rules you break. Make sure to do this before committing and submitting PRs. Use common sense, sometimes it's okay to break a rule, add a [rubocop:disable comment](http://rubocop.readthedocs.io/en/latest/configuration/#disabling-cops-within-source-code) in that situation.\n\n## Contributing\n\nBug reports and pull requests are welcome on GitHub at https://github.com/MaxLap/activerecord_where_assoc.\n\n## License\n\nThe gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).\n\n## Acknowledgements\n\n* [René van den Berg](https://github.com/ReneB) for some of the code of [activerecord-like](https://github.com/ReneB/activerecord-like) used for help with setting up the tests\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmaxlap%2Factiverecord_where_assoc","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fmaxlap%2Factiverecord_where_assoc","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmaxlap%2Factiverecord_where_assoc/lists"}