ecto snippets

How to implement tagging with Ecto and Elixir

Tagged ecto, elixir, tagging, tags  Languages elixir
defmodule Snippets.Tagging do
  import Ecto.Query
  alias Ecto.Changeset
  alias Snippets.Repo
  @doc """
  Tags a model.

  ## Model

    defmodule Snippets.Tag do
      import Ecto.Query
      use Ecto.Schema

      schema "tags" do
        field :name
        many_to_many :snippets, Snippets.Snippet, join_through: "snippet_tags"
      end
    end

    defmodule Snippets.Snippet do
      import Ecto.Query
      use Ecto.Schema
      schema "snippets" do
        ...
        field :tag_list, :string, virtual: true
      end
    end

  ## Schema

    create table(:tags) do
      add :name, :text, null: false
    end
    create index(:tags, ["lower(name)"], unique: true)

    create table(:snippet_tags) do
      add :tag_id, references(:tags)
      add :snippet_id, references(:snippets)
    end
    create index(:snippet_tags, [:tag_id, :snippet_id], unique: true)

  ## Code

    snippet
    |> Changeset.cast(conn.params, @required_params, @optional_params)
    |> Tagging.changeset(Snippets.Tag, :tags, :tag_list)
    # Supports multiple types of tags
    |> Tagging.changeset(Snippets.Language, :languages, :language_list)

  """
  def changeset(changeset, model, association, tag_list_attr) do
    # Parse tags into Enum
    tag_list = changeset.changes
                |> Map.get(tag_list_attr, "")
                |> String.split(",")
                |> Enum.map(&(String.strip(&1)))
                |> Enum.reject(fn(name) -> name == "" end)
                |> Enum.map(&(String.downcase(&1)))
                |> Enum.uniq
                |> Enum.sort
    # Find existing tags
    existing_tags = from(t in model, where: t.name in ^tag_list) |> Repo.all
    # Create or find all tags
    tags = Enum.map(tag_list, fn(name) ->
      # Initialize new tag. Equivalent to:
      # new_tag = %Snippets.Tag{name: name}
      new_tag = struct(model, name: name)
      tag = Enum.find(existing_tags, new_tag, fn(existing_tag) ->
        existing_tag.name == name
      end)
    end)
    tag_changeset = Enum.map(tags, &Ecto.Changeset.change/1)
    # Add tags to changeset
    changeset |> Changeset.put_assoc(association, tag_changeset)
  end
end

Pagination with Elixir and Ecto

Tagged ecto, elixir, pagination  Languages elixir
defmodule Pagination do
  import Ecto.Query
  alias Snippets.Repo
  #
  # ## Example
  #
  #    Snippets.Snippet
  #    |> order_by(desc: :inserted_at)
  #    |> Pagination.page(page: 0, per_page: 10)
  #
  def page(query, page: page, per_page: per_page) do
    count = per_page + 1
    result = query
              |> limit(^count)
              |> offset(^(page*per_page))
              |> Repo.all
    %{ has_next?: (length(result) == count),
       has_prev?: page > 0,
       list: Enum.slice(result, 0, count-1) }
  end
end

Multi-tenancy in Ecto and Phoenix

Tagged ecto, elixir, multi-tenancy, postgres, phoenix  Languages bash, elixir

Creating a new tentant

A new tenant requires a namespace, which is a schema in Postgres, and a prefix in Ecto:

$ psql -U postgres database_x
> create schema aktagon; 

Querying data

import Ecto.Query
email = "christian@aktagon.com"
q = from(m in User, where: m.email == ^email)
Repo.all(%{q | prefix: "aktagon"})

Documentation: https://hexdocs.pm/ecto/Ecto.Query.html#module-query-prefix

Inserting data

Repo.insert(
  Ecto.put_meta(
   %User{ email: "christian@aktagon.com" },
   prefix: "aktagon"
  )
)

Migrations

$ mix ecto.migrate --prefix "aktagon"

Notes

  • (KeyError) key :__meta__ not found

I got this error when passing a changeset to Ecto.put_meta instead of a User struct.

Ecto query using left join, group by, order by, and count

Tagged count, ecto, group_by, left_join, order_by  Languages elixir

This is an example of an Ecto query that uses a left join, group by, order by, and count to produce a count of associated records for a list:

query = from list in List,
  left_join: subscriber in assoc(list, :subscribers),
  group_by: list.id,
  order_by: [asc: :name],
  select: %{ list | subscriber_count: count(subscriber.id) }
query |> Repo.all

Remember to add a virtual attribute named subscriber_count:

schema "lists" do
  ...
  field :subscriber_count, :integer, virtual: true