|
| 1 | +<!-- |
| 2 | +SPDX-FileCopyrightText: 2020 Zach Daniel |
| 3 | +
|
| 4 | +SPDX-License-Identifier: MIT |
| 5 | +--> |
| 6 | + |
| 7 | +# Working With Existing Databases |
| 8 | + |
| 9 | +When you're building an Ash application against a database you don't own or control — such as a shared company database, a legacy system, or a third-party service's database — you need a workflow that lets you iterate on your Ash resources without generating migrations. The `--fragments` and `--no-migrations` options to `mix ash_postgres.gen.resources` are designed for exactly this. |
| 10 | + |
| 11 | +## The Problem |
| 12 | + |
| 13 | +Normally, Ash resources are the source of truth for your database schema, and migrations are generated from them. But when the database is managed externally: |
| 14 | + |
| 15 | +- You don't want Ash generating migrations for a schema you don't control |
| 16 | +- The upstream schema may change, and you need to regenerate your resources to match |
| 17 | +- You still want to customize your resources with actions, calculations, validations, and other Ash features — without losing those customizations on regeneration |
| 18 | + |
| 19 | +## The Workflow |
| 20 | + |
| 21 | +### 1. Generate resources with `--fragments` and `--no-migrations` |
| 22 | + |
| 23 | +```bash |
| 24 | +mix ash_postgres.gen.resources MyApp.ExternalDb \ |
| 25 | + --tables users,orders,products \ |
| 26 | + --no-migrations \ |
| 27 | + --fragments |
| 28 | +``` |
| 29 | + |
| 30 | +This creates two files per table: |
| 31 | + |
| 32 | +- **The resource file** (e.g., `lib/my_app/external_db/user.ex`) — contains `use Ash.Resource`, the `postgres` block, and any actions. This is *your* file to customize. |
| 33 | +- **The fragment file** (e.g., `lib/my_app/external_db/user/model.ex`) — contains the attributes, relationships, and identities introspected from the database. This file is regenerated by the tool. |
| 34 | + |
| 35 | +The resource file will include `migrate? false` in its `postgres` block (from `--no-migrations`), telling Ash not to generate migrations for it: |
| 36 | + |
| 37 | +```elixir |
| 38 | +defmodule MyApp.ExternalDb.User do |
| 39 | + use Ash.Resource, |
| 40 | + domain: MyApp.ExternalDb, |
| 41 | + data_layer: AshPostgres.DataLayer, |
| 42 | + fragments: [MyApp.ExternalDb.User.Model] |
| 43 | + |
| 44 | + postgres do |
| 45 | + table "users" |
| 46 | + repo MyApp.Repo |
| 47 | + migrate? false |
| 48 | + end |
| 49 | +end |
| 50 | +``` |
| 51 | + |
| 52 | +The fragment file contains the schema details: |
| 53 | + |
| 54 | +```elixir |
| 55 | +defmodule MyApp.ExternalDb.User.Model do |
| 56 | + use Spark.Dsl.Fragment, |
| 57 | + of: Ash.Resource |
| 58 | + |
| 59 | + attributes do |
| 60 | + uuid_primary_key :id |
| 61 | + attribute :email, :string, public?: true |
| 62 | + attribute :name, :string, public?: true |
| 63 | + # ... |
| 64 | + end |
| 65 | + |
| 66 | + relationships do |
| 67 | + has_many :orders, MyApp.ExternalDb.Order |
| 68 | + # ... |
| 69 | + end |
| 70 | + |
| 71 | + identities do |
| 72 | + identity :unique_email, [:email] |
| 73 | + end |
| 74 | +end |
| 75 | +``` |
| 76 | + |
| 77 | +### 2. Customize your resources |
| 78 | + |
| 79 | +Add actions, calculations, validations, changes, and anything else to the **resource file**. This is your space: |
| 80 | + |
| 81 | +```elixir |
| 82 | +defmodule MyApp.ExternalDb.User do |
| 83 | + use Ash.Resource, |
| 84 | + domain: MyApp.ExternalDb, |
| 85 | + data_layer: AshPostgres.DataLayer, |
| 86 | + fragments: [MyApp.ExternalDb.User.Model] |
| 87 | + |
| 88 | + actions do |
| 89 | + defaults [:read] |
| 90 | + |
| 91 | + read :by_email do |
| 92 | + argument :email, :string, allow_nil?: false |
| 93 | + filter expr(email == ^arg(:email)) |
| 94 | + end |
| 95 | + end |
| 96 | + |
| 97 | + calculations do |
| 98 | + calculate :display_name, :string, expr(name || email) |
| 99 | + end |
| 100 | + |
| 101 | + postgres do |
| 102 | + table "users" |
| 103 | + repo MyApp.Repo |
| 104 | + migrate? false |
| 105 | + end |
| 106 | +end |
| 107 | +``` |
| 108 | + |
| 109 | +### 3. Regenerate fragments when the schema changes |
| 110 | + |
| 111 | +When the upstream database schema changes (new columns, new tables, changed relationships), re-run the same command: |
| 112 | + |
| 113 | +```bash |
| 114 | +mix ash_postgres.gen.resources MyApp.ExternalDb \ |
| 115 | + --tables users,orders,products \ |
| 116 | + --no-migrations \ |
| 117 | + --fragments |
| 118 | +``` |
| 119 | + |
| 120 | +Because the resource files already exist, **only the fragment files are regenerated**. Your customizations in the resource files are untouched. |
| 121 | + |
| 122 | +### 4. Review the diff |
| 123 | + |
| 124 | +After regeneration, review the changes with `git diff` to see what changed in the schema. New columns will appear as new attributes, altered relationships will be updated, and so on. |
| 125 | + |
| 126 | +## Key Points |
| 127 | + |
| 128 | +- **`--fragments`** splits generated schema details into a separate `Model` fragment module, keeping your resource file safe from regeneration |
| 129 | +- **`--no-migrations`** prevents migration generation and adds `migrate? false` to the `postgres` block |
| 130 | +- **Fragment files are disposable** — they are regenerated from the database each time. Don't put custom code in them. |
| 131 | +- **Resource files are yours** — once created on the first run, they won't be overwritten by subsequent runs |
| 132 | +- You can also use `--skip-tables` to exclude tables, `--tables` to scope to specific schemas (e.g., `accounts.`), and `--extend` to apply extensions to generated resources |
0 commit comments