ecto snippets

How to implement tagging with Ecto and Elixir

Tagged elixir, tagging, tags, ecto  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 Phoenix and Ecto

Tagged phoenix, ecto, elixir, pagination  Languages elixir

Tested with Ecto 2.2 and Phoenix 1.3.

This module allows you to perform pagination with Phoenix and Ecto:

defmodule Pagination do
  import Ecto.Query
  alias Snippets.Repo
  #
  # ## Example
  #
  #    Snippets.Snippet
  #    |> order_by(desc: :inserted_at)
  #    |> Pagination.page(0, per_page: 10)
  #
  def page(query, page, per_page: per_page) when is_nil(page) do
    page(query, 0, per_page: per_page)
  end

  def page(query, page, per_page: per_page) when is_binary(page) do
    page = String.to_integer(page)
    page(query, page, per_page: per_page)
  end

  def page(query, 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
    total_count = Repo.one(from t in subquery(query), select: count("*"))
    page = %{
      has_next: has_next,
      has_prev: has_prev,
      prev_page: page - 1,
      next_page: page + 1,
      page: page,
      first: page*per_page+1,
      last: Enum.min([page+1*per_page, total_count]),
      count: total_count,
      list: Enum.slice(result, 0, count-1)
    }
  end
end

Usage:

Snippets.Snippet
|> order_by(desc: :inserted_at)
|> Pagination.page(0, per_page: 10)

View helper

defmodule PaginationHelpers do
  import Phoenix.HTML
  import Phoenix.HTML.Form
  import Phoenix.HTML.Link
  import Phoenix.HTML.Tag

  def pagination_text(list) do
    ~e"""
    Displaying <%= list.first %>-<%= list.last %> of <%= list.count %>
    """
  end

  def pagination_links(conn, list, route) do
    content_tag :div, class: "pagination" do
      children = []
      if list.has_prev do
        children = children ++ link "Previous", to: route.(conn, :index, page: list.prev_page), class: "btn btn-secondary col-md-1"
      end
      if list.has_next do
        children = children ++ link "Next", to: route.(conn, :index, page: list.next_page), class: "btn btn-secondary col-md-1"
      end
      children
    end
  end
end

Usage:

<%= pagination_text(@snippets) %>
<%= pagination_links(@conn, @snippets, &snippet_path/3) %>

Multi-tenancy in Ecto and Phoenix

Tagged elixir, multi-tenancy, postgres, phoenix, ecto  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 ecto, group_by, left_join, order_by, count  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_merge: %{ 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

Tested with Ecto 2.2.