{"id":15430811,"url":"https://github.com/bajena/ams_lazy_relationships","last_synced_at":"2025-04-09T06:12:53.990Z","repository":{"id":33920772,"uuid":"162046840","full_name":"Bajena/ams_lazy_relationships","owner":"Bajena","description":"ActiveModel Serializers addon for eliminating N+1 queries problem from the serializers.","archived":false,"fork":false,"pushed_at":"2024-03-19T00:45:10.000Z","size":152,"stargazers_count":86,"open_issues_count":6,"forks_count":11,"subscribers_count":3,"default_branch":"master","last_synced_at":"2025-04-09T06:12:49.570Z","etag":null,"topics":["batch-loading","lazy-loading","rails","ruby","serialization","serializer"],"latest_commit_sha":null,"homepage":"","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/Bajena.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2018-12-16T22:27:59.000Z","updated_at":"2024-10-30T16:26:46.000Z","dependencies_parsed_at":"2024-06-21T02:14:45.093Z","dependency_job_id":"4d9d19ab-45b2-4619-9ace-57cd9d3f741c","html_url":"https://github.com/Bajena/ams_lazy_relationships","commit_stats":{"total_commits":82,"total_committers":5,"mean_commits":16.4,"dds":"0.25609756097560976","last_synced_commit":"35abffbf239b9581e415c85cdbda2abe4ce0ea95"},"previous_names":[],"tags_count":11,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Bajena%2Fams_lazy_relationships","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Bajena%2Fams_lazy_relationships/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Bajena%2Fams_lazy_relationships/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Bajena%2Fams_lazy_relationships/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/Bajena","download_url":"https://codeload.github.com/Bajena/ams_lazy_relationships/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":247987285,"owners_count":21028895,"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":["batch-loading","lazy-loading","rails","ruby","serialization","serializer"],"created_at":"2024-10-01T18:18:55.084Z","updated_at":"2025-04-09T06:12:53.970Z","avatar_url":"https://github.com/Bajena.png","language":"Ruby","funding_links":[],"categories":[],"sub_categories":[],"readme":"[![Build Status](https://travis-ci.com/Bajena/ams_lazy_relationships.svg?branch=master)](https://travis-ci.com/Bajena/ams_lazy_relationships)\n[![Maintainability](https://api.codeclimate.com/v1/badges/c21b988e09db63396309/maintainability)](https://codeclimate.com/github/Bajena/ams_lazy_relationships/maintainability)\n[![Test Coverage](https://api.codeclimate.com/v1/badges/c21b988e09db63396309/test_coverage)](https://codeclimate.com/github/Bajena/ams_lazy_relationships/test_coverage)\n\n# AmsLazyRelationships\n\n#### What does the gem do?\nEliminates N+1 queries problem in [Active Model Serializers gem](https://github.com/rails-api/active_model_serializers) thanks to batch loading provided by a great [BatchLoader gem](https://github.com/exAspArk/batch-loader).\n\nThe gem provides a module which defines a set of methods useful for eliminating N+1 query problem\nduring the serialization. Serializers will first prepare a tree of \"promises\"\nfor every nested lazy relationship. The relationship promises will be\nevaluated only when they're requested.\nE.g. when including `blog_posts.user`: instead of loading a user for each blog post separately it'll gather the blog posts and load all their users at once when including the users in the response.\n\n#### How is it better than Rails' includes/joins methods?\nIn many cases it's fine to use [`includes`](https://apidock.com/rails/ActiveRecord/QueryMethods/includes) method provided by Rails. \nThere are a few problems with `includes` approach though:\n- It loads all the records provided in the arguments hash. Often you may not need all the nested records to serialize the data you want. `AmsLazyRelationships` will load only the data you need thanks to lazy evaluation.\n- When the app gets bigger and bigger you'd need to update all the `includes` statements across your app to prevent the N+1 queries problem which quickly becomes impossible.\n- It lets you remove N+1s even when not all relationships are ActiveRecord models (e.g. some records are stored in a MySQL DB and other models are stored in Cassandra)\n\n## Installation\n\n1. Add this line to your application's Gemfile:\n\n```ruby\ngem \"ams_lazy_relationships\"\n```\n\n2. Execute:\n```\n$ bundle\n```\n\n3. Include `AmsLazyRelationships::Core` module in your base serializer\n\n```ruby\nclass BaseSerializer \u003c ActiveModel::Serializer\n  include AmsLazyRelationships::Core\nend\n```\n\n4. **Important:** \nThis gem uses `BatchLoader` heavily. I highly recommend to clear the batch loader's cache between HTTP requests.\nTo do so add a following middleware:\n`config.middleware.use BatchLoader::Middleware` to your app's `application.rb`.\n\nFor more info about the middleware check out BatchLoader gem docs: https://github.com/exAspArk/batch-loader#caching\n\n## Usage\nAdding the `AmsLazyRelationships::Core` module lets you define lazy relationships in your serializers:\n```ruby\n\nclass UserSerializer \u003c BaseSerializer\n  # Short version - preloads a specified ActiveRecord relationship by default\n  lazy_has_many :blog_posts\n  \n  # Works same as the previous one, but the loader option is specified explicitly\n  lazy_has_many :blog_posts,\n                serializer: BlogPostSerializer,\n                loader: AmsLazyRelationships::Loaders::Association.new(\"User\", :blog_posts)\n  \n  # The previous one is a shorthand for the following lines:\n  lazy_relationship :blog_posts, loader: AmsLazyRelationships::Loaders::Association.new(\"User\", :blog_posts)\n  has_many :blog_posts, serializer: BlogPostSerializer do |serializer|\n    # non-proc custom finder will work as well, but it can produce redundant sql\n    # queries, please see [Example 2: Modifying the relationship before rendering](#example-2-modifying-the-relationship-before-rendering)\n    -\u003e { serializer.lazy_blog_posts }\n  end\n   \n  lazy_has_one :poro_model, loader: AmsLazyRelationships::Loaders::Direct.new(:poro_model) { |object| PoroModel.new(object) }\n  \n  lazy_belongs_to :account, loader: AmsLazyRelationships::Loaders::SimpleBelongsTo.new(\"Account\")\n  \n  lazy_has_many :comment, loader: AmsLazyRelationships::Loaders::SimpleHasMany.new(\"Comment\", foreign_key: :user_id)\nend\n```\n\nAs you may have already noticed the gem makes use of various loader classes. \n\nI've implemented the following ones for you:\n- `AmsLazyRelationships::Loaders::Association` - Batch loads a ActiveRecord association (has_one/has_many/has_many-through/belongs_to). This is a deafult loader in case you don't specify a `loader` option in your serializer's lazy relationship.\nE.g. in order to lazy load user's blog posts use a following loader: `AmsLazyRelationships::Loaders::Association.new(\"User\", :blog_posts)`.\n\n- `AmsLazyRelationships::Loaders::SimpleBelongsTo` - Batch loads ActiveRecord models using a foreign key method called on a serialized object. E.g. `AmsLazyRelationships::Loaders::SimpleBelongsTo.new(\"Account\")` called on users will gather their `account_id`s and fire one query to get all accounts at once instead of loading an account per user separately. \nThis loader can be useful e.g. when the serialized object is not an ActiveRecord model.\n\n- `AmsLazyRelationships::Loaders::SimpleHasMany` - Batch loads ActiveRecord records belonging to given record by foreign key. E.g. `AmsLazyRelationships::Loaders::SimpleHasMany.new(\"BlogPosts\", foreign_key: :user_id)` called on users will  and fire one query to gather all blog posts for the users at once instead of loading an the blog posts per user separately.\nThis loader can be useful e.g. when the serialized object is not an ActiveRecord model.\n\n- `AmsLazyRelationships::Loaders::Direct` - Lazy loads data in a \"dumb\" way - just executes the provided block when needed. Useful e.g. when the relationship is just a PORO which then in its own serializer needs to lazy load some relationships.\nYou can use it like this: `AmsLazyRelationships::Loaders::Direct.new(:poro_model) { |object| PoroModel.new(object)`.\n\nThe abovementioned loaders are mostly useful when using ActiveRecord, but there should be no problem building a new loader for different frameworks.\nIf you're missing a loader you can create an issue or create your own loader taking the existing ones as an example. \n\n### More examples\nHere are a few use cases for the lazy relationships. Hopefully they'll let you understand a bit more how the gem works.\n\n#### Example 1: Basic ActiveRecord relationships\nIf the relationships in your serializers are plain old ActiveRecord relationships you're lucky, because ams_lazy_relationships by default assumes that the relationship is an ActiveRecord relationship, so you can use the simplest syntax.\nImagine you have an endpoint that renders a list of blog posts and includes their comments.\nThe N+1 prone way of defining the serializer would be:\n```ruby\nclass BlogPostSerializer \u003c BaseSerializer\n  has_many :comments\nend\n```\n\nTo prevent loading comments using a separate DB query for each post just change it to:\n```ruby\nclass BlogPostSerializer \u003c BaseSerializer\n  lazy_has_many :comments\nend\n```\n\n#### Example 2: Modifying the relationship before rendering\nSometimes it may happen that you need to process the relationship before rendering, e.g. decorate the records. In this case the gem provides a special method (in our case `lazy_comments`) for each defined relationship. Check out the example - we'll decorate every comment before serializing:\n\n```ruby\nclass BlogPostSerializer \u003c BaseSerializer\n  lazy_has_many :comments do |serializer|\n    -\u003e { serializer.lazy_comments.map(\u0026:decorate) }\n   end\nend\n```\n\nDespite the fact that non-block custom finder such as\n\n```ruby\nclass BlogPostSerializer \u003c BaseSerializer\n  lazy_has_many :comments do |serializer|\n    serializer.lazy_comments.map(\u0026:decorate)\n   end\nend\n```\n\nwill work still, it's better to implement it in a form of lambda, in order to avoid redundant SQL queries when `include_data` AMS setting appears to be `false`:\n\n```ruby\nclass BlogPostSerializer \u003c BaseSerializer\n  lazy_has_many :comments do |serializer|\n    include_data :if_sideloaded\n    -\u003e { serializer.lazy_comments.map(\u0026:decorate) }\n   end\nend\n```\n\nFeel free to skip custom lazy finder for association if your goal is just to define `include_data` setting and/or to specify some links and metas:\n\n```ruby\nclass BlogPostSerializer \u003c BaseSerializer\n  lazy_has_many :comments do\n    include_data :if_sideloaded\n    link :self, 'a link'\n    meta name: 'Dan Brown'\n   end\nend\n```\n\n#### Example 3: Introducing loader classes\nUnder the hood ams_lazy_relationships uses special loader classes to batch load the relationships. By default the gem uses serializer class names and relationship names to instantiate correct loaders, but it may happen that e.g. your serializer's class name doesn't match the model name (e.g. your model's name is `BlogPost` but the serializer's name is `PostSerializer`).\n\nIn this case you can define the lazy relationship by passing a correct loader param:\n```ruby\nclass PostSerializer \u003c BaseSerializer\n  lazy_has_many :comments, serializer: CommentSerializer,\n    loader: AmsLazyRelationships::Loaders::Association.new(\n              \"BlogPost\", :comments\n            )\nend\n```\n\n#### Example 4: Non ActiveRecord -\u003e ActiveRecord relationships\nThis one is interesting. It may happen that the root record is not an ActiveRecord model (e.g. a Cequel model), however its relationship is an AR model.\nImagine that `BlogPost` is not an AR model and `Comment` is a standard AR model. The lazy relationship would look like this:\n```ruby\nclass BlogPostSerializer \u003c BaseSerializer\n  lazy_has_many :comments, \n    loader: AmsLazyRelationships::Loaders::SimpleHasMany.new(\n      \"Comment\", foreign_key: :blog_post_id\n    )\nend\n```\n\n#### Example 5: Use lazy relationship without rendering it\nSometimes you may just want to make use of lazy relationship without rendering the whole nested record. \nFor example imagine that your `BlogPost` serializer is supposed to render `author_name` attribute. You can define the lazy relationship and just use it in other attribute evaluator:\n\n```ruby\nclass BlogPostSerializer \u003c BaseSerializer\n  lazy_relationship :author\n  \n  attribute :author_name do\n    lazy_author.name\n  end\nend\n```\n\n#### Example 6: Lazy dig through relationships\nIn additional to previous example you may want to make use of nested lazy relationship without rendering of any nested record.\nThere is an `lazy_dig` method to be used for that:\n\n```ruby\nclass AuthorSerializer \u003c BaseSerializer\n  lazy_relationship :address\nend\n\nclass BlogPostSerializer \u003c BaseSerializer\n  lazy_relationship :author\n\n  attribute :author_address do\n    lazy_dig(:author, :address)\u0026.full_address\n  end\nend\n```\n\n## Performance comparison with vanilla AMS\n\nIn general the bigger and more complex your serialized records hierarchy is and the more latency you have in your DB the more you'll benefit from using this gem. \nExample results for average size records tree (10 blog posts -\u003e 10 comments each -\u003e 1 user per comment, performed on local in-memory SQLite DB) are:\n\n### Time:\n\n```bash\n# With lazy relationships:    0.860000   0.010000   0.870000 (  0.870297)\n# Vanilla AMS:                1.050000   0.000000   1.050000 (  1.059801)\n```\n\nThis means your serializers should get **~13%** speed boost by introducing lazy relationships.\n\n### Memory:\n\n```bash\n# With lazy relationships:\n#                         46.283M memsize (     0.000  retained)\n#                        506.696k objects (     0.000  retained)\n#                         50.000  strings (     0.000  retained)\n# Vanilla AMS:            42.738M memsize (     0.000  retained)\n#                        545.266k objects (     0.000  retained)\n#                         50.000  strings (     0.000  retained)\n```\n\nThis means that serialization may consume **~5%** more memory.\n\nDetailed benchmark script \u0026 results can be found [here](/spec/benchmark_spec.rb).\n\n## Development\n\nAfter checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.\n\nTo install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).\n\n## Contributing\n\nBug reports and pull requests are welcome on GitHub at https://github.com/Bajena/ams_lazy_relationships.\n\n## License\n\nThe gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fbajena%2Fams_lazy_relationships","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fbajena%2Fams_lazy_relationships","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fbajena%2Fams_lazy_relationships/lists"}