Ecto Documentation

repository·master·Indexed 27 days ago

https://github.com/elixir-ecto/ecto

A toolkit for data mapping and language-integrated querying for Elixir. Ecto allows developers to map data from various sources into Elixir structs and perform complex queries using a powerful DSL. It supports multiple database adapters including PostgreSQL, MySQL, MSSQL, SQLite3, ClickHouse, and ETS. Key features include Ecto.Query for data retrieval, Ecto.Repo for database communication, and Ecto.Changeset for data validation and mapping.

Tokens
16.5K
Snippets
58
Records
75
Agent score
91%

What's inside Ecto

  1. Set `org_id` as a default for Repository operations

    master

    To avoid manually passing org_id to every repository call, you can store the tenant ID in the process dictionary and use the default_options/1 callback to inject it automatically.

    1. Store the ID: Create put_org_id/1 and get_org_id/0 functions in your Repo module that use Process.put/2 and Process.get/1 with a private @tenant_key.
    2. Inject the ID: Implement default_options/1 to return [org_id: get_org_id()].
    3. Usage: Call MyApp.Repo.put_org_id(id) at the start of a process (e.g., in a web request handler or a test setup).
    @impl true
    def default_options(_operation) do
      [org_id: get_org_id()]
    end
  2. Perform schemaless queries in Ecto

    master

    You can write queries without using predefined schemas by using string names for tables. This is useful for selecting specific fields without duplication or generating reports that don't map directly to application schemas.

    When selecting fields using a list, Ecto automatically converts the result to a map or a struct. You can also use Ecto.Query.type/2 to specify expected types for interpolated arguments, providing type casting guarantees similar to schemas.

    import Ecto.Query
    
    # Select specific fields from a table named "posts"
    query = from "posts", select: [:title, :body]
    
    # Complex reporting query without schemas
    query = 
      from u in "users",
        join: a in "activities",
        on: a.user_id == u.id,
        where: a.start_at > type(^start_at, :naive_datetime),
        group_by: a.user_id,
        select: %{
          user_id: a.user_id,
          count: count(u.id)
        }
    
    MyApp.Repo.all(query)
  3. Implement test factories with Ecto

    master

    Instead of using third-party libraries, you can implement test factories by creating a module that defines functions to return structs with required database fields. You can use Elixir's System.unique_integer() to generate unique values for fields like emails or usernames to avoid constraint violations during testing.

    defmodule MyApp.Factory do
      alias MyApp.Repo
    
      # Factories
    
      def build(:post) do
        %MyApp.Post{title: "hello world"}
      end
    
      def build(:comment) do
        %MyApp.Comment{body: "good post"}
      end
    
      def build(:post_with_comments) do
        %MyApp.Post{
          title: "hello with comments",
          comments: [
            build(:comment, body: "first"),
            build(:comment, body: "second")
          ]
        }
      end
    
      def build(:user) do
        %MyApp.User{
          email: "hello#{System.unique_integer()}",
          username: "hello#{System.unique_integer()}"
        }
      end
    
      # Convenience API
    
      def build(factory_name, attributes) do
        factory_name |> build() |> struct!(attributes)
      end
    
      def insert!(factory_name, attributes \ []) do
        factory_name |> build(attributes) |> Repo.insert!()
      end
    end
  4. Enforce multi-tenant integrity with composite foreign keys

    master

    When using a shared table approach for multi-tenancy, standard foreign keys only ensure a child belongs to a parent. They do not ensure that the child and parent belong to the same tenant. To prevent data corruption where a child record has a different org_id than its parent, use composite foreign keys.

    Implementation Steps:

    1. Unique Index: Create a unique index on the parent table covering both the primary key and the org_id (e.g., [:id, :org_id]).
    2. Composite Reference: In the child table migration, use Ecto.Migration.references/2 with the with: option to link both the parent ID and the org_id.
    3. PostgreSQL Strictness: For PostgreSQL, use match: :full to ensure none of the columns in the foreign key are nil.

    This ensures that if a Comment belongs to a Post, the org_id on the Comment must match the org_id on the Post.

    # 1. Create unique index on parent
    create unique_index(:posts, [:id, :org_id])
    
    # 2. Define composite foreign key on child
    create table(:comments) do
      add :body, :string
      add :org_id, :integer, null: false
    
      add :post_id,
          references(:posts, with: [org_id: :org_id]),
          null: false
    
      timestamps()
    end
    
    # For PostgreSQL strict enforcement:
    # references(:posts, with: [org_id: :org_id], match: :full)
  5. Use put_assoc/4 instead of cast_assoc/3 for complex associations

    master

    When dealing with associations where you cannot provide a list of maps containing primary keys (for example, when parsing a comma-separated string into many-to-many tags), use Ecto.Changeset.put_assoc/4.

    Unlike cast_assoc/3, which expects external parameters and relies on primary keys to determine if an association should be inserted, updated, or deleted, put_assoc/4 allows you to pass Ecto structs or changesets directly. This gives you explicit control over the data being associated.

    To handle deletions of removed associations, ensure your schema defines an :on_replace option (e.g., on_replace: :delete) in the association definition.

    defmodule MyApp.Post do
      use Ecto.Schema
    
      schema "posts" do
        field :title
        field :body
    
        many_to_many :tags, MyApp.Tag,
          join_through: "posts_tags",
          on_replace: :delete
    
        timestamps()
      end
    
      def changeset(struct, params \ %{}) do
        struct
        |> Ecto.Changeset.cast(params, [:title, :body])
        |> Ecto.Changeset.put_assoc(:tags, parse_tags(params))
      end
    
      defp parse_tags(params) do
        (params["tags"] || "")
        |> String.split(",")
        |> Enum.map(&String.trim/1)
        |> Enum.reject(& &1 == "")
        |> Enum.map(&get_or_insert_tag/1)
      end
    
      defp get_or_insert_tag(name) do
        Repo.get_by(MyApp.Tag, name: name) ||
          Repo.insert!(%MyApp.Tag{name: name})
      end
    end
  6. Define an Ecto Repo and Schema

    master

    To use Ecto, define a module that uses Ecto.Repo to handle database communication and modules that use Ecto.Schema to map data to Elixir structs.

    # Define the Repository
    defmodule Sample.Repo do
      use Ecto.Repo,
        otp_app: :my_app,
        adapter: Ecto.Adapters.Postgres
    end
    
    # Define a Schema
    defmodule Sample.Weather do
      use Ecto.Schema
    
      schema "weather" do
        field :city, :string
        field :temp_lo, :integer
        field :temp_hi, :integer
        field :prcp, :float, default: 0.0
      end
    end
    # In your application code
    defmodule Sample.Repo do
      use Ecto.Repo,
        otp_app: :my_app,
        adapter: Ecto.Adapters.Postgres
    end
    
    defmodule Sample.Weather do
      use Ecto.Schema
    
      schema "weather" do
        field :city     # Defaults to type :string
        field :temp_lo, :integer
        field :temp_hi, :integer
        field :prcp,    :float, default: 0.0
      end
    end
  7. Implement polymorphic associations using join tables

    master

    Ecto does not support the parent_id/parent_type pattern used in some other frameworks because it breaks database referential integrity. Instead, the recommended approach for performant and safe polymorphic associations is to use separate join tables for each association pair.

    For example, to associate TodoItem with both TodoList and Project, create two join tables: todo_list_items and project_items. This preserves database foreign key constraints and allows for efficient indexed queries.

    create table(:todo_lists)  do
      add :title
      timestamps()
    end
    
    create table(:projects)  do
      add :name
      timestamps()
    end
    
    create table(:todo_items)  do
      add :description
      timestamps()
    end
    
    create table(:todo_list_items) do
      add :todo_item_id, references(:todo_items)
      add :todo_list_id, references(:todo_lists)
      timestamps()
    end
    
    create table(:project_items) do
      add :todo_item_id, references(:todo_items)
      add :project_id, references(:projects)
      timestamps()
    end
  8. Manage database via migrations

    master

    Ecto uses migrations to construct and modify your database schema.

    • Create a database: mix ecto.create.
    • Generate a migration file: mix ecto.gen.migration <name> (creates a file in priv/repo/migrations).
    • Run migrations: mix ecto.migrate.
    • Undo migrations: mix ecto.rollback.

    Example migration to create a table:

    defmodule Friends.Repo.Migrations.CreatePeople do
      use Ecto.Migration
    
      def change do
        create table(:people) do
          add :first_name, :string
          add :last_name, :string
          add :age, :integer
        end
      end
    end
    mix ecto.create
    mix ecto.gen.migration create_people
    mix ecto.migrate
    mix ecto.rollback
  9. Map Postgres intervals to Elixir Duration types

    master

    As of Ecto 3.12.0, you can map Postgres interval columns to Elixir's Duration struct (available in Elixir 1.17+) instead of the default Postgrex.Interval struct.

    To enable this mapping, follow these four steps:

    1. Migration: Create a table or column using the :interval type.
    2. Schema: Define the field in your Ecto schema using the :duration type.
    3. Postgrex Type Definition: Use Postgrex.Types.define/2 to create a custom type module that specifies interval_decode_type: Duration.
    4. Configuration: Update your Repo configuration to use the newly defined Postgrex type module.
    # 1. Migration
    create table("movies") do
      add :running_time, :interval
    end
    
    # 2. Schema
    defmodule Movie do
      use Ecto.Schema
    
      schema "movies" do
        field :running_time, :duration
      end
    end
    
    # 3. Custom Postgrex type module
    # Inside lib/my_app/postgrex_types.ex
    Postgrex.Types.define(MyApp.PostgrexTypes, [], interval_decode_type: Duration)
    
    # 4. Configuration
    config :my_app, MyApp.Repo, types: MyApp.PostgresTypes
  10. Handle database insertion results

    master

    When calling Repo.insert/1 with a changeset, Ecto returns a tuple indicating success or failure. It is best practice to use a case statement to handle both outcomes.

    Note: changeset.valid? only checks application-level validations. It does not check database constraints (like uniqueness_constraint). To catch database-level errors, you must attempt the insertion and handle the returned {:error, changeset} tuple.

    case Friends.Repo.insert(changeset) do
      {:ok, person} ->
        # do something with person
      {:error, changeset} ->
        # do something with changeset
    end
  11. Use data structures for simple queries

    master

    Ecto allows you to pass data structures (like keyword lists) directly into query functions like where/3 and order_by/3. This avoids the need for explicit bindings (e.g., [p]) when performing simple equality checks or basic ordering. This is useful for building queries where the filter criteria are known at runtime as a map or keyword list.

    # Using keyword lists for equality and ordering
    where = [author: "José", category: "Elixir"]
    order_by = [desc: :published_at]
    
    Post
    |> where(^where)
    |> order_by(^order_by)