Ecto是一个Elixir的数据库ORM库

gogofyy 9年前

来自: https://medium.com/@tingwang_de/ecto-fbcdb06eaf2c#.cu3pu7pyy


原文,文中黑体字是我的理解。
 Ecto是一个Elixir的数据库ORM库

Ecto 的主要组成组件有四个:

  • Ecto.Repo — Repository 是在数据仓库上的一层封装。通过 repository 我们可以创建、更新、销毁或者查询已有的数据。 为了和数据库连接,一个 repository 需要一个适配器和相应的权限配置。
  • Ecto.Schema — Schema 允许开发人员定义数据结构来映射底层的数据库数据结构
  • Ecto.Changeset — Changeset 为开发人员提供了一个方法来过滤(filter)、转换(cast)外部数据。在提交数据之前,changeset还提供了跟踪、验证变化的机制,保证提交的数据是有效的。在不同的上下文中,对数据的有效性的验证(哪些字段需要验证、如何验证)有可能是不一样的, 所以在Ecto中, 一个Model里会定义多个 changesets 来应对不同的场景。
  • Ecto.Query — 通过Elixir的语法, 把数据从 repository 中取出来。查询在 Ecto 中是安全的,避免了一般的安全问题, 比如 SQL注入等。查询也是可以被组合的,(组合是FP最总要的武器),允许开发者一块一块的创建查询再整体组合起来。

Repositories

Repository 是在数据库外的一层, 我们可以通过下面的代码创建一个 repository

defmodule Repo do  use Ecto.Repo, otp_app: :my_append

Repo必须在应用的配置文件中配置, 通常是在 config/config.exs里:

config: my_app, Repo,  adapter: Ecto.Adapter.Postgres,  database: "ecto_sample",  username: "postgres",  password: "postgres",  hostname: "localhost"

在Ecto中的每个repository都会定义一个 start_link/0, 使用repository之前这个函数必须被调用。(这个应该是otp里启动了一个erlang的进程, 之后对repository的调用都是对一个进程的调用) 一般这个函数是不需要手动调用的,而是在application的 supervision tree里。(这个也是erlang运行的方式, 每个进程都需要看护进程)

如果在生成应用的时候我们用了 supervisor (通过传递 — sup 给 mix new),lib/my_app.ex 文件中会包含应用启动的回调函数, 这个函数内定义并启动你的监控进程(supervisor)。你只需要编辑 start/2 这个函数来把repository作为一个supervisor启动,这个supervoir是在应用的监控下的。

def start(_type, _args) do  import Supervisor.Spec    children = [    supervisor(Repo, []  ]    opts = [strategy: one_for_one, name: MyApp.Supervisor]  Supervisor.start_link(childern, opts)

Schema

Schema提供了一组函数方便你定义结构化数据,数据字段直接的关系并且把这些改变应用到repository里。

让我们看个例子:

defmodule Weather do  use Ecto.Schema    schema "weather" do    field :city, :string    field :temp_lo, :integer    field :temp_hi, :integer    field :procp, :float, default: 0.0  endend

定义schema的时候, ecto会自动的定义一个结构体, 这个结构体包含了schema的所有字段。

iex> wheather = %Weather{temp_lo: 30}iex> weather.temp_lo30

Schema还允许我们和repository交互

iex> weather = %Weather{temp_lo: 0, temp_hi: 23}iex> Repo.insert!(weather)%Weather{...}

在持久化 weather 到数据库库之后, %Weather{} 的一个新拷贝会被返回, 返回的这个拷贝会有插入后的主键只(id)。我们可以通过这个主键从repository中获取结构体。

# 获得结构体iex> weather = Repo.get Weather, 1%Weather{id: 1, ...}# 删除数据iex> Repo.delete!(weather)%Weather{id: 1, ...}
注意:使用 Ecto.Schema就会自动添加一个 :id (:integer),这个id会被用作主键。如果你想使用其他字段作为主键, 你可以通过在schema/2前使用 @primary_key。更多内容可以常看 Ecto.Schema的文档。

这里我们可以看到数据和数据获取代码是怎样隔离开的。一个在Repository里,一个在schema里。 这样做的好处是:

  • 数据定义在结构体内, 保证了数据是轻量级的而且能够被序列化。在很多语言中,数据常常被定义为一个肥大的对象,包含了数据状态变换的方法,这些导致了数据很难被序列化、维护,而且很难被理解。
  • 把数据和repository分开, 也保证了repository没有过多没必要的代码,从而可以直接、高效的访问数据。这也是FP的方式, 就是一个处理数据的链条

changeset

虽然在上面的例子里我们直接向repository里插入、删除结构体,在更新数据的时候我们需要用到changeset来保证Ecto能够有效的跟踪变化。

不仅如此,changeset还允许开发人员对修改的内容做过滤,强制转换或者验证其有效性,只有通过之后才会把修改应用于数据。例如下面我们定义了一个schema

defmodule User do  use Ecto.Schema    import Ecto.Changeset    schema "users" do    field :name    field :email    field :age, :integer  end    def changeset(user, params \\ :invalid) do    user    |> cast(params, [:name, :email, :age])    |> validate_required([:name, :email])    |> validate_format(:email, ~r/@/)    |> validate_inclusion(age, 18..100)  endend

changeset/2 函数首先调用了Ecto.Changeset.cast/3这个函数,传入结构体、参数和必选和可选的字段; 这个函数返回一个changeset。 参数是一个map, 键是二进制值,这个键对应的值会根据其在schema中的定义做强制的转换。

任何不在必选或者可选参数表里的参数都会被忽略掉。如果一个字段被定义为必选, 但是在结构体里和参数中都没有传入, 这个changeset会标记上一个错误信息,这个changeset也会被标定为无效。

强制转换之后, changeset被传给Ecto.Changeset.validate/2,这个函数只会验证有改变的字段。也就是说,如果一个字段不是作为参数传入的, 这个字段压根就不会被验证。model转入的是原本的模型,参数传入的是修改的字段集。例如,在参数集里只有 “name” 和 “email”键,那么“age”的验证是不会被运行的。

def update(id, params) do  changeset = User.changeset Repo.get!(User, id), params["user"]    case Repo.update(changeset) do    {:ok, user} ->      send_resp conn, 200, "Ok"    {:error, changeset} _>      send_resp conn, 400, "Bad request"  endend

user和参数传入changeset/2, 另一个changeset被返回。如果changeset是有效的, 我们把修改持久化到数据库, 如果不是有效的我们会返回一个400。

下面的例子是创建一个user

def create(id, params) do  changeset = User.changeset %User{}, params["user"]    case Repo.insert(changeset) do    {:ok, user} ->      send_resp conn, 200, "Ok"    {:error, changeset} ->      send_resp conn, 400, "Bad request"  endend

显示的定义changeset (一个model会有多个changeset) 的好处是我们可以根据不同的情景定义不同的changeset。例如,我们可以分别处理创建更新的changeset:

def create_changeset(user, params) do  # 创建时使用的Changeset enddef update_changeset(user, params) do  # 修改时使用的Changesetend

Changeset还可以根据数据库的约束(constrains)例如唯一索引、外键等检验是否有效, 如果不通过会产生一个错误, 这样开发人员就可以在保证数据库的一致性的前提下,还能给终端用户友好的提示。查看Ecto.Changeset.unique_contraint/3的文档查看跟多的例子并且了解_constraint函数的用法

Query

最后,Ecto允许你用Elixir的语法写查询语句,并把这些查询发送给repository,repository会把这些elixir写的查询翻译成为底层数据库的查询。让我们看个例子:

import Ecto.Query, only[from: 2]query = from w in Weather,               where: w.prcp > 0 or is_nil(w.prcp),               select: w# 返回符合查询条件的 %Weather{} Repo.all(query)

查询是在from宏中定义扩展的。支持的关键字包括:

  • :distinct
  • :where
  • :order_by
  • :offset
  • :limit
  • :lock
  • :group_by
  • :having
  • :join
  • :select
  • preload

在Ecto.Query模块中你可以找到这些关键字的解释还有具体的例子。所有支持在查询语句里使用的函数都在Ecto.Query.API里。

当我们在写查询语句的时候, 我们其实在使用查询语句的DSL, 如果在这个DSL里使用Elixir的函数或者嵌入参数,我们需要在函数或者参数的前面加上^:

def min_prcp(min) do  from w in Weather, where: w.prcp > ^min or is_nil(w.prcp)end

除了Repo.all/1会返回所有符合条件的结果之外, 我们还可以用Repo.first/1只返回一个结果或者nil,Repo.first!/1会返回一个结果或者在没有结果的时候报错,Repo.get/2根据主机获得结果。

如果你想生写一些SQL代码的话, Ecto提供了片段fragments)参看Ecto.Query.API.fragment/1。初次之外,很多的适配器还提供了直接查询的接口,例如Ecto.Adapters.SQL.query/4。