Run this notebook

Use Livebook to open this notebook and explore new ideas.

It is easy to get started, on your machine or the cloud.

Click below to open and run it in your Livebook at .

(or change your Livebook location)

# Comparing usage of Ash & Ecto ```elixir # https://gist.github.com/Gazler/b4e92e9ab7527c7e326f19856f8a974a Application.put_env(:phoenix, :json_library, Jason) Application.put_env(:sample, SamplePhoenix.Endpoint, http: [ip: {127, 0, 0, 1}, port: 5001], server: true, secret_key_base: String.duplicate("a", 64) ) Application.put_env(:ash, :validate_domain_config_inclusion?, false) Mix.install( [ {:plug_cowboy, "~> 2.5"}, {:jason, "~> 1.0"}, {:phoenix, "~> 1.7.0"}, {:ecto_sql, "~> 3.10"}, {:postgrex, ">= 0.0.0"}, {:ash, "~> 3.0.0-rc"}, {:ash_postgres, "~> 2.0.0-rc"} ] ) Application.put_env(:sample, Repo, database: "mix_install_examples") defmodule Repo do use AshPostgres.Repo, otp_app: :sample def installed_extensions, do: ["ash-functions"] end defmodule Migration0 do use Ecto.Migration def change do create table("posts") do add(:title, :string) timestamps(type: :utc_datetime_usec) end create table("comments") do add(:content, :string) add(:post_id, references(:posts, on_delete: :delete_all), null: false) end end end # This file is autogenerated by ash_postgres, not something you need to define yourself defmodule Migration1 do use Ecto.Migration def up do execute(""" CREATE OR REPLACE FUNCTION ash_elixir_or(left BOOLEAN, in right ANYCOMPATIBLE, out f1 ANYCOMPATIBLE) AS $$ SELECT COALESCE(NULLIF($1, FALSE), $2) $$ LANGUAGE SQL IMMUTABLE; """) execute(""" CREATE OR REPLACE FUNCTION ash_elixir_or(left ANYCOMPATIBLE, in right ANYCOMPATIBLE, out f1 ANYCOMPATIBLE) AS $$ SELECT COALESCE($1, $2) $$ LANGUAGE SQL IMMUTABLE; """) execute(""" CREATE OR REPLACE FUNCTION ash_elixir_and(left BOOLEAN, in right ANYCOMPATIBLE, out f1 ANYCOMPATIBLE) AS $$ SELECT CASE WHEN $1 IS TRUE THEN $2 ELSE $1 END $$ LANGUAGE SQL IMMUTABLE; """) execute(""" CREATE OR REPLACE FUNCTION ash_elixir_and(left ANYCOMPATIBLE, in right ANYCOMPATIBLE, out f1 ANYCOMPATIBLE) AS $$ SELECT CASE WHEN $1 IS NOT NULL THEN $2 ELSE $1 END $$ LANGUAGE SQL IMMUTABLE; """) execute(""" CREATE OR REPLACE FUNCTION ash_trim_whitespace(arr text[]) RETURNS text[] AS $$ DECLARE start_index INT = 1; end_index INT = array_length(arr, 1); BEGIN WHILE start_index <= end_index AND arr[start_index] = '' LOOP start_index := start_index + 1; END LOOP; WHILE end_index >= start_index AND arr[end_index] = '' LOOP end_index := end_index - 1; END LOOP; IF start_index > end_index THEN RETURN ARRAY[]::text[]; ELSE RETURN arr[start_index : end_index]; END IF; END; $$ LANGUAGE plpgsql IMMUTABLE; """) execute(""" CREATE OR REPLACE FUNCTION ash_raise_error(json_data jsonb) RETURNS BOOLEAN AS $$ BEGIN -- Raise an error with the provided JSON data. -- The JSON object is converted to text for inclusion in the error message. RAISE EXCEPTION 'ash_error: %', json_data::text; RETURN NULL; END; $$ LANGUAGE plpgsql; """) execute(""" CREATE OR REPLACE FUNCTION ash_raise_error(json_data jsonb, type_signal ANYCOMPATIBLE) RETURNS ANYCOMPATIBLE AS $$ BEGIN -- Raise an error with the provided JSON data. -- The JSON object is converted to text for inclusion in the error message. RAISE EXCEPTION 'ash_error: %', json_data::text; RETURN NULL; END; $$ LANGUAGE plpgsql; """) end def down do execute( "DROP FUNCTION IF EXISTS ash_raise_error(jsonb), ash_raise_error(jsonb, ANYCOMPATIBLE), ash_elixir_and(BOOLEAN, ANYCOMPATIBLE), ash_elixir_and(ANYCOMPATIBLE, ANYCOMPATIBLE), ash_elixir_or(ANYCOMPATIBLE, ANYCOMPATIBLE), ash_elixir_or(BOOLEAN, ANYCOMPATIBLE), ash_trim_whitespace(text[])" ) end end try do Repo.stop() catch :exit, _ -> :ok end Repo.start_link() Repo.__adapter__().storage_down(Repo.config()) Repo.__adapter__().storage_up(Repo.config()) Ecto.Migrator.run(Repo, [{0, Migration0}, {1, Migration1}], :up, all: true, log_migrations_sql: :debug ) ``` ## Foreword These are simple examples. The goal here is not to say in some way that Ecto is limited. It is *not* limited. It is, however, a *library for interacting with databases*. Its primitives are around data. Ash's primitives are around application-level concepts, which *may or may not* map to a data layer. This document is intentionally not titled "Ash vs Ecto". Ash (for some data layers) actually *sits on top of* ecto. Comparing them direclty is an *apples and oranges comparison*. They are very different things, even though what you're about to see are a bunch of examples of them doing the same thing. Ash extends far beyond the examples shown here. I want to reiterate that Ash is not a "data wrapper". While what you see here may seem very "CRUD-y", you are strongly encouraged to define your actions as "domain-relevant events", i.e you `:publish` a `Post`, or `:revoke` a `License`. I went off of my memory of what building things with Phoenix Contexts and Ecto are like, if I got something wrong, it is ignorance, not malice 😅 ## Boilerplate Reducing the amount of code that you write is not a goal of Ash Framework. It is, however, a natural effect of our design patterns. The goals of Ash framework include: - Building maintainable, stable applications. We care about day 1, but we also care about year 5 - Increasing flexibility and code reuse. - Adding *new capabilities* that, without a smart framework like Ash are *practical* impossibilities otherwise. Atomics & bulk actions are an example of this. note *practical* impossibilities. You *could* design all your operations to be fully concurrency safe and run in batches. But you would almost certainly not expend that effort. With Ash, that is very easy to accomplish. - Derive layers from your application definition. We derive APIs and new behavior directly from your resources. This maximizes flexibiltiy, saves time, and enables the tooling to do really smart things for you. No need to write absinthe resolvers or data loaders. No need to write an OpenAPI schema. Ash does it all for you. ## Ecto Schemas & a context ```elixir defmodule Ecto.Post do use Ecto.Schema import Ecto.Changeset schema "posts" do field(:title, :string) timestamps(type: :utc_datetime_usec) has_many(:comments, Ecto.Comment) end def changeset(product, attrs) do product |> cast(attrs, [:title]) |> validate_required([:title]) end end defmodule Ecto.Comment do use Ecto.Schema import Ecto.Changeset schema "comments" do field(:content, :string) belongs_to(:post, Ecto.Post) end def changeset(comment, attrs) do comment |> cast(attrs, [:content, :post_id]) |> validate_required([:content, :post_id]) end end defmodule Ecto.Blog do require Ecto.Query def list_posts do Repo.all(Ecto.Post, preload: :comments) end def get_post(id) do Ecto.Post |> Repo.get!(id) |> Repo.preload(:comments) end def create_post(title) do %Ecto.Post{} |> Ecto.Post.changeset(%{title: title}) |> Repo.insert!() end def update_post(%Ecto.Post{} = post, changes) do post |> Ecto.Post.changeset(changes) |> Repo.update!() |> Repo.preload(:comments) end def delete_post(%Ecto.Post{} = post) do Repo.delete!(post) end def add_comment(post_id, content) do %Ecto.Comment{} |> Ecto.Comment.changeset(%{content: content, post_id: post_id}) |> Repo.insert!() end def remove_comment(%Ecto.Comment{} = comment) do Repo.delete!(comment) end end ``` ## Ash Resources ```elixir defmodule Ash.Post do use Ash.Resource, domain: Ash.Blog, data_layer: AshPostgres.DataLayer postgres do table "posts" repo Repo end actions do defaults [:read, :destroy, create: [:title], update: [:title]] end attributes do integer_primary_key :id attribute :title, :string, allow_nil?: false create_timestamp :inserted_at update_timestamp :updated_at end relationships do has_many :comments, Ash.Comment end end defmodule Ash.Comment do use Ash.Resource, domain: Ash.Blog, data_layer: AshPostgres.DataLayer actions do defaults [:read, :destroy, create: [:content, :post_id], update: [:content]] end postgres do table "comments" repo Repo end attributes do integer_primary_key :id attribute :content, :string, allow_nil?: false end relationships do belongs_to :post, Ash.Post do attribute_type :integer allow_nil? false end end end defmodule Ash.Blog do use Ash.Domain resources do resource Ash.Post do define :list_posts, action: :read define :create_post, action: :create define :get_post, action: :read, get_by: [:id] define :update_post, action: :update define :delete_post, action: :destroy end resource Ash.Comment do define :add_comment, action: :create, args: [:post_id, :content] define :remove_comment, action: :destroy end end end ``` ## Now lets use them ## Creating Posts ```elixir # Creating posts Ecto.Blog.create_post("ecto_title") Ash.Blog.create_post!(%{title: "ash_title"}) # Bulk creating posts # Ecto: you can do it iteratively, i.e # for input <- [....] # but you need to write new code to do a "bulk create" in ecto using insert_all Ash.Blog.create_post!([%{title: "ash_title1"}, %{title: "ash_title2"}]) ``` ## Listing Posts ```elixir # Listing Posts Ecto.Blog.list_posts() Ash.Blog.list_posts!() Ecto.Blog.get_post(1) Ash.Blog.get_post!(1) # Modifying the query # Ecto: need to add arguments to your context functions for ecto require Ash.Query Ash.Blog.list_posts!(query: Ash.Query.filter(Ash.Post, contains(title, "ash"))) Ash.Blog.list_posts!(query: Ash.Query.sort(Ash.Post, title: :asc)) Ash.Blog.list_posts!(query: Ash.Query.limit(Ash.Post, 1)) # loading data # Ecto: need to put it in your context function, or accept an arg # note: you can put this in the action if you like, just showing that you can do it here Ash.Blog.list_posts!(load: :comments) # Pagination # Ecto: need to bring in a pagination library # Ash: can use offset or keyset(A.K.A cursor) pagination %{results: [first | _]} = Ash.Blog.list_posts!(page: [limit: 5, offset: 1]) Ash.Blog.list_posts(page: [after: first.__metadata__.keyset, limit: 1]) ``` ## Updating Posts ```elixir ecto_post = Ecto.Blog.get_post(1) Ecto.Blog.update_post(ecto_post, %{title: "new_title"}) ash_post = Ash.Blog.get_post!(1) Ash.Blog.update_post!(ash_post, %{title: "new_title2"}) # update many posts # Ecto: need to write something new, maybe multiple somethings new if you want to use a # list input or a query input require Ash.Query # update a query Ash.Post |> Ash.Query.filter(contains(title, "ecto")) |> Ash.Blog.update_post!(%{title: "gotcha"}) # update records in batches [Ash.Blog.get_post!(1), Ash.Blog.get_post!(2)] |> Ash.Blog.update_post!(%{title: "gotcha again"}) # stream-capable [%{title: "title1"}, %{title: "title2"}] |> Ash.Blog.create_post!(bulk_options: [return_stream?: true, return_records?: true]) |> Ash.Blog.update_post!(%{title: "updated"}, bulk_options: [return_stream?: true, return_records?: true] ) |> Enum.to_list() ``` ## And a whole lot more ### Multitenancy built in Attribute multi tenancy works across any data layer. data layers can provide multitenancy features. For example, `ash_postgres` can manage schemas for each tenants. ### Authorization built in * Add policies using `Ash.Policy.Authorizer`. ### Authentication * Authentication with `AshAuthentication` and `AshAuthenticationPhoenix` ### Add APIs in literally minutes * Full featured JSON:API with `AshJsonApi` (filter, sort, pagination, data inclusion, OpenAPI etc.) * Full featured GraphQL with `AshGraphql` (filter, sort, pagination, relay, etc.) ### Easily Extensible * Add declarative changes/validations, like plugs for your changesets. Use them to support to batch and atomic (i.e `update_all`) * Override action behavior with the `manual` option. * Or use "generic actions", which * benefit from being typed * can be placed in your APIs using the api extensions (AshJsonApi, not yet but soon) * honor policy authorization ### Advanced tools you won't find anywhere else #### Calculations Define expression calculations which can be run in Elixir or in the data layer <!-- livebook:{"force_markdown":true} --> ```elixir calculate :full_name, :string, expr(first_name <> " " <> last_name) ``` Use fragments, custom expressions and more to extend this syntax. #### Atomics Encapsulate logic for your changesets that covers "regular", atomic and batch cases <!-- livebook:{"force_markdown":true} --> ```elixir defmodule Increment do use Ash.Resource.Change @impl true def change(changeset, opts, _) do field = opts[:field] amount = opts[:amount] || 1 value = Map.get(changeset.data, field) || 0 Ash.Changeset.change_attribute(changeset, field, value + amount) end @impl true def atomic(_, opts, _) do field = opts[:field] amount = opts[:amount] || 1 {:atomic, %{field => expr(^ref(field) + ^amount)}} end # @impl true # def batch_change(changesets, _, _) do # we don't need this, so we don't define it and the single change is used # end end ``` <!-- livebook:{"force_markdown":true} --> ```elixir update :game_won do accept [] change {Increment, field: :total_games_won} change {Increment, field: :score, amount: 100} end ``` #### Auto filtering policies <!-- livebook:{"force_markdown":true} --> ```elixir policies do policy action_type(:read) do authorize_if expr(owner_id == ^actor(:id)) forbid_if expr(content_hidden == true) authorize_if expr(public == true) end end # Generates a query like the following %Ash.Query{filter: #Ash.Filter<owner_id == ^id or (not(content_hidden == true) and public == true)>} ``` ### Packages Ash has a rich package ecosystem that continues to grow. These packages are more than just utilities or libraries. They are powerful extensions that are resource-aware. Here are just the core packages #### Data Layers * [AshPostgres](https://hexdocs.pm/ash_postgres) | PostgreSQL data layer * [AshSqlite](https://hexdocs.pm/ash_sqlite) | SQLite data layer * [AshCsv](https://hexdocs.pm/ash_csv) | CSV data layer * [AshCubdb](https://hexdocs.pm/ash_cubdb) | CubDB data layer #### API Extensions * [AshJsonApi](https://hexdocs.pm/ash_json_api) | JSON:API builder * [AshGraphql](https://hexdocs.pm/ash_graphql) | GraphQL builder #### Web * [AshPhoenix](https://hexdocs.pm/ash_phoenix) | Phoenix integrations * [AshAuthentication](https://hexdocs.pm/ash_authentication) | Authenticate users with password, OAuth, and more * [AshAuthenticationPhoenix](https://hexdocs.pm/ash_authentication_phoenix) | Integrations for AshAuthentication and Phoenix #### Finance * [AshMoney](https://hexdocs.pm/ash_money) | A money data type for Ash * [AshDoubleEntry](https://hexdocs.pm/ash_double_entry) | A double entry system backed by Ash Resources #### Resource Utilities * [AshOban](https://hexdocs.pm/ash_oban) | Background jobs and scheduled jobs for Ash, backed by Oban * [AshArchival](https://hexdocs.pm/ash_archival) | Archive resources instead of deleting them * [AshStateMachine](https://hexdocs.pm/ash_state_machine) | Create state machines for resources * [AshPaperTrail](https://hexdocs.pm/ash_paper_trail) | Keep a history of changes to resources * [AshCloak](https://hexdocs.pm/ash_cloak) | Encrypt attributes of a resource #### Admin & Monitoring * [AshAdmin](https://hexdocs.pm/ash_admin) | A push-button admin interface * [AshAppsignal](https://hexdocs.pm/ash_appsignal) | Monitor your Ash resources with AppSignal #### Testing * [Smokestack](https://hexdocs.pm/smokestack) | Declarative test factories for Ash resources ### So much more I could go on, but a lot of things make more sense when you see how they all play together vs explaining them without the necessary context.
See source

Have you already installed Livebook?

If you already installed Livebook, you can configure the default Livebook location where you want to open notebooks.
Livebook up Checking status We can't reach this Livebook (but we saved your preference anyway)
Run notebook

Not yet? Install Livebook in just a minute

Livebook is open source, free, and ready to run anywhere.

Run in the cloud

on select platforms

To run on Linux, Docker, embedded devices, or Elixir’s Mix, check our README.

PLATINUM SPONSORS
SPONSORS
Code navigation with go to definition of modules and functions Read More