Elixir Ectoのスキーマ定義と合成可能なクエリ - モジュールからデータベース用DSLへ

[前回](http://qiita.com/yoavlt/items/2faa2107eedd8c82ff8f)の続き。
今回はEcto.Queryableとクエリ合成、Migrationの詳細。

適切な道具があれば、人間の生産性と思考力は大きく飛躍する。Ectoはデータベースを操作することに特化したプログラミング言語のように操作することができる。
他のプログラミング言語では真似できないほどの柔軟性を持ったElixirだからこそできることだ。
データベースを操作するモジュールのEctoでは、データベースを操作するために作られたプログラミング言語のようなエレガントなAPIを通してアプリケーションを構築することができる。

## マイグレーション
`class`でも`struct`でもなく、スキーマ定義は`schema`マクロで構築できたように、マイグレーション定義においても、SQLシンタックスのように、`create`や`drop`を通して操作できる。

### create

```sh:
mix ecto.gen.migration create_users
```

とすると、モジュールと空の`change/0`関数のあるファイルが生成されるので以下のようにして、テーブル定義をする。

```elixir:
defmodule MyRepo.Migrations.CreateUsers do
  use Ecto.Migration

  def change do
    create table(:users) do
      add :username, :string
      add :email, :string
      add :age, :integer
      add :group_id, references(:groups)

      timestamps
    end
    create index(:users, [:group_id])
    create index(:users, [:username], unique: true)
  end
end
```

`timestamps`は`:inserted_at`と`:updated_at`をカラムに追加する。

### drop

dropでテーブルやインデックスを削除する。

```elixir:
drop table(:users)
drop index(:users, [:username])
```

### alter

alterはテーブルの定義を変更するときに使う。
`remove`、`modiry`、`description`が使える。

```elixir:
alter table(:users) do
  remove :age
  modify :age, :years
  add :description, :text
end
```

### exists?

テーブルの存在を確認したいときはこうする。
```elixir:
exists? table(:users)
```
### rename

`rename`でテーブル名を変更することもできる。

```elixir:
rename table(:users), table(:new_users)
```

## Ectoのクエリ
Elixirのクエリは`from`マクロを通して構築することができる。そして関数合成のようにクエリを **合成可能** なことが大きな特徴だ。
クエリは以下のようなお馴染みのSQLのような構文で構築していける。

```elixir:
from u in User,
where: u.age > 20,
select: u.name
```

### 使用可能なクエリ

* `distinct`
* `exclude`
* `from`
* `group_by`
* `having`
* `join`
* `limit`
* `lock`
* `offset`
* `order_by`
* `preload`
* `select`
* `update`
* `where`

### 使用可能なオペレーター

* `==`, `!=`, `<=`, `>=`, `<`, `>`
* `and`, `or`, `not`
* `in`
* `like`, `ilike`
* `is_nil`
* `count`, `avg`, `sum`, `min`, `max`
* `datetime_add`, `date_add`

#### `like`, `ilike`

```elixir:
from b in Books,
where: ilike(b.title, "Elixir%")
```

#### `avg`, `sum`, `min`, `max`

```elixir:
from b in Books,
select: avg(b.price)
```

#### `datetime_add`, `date_add`

直近1ヶ月に発売された本のリストを返すクエリはこう書けば良い。

```elixir:
from book in Books,
where book.published_at > datetime_add(^Ecto.DateTime.utc, -1, "month")
```

### Elixirコードの挿入

Elixirのソースコードをクエリに含めるには変数や式の前に`^`をつける。

```elixir:
from u in User,
where: u.age > ^age and u.height > ^(height * 100)
```

### クエリの合成

クエリ操作は合成することができる。第一引数をQueryableプロトコルを実装した変数を受け取る関数定義をしておいてパイプ演算子で合成するとかっこいい。

```elixir:
defmodule App.Book do
  use App.Web, :model

  schema "books" do
    field :title, :string
    field :price, :integer
    field :published_at, Ecto.DateTime

    timestamps
  end

  def published_recently(query) do
    from b in query,
    where: b.published_at > datetime_add(^Ecto.DateTime.utc, -1, "month")
  end

  def starts_with(query, title) do
    from b in query,
    where: ilike(b.title, ^(title <> "%"))
  end

  def lower_price(query, price) do
    from b in query,
    where: b.price < ^price
  end
end
```

と定義しておけば、直近1ヶ月で、Elixirから始まるタイトルかつ3000円以下の本を以下のように取得してくることができる。

```elixir:
alias App.Book
Book 
|> Book.starts_with("Elixir") 
|> Book.lower_price(3000) 
|> Book.published_recently
|> Repo.all
```