Repo.aggregateを楽しむ

https://qiita.com/official-campaigns/engineer-festa/2022

# はじめに

[Elixir](https://elixir-lang.org/)を楽しんでいますか:bangbang::bangbang::bangbang:
楽しんでいますよね:bangbang::bangbang::bangbang:

この記事はRepo.aggregateを取り上げます。
ひとことで言うと、**集計する**です。

# 題材

[Ecto](https://hexdocs.pm/ecto/Ecto.html)は、[Elixir](https://elixir-lang.org/)製のデータベースライブラリです。
[Ecto](https://hexdocs.pm/ecto/Ecto.html)の[Getting Started](https://hexdocs.pm/ecto/getting-started.html)で取り上げられている`people`テーブルを使います。


```elixir
people = [
  %Person{first_name: "Ryan", last_name: "Bigg", age: 28},
  %Person{first_name: "John", last_name: "Smith", age: 27},
  %Person{first_name: "Jane", last_name: "Smith", age: 26},
]

Enum.each(people, fn (person) -> Repo.insert(person) end)
```

各レコードのカラムは、`first_name`と`last_name`に加えて、`age`の3つのカラムがあります。
3人登録されています。


作り方は後述します。

# Repo.aggregate

`iex -S mix`でいろいろ楽しんでいきます。
あらかじめ以下を実行しておくとタイプ量が少なく済みます。

```bash
iex -S mix
```

iexが立ち上がります。

```elixir
iex> alias Hello.Repo
iex> alias Hello.Friends.Person
iex> import Ecto.Query
```

## 友達何人いるのかな？

### [Elixir](https://elixir-lang.org/)で計算

```elixir
Repo.all(Person) |> Enum.count()
```

### SQLで計算 -- Repo.aggregateを使う

```elixir
Repo.aggregate(Person, :count)
```

こんなSQLが実行されています。

```SQL
SELECT count(*) FROM "people" AS p0 []
```

## 友達の年齢を平均する

### [Elixir](https://elixir-lang.org/)で計算

```elixir
Repo.all(Person) |> Enum.map(& &1.age) |> Enum.sum |> Kernel./(Repo.all(Person) |> Enum.count())
```

### SQLで計算 -- Repo.aggregateを使う

```elixir
Repo.aggregate(Person, :avg, :age)
```

こんなSQLが実行されています。

```SQL
SELECT avg(p0."age") FROM "people" AS p0 []
```

## 最年少はだれ？

### [Elixir](https://elixir-lang.org/)で計算

```elixir
Repo.all(Person) |> Enum.min_by(& &1.age)
```

### SQLで計算 -- Repo.aggregateを使う

```elixir
(from p in Person, where: p.age == ^Repo.aggregate(Person, :min, :age)) |> Repo.one
```

一人しかいないことがわかっているので、`Repo.one`にしています。
複数いる場合は、例外が発生します。
`Repo.all`を使うと複数人得ることができます。

`Repo.aggregate(Person, :min, :age)`は外部変数扱いになるのでクエリの中で`^`を付けています。

こんなSQLが実行されています。

```SQL
SELECT min(p0."age") FROM "people" AS p0 []
SELECT p0."id", p0."age", p0."first_name", p0."last_name", p0."inserted_at", p0."updated_at" FROM "people" AS p0 WHERE (p0."age" = $1) [26]
```

## 最年長はだれ？

### [Elixir](https://elixir-lang.org/)で計算

```elixir
Repo.all(Person) |> Enum.max_by(& &1.age)
```

### SQLで計算 -- Repo.aggregateを使う

```elixir
(from p in Person, where: p.age == ^Repo.aggregate(Person, :max, :age)) |> Repo.one
```

一人しかいないことがわかっているので、`Repo.one`にしています。
複数いる場合は、例外が発生します。
`Repo.all`を使うと複数人得ることができます。

`Repo.aggregate(Person, :max, :age)`は外部変数扱いになるのでクエリの中で`^`を付けています。

こんなSQLが実行されています。

```SQL
SELECT max(p0."age") FROM "people" AS p0 []
SELECT p0."id", p0."age", p0."first_name", p0."last_name", p0."inserted_at", p0."updated_at" FROM "people" AS p0 WHERE (p0."age" = $1) [28]
```

## ひとこと


今回はデータが3件しかありませんので、どちらの方法で処理してもだいぶ広義の意味でのWebアプリケーションとしての性能、メモリ使用量に大差はないとおもいます。
データが大量になるとSQLに計算してもらったほうがよい場合が多いとおもいます。



# 環境構築

[Phoenix](https://www.phoenixframework.org/)は、[Elixir](https://elixir-lang.org/)製のWebアプリケーションフレームワークです。
[Ecto](https://hexdocs.pm/ecto/Ecto.html)は、[Phoenix](https://www.phoenixframework.org/)と必ずしも組み合わせる必要はありません。
そうではあるのですがここでは、[Phoenix](https://www.phoenixframework.org/)アプリを前提にします。
というのも、各種初期設定が楽ちんだからです。

[Phoenix](https://www.phoenixframework.org/)に詳しい方はこの節はどうぞ読み飛ばしてください。

## PostgreSQL

Dockerで起動する例を示します。

```bash
docker run -d --rm -p 5432:5432 -e POSTGRES_USER=postgres -e POSTGRES_PASSWORD=postgres postgres:13
```

ローカルマシンにPostgreSQLをインストールして動作させる場合は、`postgres`ユーザを作っておいてください。
パスワードは`postgres`が吉です。
https://db.just4fun.biz/?PostgreSQL/PostgreSQL%E3%81%A7%E3%83%A6%E3%83%BC%E3%82%B6%E3%81%AE%E3%83%AD%E3%83%BC%E3%83%AB%E3%82%92%E5%A4%89%E6%9B%B4%E3%81%99%E3%82%8B
が参考になるとおもいます。

## [Installation](https://hexdocs.pm/phoenix/installation.html)

[Phoenix](https://www.phoenixframework.org/)公式の[Installation](https://hexdocs.pm/phoenix/installation.html)を参考にします。

### [Elixir](https://elixir-lang.org/)

私は、`1.13.4-otp-24`を使っています。
この記事では説明しません。
[Elixir](https://elixir-lang.org/)をインストールしてください。
macOSをお使いの方はまずは`brew install elixir`で始めるのが早いとおもいます。

### [Phoenix](https://www.phoenixframework.org/)のインストール

ターミナルでインストールを進めましょう。

```bash
mix local.hex
mix archive.install hex phx_new
```

[Yn]を聞かれたら、迷わず`Y`です。
楽しみましょう！！！

環境構築は以上です。

# 説明に使ったコードスニペットが実行できるプロジェクトの作成

ここからはこの記事の前半で説明につかったコードスニペットが実行できるプロジェクトを作ります。

## [Phoenix](https://www.phoenixframework.org/)アプリの作成

```
mix phx.new hello
```

`Fetch and install dependencies? [Yn]`には一旦`n`で抜けます。
もちろん元気よく`Y`でもよいです。

## setup

`hello`ディレクトリへ移動して以下のコマンドを実行します。

```bash
cd hello
mix setup
```

`mix setup`て何だ？　という方は、`mix.exs`をみてください。
以下を行ってくれます。

- `mix deps.get`: ライブラリのインストール
- `mix ecto.create`: データベースの作成
- `mix ecto.migrate`: マイグレート
- `mix run priv/repo/seeds.exs`: シードデータの投入

もうなんなら、`mix setup`だけ覚えておけばなんとかなります。

## mix phx.gen.schema

いきなり長めのコマンドで恐縮です。

```bash
mix phx.gen.schema Friends.Person people first_name:string last_name:string age:integer
```

このコマンドを実行すると以下のファイルができあがります！
`lib/hello/friends/person.ex`はスキーマモジュールと呼ばれるコードです。

```elixir:lib/hello/friends/person.ex
defmodule Hello.Friends.Person do
  use Ecto.Schema
  import Ecto.Changeset

  schema "people" do
    field :age, :integer
    field :first_name, :string
    field :last_name, :string

    timestamps()
  end

  @doc false
  def changeset(person, attrs) do
    person
    |> cast(attrs, [:first_name, :last_name, :age])
    |> validate_required([:first_name, :last_name, :age])
  end
end
```

`20220710132740_create_people.exs`はマイグレーションファイルと呼ばれるものです。
`20220710132740_create_people.exs`ファイル名の先頭は、作成日時によって変わります。

```elixir:priv/repo/migrations/20220710132740_create_people.exs
defmodule Hello.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

      timestamps()
    end
  end
end
```

## シードデータの作成

`priv/repo/seeds.exs`を編集します。

```elixir:priv/repo/seeds.exs
alias Hello.Repo
alias Hello.Friends.Person

people = [
  Person.changeset(%Person{}, %{first_name: "Ryan", last_name: "Bigg", age: 28}),
  Person.changeset(%Person{}, %{first_name: "John", last_name: "Smith", age: 27}),
  Person.changeset(%Person{}, %{first_name: "Jane", last_name: "Smith", age: 26}),
]

Enum.each(people, fn (person) -> Repo.insert(person) end)
```

## migrateとシードデータの投入

```bash
mix setup
```

さきほどご紹介したように、`mix setup`ひとつでmigrate、シードデータの投入を行ってくれます。

## 【おまけ】データベースをリセットしたい

データベースをリセットしたい、そんなときは以下のコマンドを実行してください。
これも`mix.exs`に定義されています。

```bash
mix ecto.reset
```

# おわりに

この記事は、Repo.aggregateを楽しみました。
[Ecto](https://hexdocs.pm/ecto/Ecto.html)をさらに楽しむにはSQLと仲良くなるともっと楽しめるとおもいます。

Enjoy [Elixir](https://elixir-lang.org/):bangbang::bangbang::bangbang: