{"id":20657374,"url":"https://github.com/chrislaskey/assoc","last_synced_at":"2025-04-19T12:37:23.313Z","repository":{"id":46166806,"uuid":"168580463","full_name":"chrislaskey/assoc","owner":"chrislaskey","description":"An easy way to manage many_to_many, has_many and belongs_to Ecto associations","archived":false,"fork":false,"pushed_at":"2021-11-09T18:40:07.000Z","size":53,"stargazers_count":6,"open_issues_count":0,"forks_count":2,"subscribers_count":2,"default_branch":"main","last_synced_at":"2024-10-28T22:15:54.378Z","etag":null,"topics":["associations","ecto","elixir","has-many","many-to-many","phoenix"],"latest_commit_sha":null,"homepage":"","language":"Elixir","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/chrislaskey.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE.txt","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2019-01-31T19:07:19.000Z","updated_at":"2023-06-22T03:28:57.000Z","dependencies_parsed_at":"2022-09-17T04:51:05.945Z","dependency_job_id":null,"html_url":"https://github.com/chrislaskey/assoc","commit_stats":null,"previous_names":[],"tags_count":11,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/chrislaskey%2Fassoc","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/chrislaskey%2Fassoc/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/chrislaskey%2Fassoc/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/chrislaskey%2Fassoc/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/chrislaskey","download_url":"https://codeload.github.com/chrislaskey/assoc/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":224953366,"owners_count":17397689,"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":["associations","ecto","elixir","has-many","many-to-many","phoenix"],"created_at":"2024-11-16T18:20:23.254Z","updated_at":"2024-11-16T18:20:23.864Z","avatar_url":"https://github.com/chrislaskey.png","language":"Elixir","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Assoc\n\n[![Build Status](https://travis-ci.com/chrislaskey/assoc.svg?branch=master)](https://travis-ci.com/chrislaskey/assoc)\n\n\u003e Tired of writing custom boilerplate to manage database associations in ecto?\n\nEcto is an incredibly powerful toolkit, and it's flexible enough to fit all kinds of database designs. That flexibility also means you sometimes end up writing a lot of code to accomplish a simple pattern.\n\nAssoc simplifies the code needed to manage common Ecto associations without the custom code and boilerplate. Drop in a `use` statement and define which associations are updatable. It's that easy.\n\n## Quickstart\n\nAdd to `mix.exs`:\n\n```elixir\ndefp deps do\n  [{:assoc, \"~\u003e 0.1\"}]\nend\n```\n\nIn the schema file, include `Assoc.Schema` and define which associations can be updated:\n\n```elixir\ndefmodule MyApp.User do\n  use MyApp.Schema\n  use Assoc.Schema, repo: MyApp.Repo\n\n  schema \"users\" do\n    many_to_many :tags, MyApp.Tag, join_through: \"tags_users\", on_replace: :delete\n  end\n\n  def updatable_associations, do: [\n    tags: MyApp.Tag\n  ]\nend\n```\n\nThen update the associations:\n\n```elixir\nimport Assoc.Updater\n\nparams = %{\n  tags: [\n    %{id: 5}, # Associate existing tag\n    %{id: 8, name: \"updated name\"}, # Associate and update the name of an existing tag\n    %{name: \"new tag\"} # Create and associate new tag\n  ]\n}\n\nupdate_associations(MyApp.Repo, user, params)\n```\n\n## How It Works\n\nThe power of Ecto is that it can be used to fit all kinds of database topologies. The goal of Assoc is to reduce the amount of code needed for some of the most common ones. It's not a replacement for learning Ecto. In fact, under the covers all it's doing is making common ecto calls. Don't let the learning curve of Ecto be intimidating!\n\n### Understanding the Danger\n\nThe first step to dispelling the magic is knowing Assoc uses `put_assoc` to generate a changeset. As a consequence, this means:\n\n\u003e If an association key is included in the params, then the existing associations will be replaced by the param value. This means records can be deleted. When passing association values, always include every association.\n\n### The Good Parts\n\nWith the potential for data loss in mind, Assoc adds some guard-rails and quality of life improvements like:\n\n\u003e If an association key is not included in the params, then the existing associations will not be changed. All the existing associations will remain.\n\n### Step by Step Example\n\nLet's walk through the quickstart example to see each step in action.\n\n```elixir\nparams = %{\n  tags: [\n    %{id: 5}, # Associate existing tag\n    %{id: 8, name: \"updated name\"}, # Associate and update the name of an existing tag\n    %{name: \"new tag\"} # Create and associate new tag\n  ]\n}\n\nupdate_associations(MyApp.Repo, user, params)\n```\n\nThe first thing that happens is the `user` record is examined. The associated struct `MyApp.User` is found, and the `updatable_associations` function defined in the schema file is read.\n\n```elixir\ndef updatable_associations, do: [\n  tags: MyApp.Tag\n]\n```\n\nFrom there, each of the updatable associations is walked through, checking the params for a matching key. When a matching key is found, each value is walked through. In the case of a `has_many` or `many_to_many` this will be a list of values.\n\n#### Associating an Existing Tag\n\nTaking the first value:\n\n```elixir\n%{id: 5}\n```\n\nTo help with `belongs_to` associations, the `user` record id is included in the params:\n\n```elixir\n%{id: 5, user_id: user.id}\n```\n\nThanks to how changesets work, this will be silently removed by associations that don't use it. But available for those that do.\n\nNext it searches the database for an existing `MyApp.Tag` record by the `id` value. If one is found, then the record is updated and returned:\n\n```elixir\ntag_record\n|\u003e MyApp.Tag.changeset(params)\n|\u003e MyApp.Repo.update\n```\n\n#### Associating and Updating an Existing Tag\n\nThe same process is repeated for the second example:\n\n```elixir\n%{id: 8, name: \"updated name\"}\n```\n\nThe only difference is since this includes attributes like `name`, the association values are also updated.\n\n#### Creating a New Tag\n\nFor the last example payload:\n\n```elixir\n%{name: \"new tag\"}\n```\n\nThis one doesn't have an `id`, so instead a record is inserted instead of updated:\n\n```elixir\n%MyApp.Tag{}\n|\u003e MyApp.Tag.changeset(params)\n|\u003e MyApp.Repo.insert\n```\n\nNow that the three `tag` records have been created or updated, they are ready to be passed into a `user` changeset with `put_assoc`:\n\n```elixir\n%{\n  tags: [\n    %MyApp.Tag{id: 5},\n    %MyApp.Tag{id: 8},\n    %MyApp.Tag{id: 10}\n  ]\n}\n```\n\nFor each association param, a `put_assoc` is dynamically added before updating the record:\n\n```elixir\nuser\n|\u003e MyApp.User.associations_changeset(params)\n|\u003e MyApp.Repo.update\n```\n\n## Usage\n\nThe same code works for `many_to_many`, `has_many`, and `belongs_to` associations.\n\n### Using with Pipes\n\nThough the direct call used in the quickstart is handy:\n\n```elixir\nupdate_associations(MyApp.Repo, user, params)\n```\n\nHaving to pass in `MyApp.Repo` as the first argument isn't very pipe friendly.\n\nTo help with this, Assoc supports including the library in a module and passing the repo as an option:\n\n```elixir\ndefmodule MyApp.CreateUser do\n  use Assoc.Updater, repo: MyApp.Repo\n\n  def call(params) do\n    %User{}\n    |\u003e User.changeset(params)\n    |\u003e Repo.insert\n    |\u003e update_associations(params)\n  end\nend\n```\n\nThis removes the requirement to explicitly pass `MyApp.Repo`. To make it even more pipe friendly, the `update_associations` function can take either a `record` directly or a `{:ok, record}` tuple. Any other values are silently returned.\n\n### Params\n\nThe `update_associations` function accepts a wide variety of data sources for association params.\n\nIt takes Structs:\n\n```elixir\n[\n  %MyApp.Tag{id: 5, name: \"existing tag\"},\n  %MyApp.Tag{name: \"new tag\"}\n]\n```\n\nAs well as Maps:\n\n```elixir\n[\n  %{id: 5, name: \"existing tag\"},\n  %{\"id\" =\u003e \"8\", \"name\" =\u003e \"existing tag, too\"},\n  %{\"name\" =\u003e \"new tag\"}\n]\n```\n\nOr any combination of both:\n\n```elixir\n[\n  %MyApp.Tag{id: 5, name: \"existing tag\"},\n  %{\"name\" =\u003e \"new tag\"}\n]\n```\n\n## Examples\n\nAdditional examples showing `belongs_to`, `has_many` and `many_to_many` associations are included in the tests:\n\n- [Test Schemas](https://github.com/chrislaskey/assoc/tree/master/test/setup/schemas)\n- [Test Schema Migrations](https://github.com/chrislaskey/assoc/tree/master/priv/repo/migrations)\n- [Tests](https://github.com/chrislaskey/assoc/tree/master/test/assoc/updater_test.exs)\n\n## Frequently Asked Questions\n\n### Why not use `cast_assoc`?\n\nEcto's `cast_assoc` is great for when associations are always managed through a parent. To illustrate its limitations, take the following example:\n\n```elixir\nparams = %{\n  tags: [\n    %{id: existing_tag.id, name: \"Existing Tag\"}\n  ]\n}\n```\n\nWhen using `cast_assoc`, if the existing tag is already associated with the parent then it'll stay associated, and any values like `name` will be updated.\n\nNow, what if the tag already exists in the database but isn't associated with the parent? One might expect the behaviour to be the same - associate the tag with the parent record and update the values. Though a reasonable assumption, it's wrong.\n\nWhat actually happens is the `id` value is ignored, acting as if it were passed:\n\n```elixir\nparams = %{\n  tags: [\n    %{name: \"Existing Tag\"}\n  ]\n}\n```\n\nAs a result, Ecto will try to create a new tag entry. Depending on the applications constraints, this will either blow up on a uniqueness validation or create two competing tags with the same name.\n\n### `put_assoc` to the rescue\n\nThis example highlights how `cast_assoc` isn't meant to manage cases where a record already exists before being associated with a record. Instead, that's the domain of  `put_assoc`.\n\nWhen dealing with existing associations, there's a lot more edge cases to deal with. Without the tighter constraints `cast_assoc` operates in, `put_assoc` can't automatically manage the relationships without making assumptions. As a result, `put_assoc` only handles associating existing records, without the ability to create new or update existing associations.\n\nThe good news is ecto gives us the tools to write our own custom code to achieve the same results:\n\n- Create new records if they don't exist\n- Update existing records if they do exist\n- Associate all created and updated records with the parent\n\nAnd that is exactly what Assoc is written to do. It takes the common case, wraps it up in an easy to use interface, and delivers the expected functionality without having to write it yourself.\n\nIt won't fit every database design - it can't. But it does solve the common case, and gives a good jumping off point for writing custom solutions where it can't.\n\nThe best part, is the same pattern works just as well for `has_many` as it does for `many_to_many`.\n\n### Is this a good idea? [You don't have take my word for it](https://www.youtube.com/watch?v=vAvQbEeTafk)\n\n\u003e put_assoc is also a good choice when you’re managing parent and child records separately, even when working with external data. You could for example use changesets to create/update/delete the child records on their own, then use put_assoc in a separate changeset to update the collection on the parent record. This is often a great way to work with many-to-many associations.\n\u003e\n\u003e — [Programming Ecto](https://pragprog.com/book/wmecto/programming-ecto), Chapter 4, Working with Associations.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fchrislaskey%2Fassoc","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fchrislaskey%2Fassoc","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fchrislaskey%2Fassoc/lists"}