也不知道今天写不写得完,写多少算多少吧。
Elixir
是一门怎样的语言,此处不表,可以看一篇被转载了无数次的翻译,我顺带也整合进了专栏:
今天介绍一下 Ecto
的使用,和一些容易踩坑的地方。这篇文章也会持续记录我踩到的关于 Ecto
的坑,给大家在缺乏足够文档的时候做做参考。
Ecto is a toolkit for data mapping and language integrated query for Elixir.
—— https:// github.com/elixir-ecto/ ecto
Ecto 团队
(其实感觉就 josevalim 一个人主力在撸。。)如是说道:Ecto
是一个数据映射和程序语言查询的工具。
Ecto
不是一个 ORM
,ORM
是面向对象的专属产物,而 Elixir
里面是没有对象的(嗯。写多了会单身?)。但 Ecto
做的事情和 Java
的 MyBatis
很类似,就是用程式语言去生成 SQL
,执行查询并将数据匹配为程式语言的数据结构。得益于 Elixir
支持秀出天际的宏(具体后面文章会讲),Ecto
完美地定义了自己的 DSL
,写法和写 SQL
类似,所以还是非常直观。
废话不多说,直接开始走代码!
Ecto 环境配置
创建一个项目(这里用 Phoenix
项目模版举例)
$shell> mix phx.new zhihu_demo
首先我们需要在 mix.exs
文件添加我们的依赖,我们使用 Postgres
进行演示,需要的同学可以自行换成 MySQL
:
{:postgrex, ">= 0.0.0"}
{:ecto_sql, "~> 3.1"}
然后在 config/dev.exs
中配置好数据库信息:
# Configure your database
config :zhihu_demo, ZhihuDemo.Repo,
username: System.get_env("DATABASE_USERNAME") || "root",
password: System.get_env("DATABASE_PASSWORD") || "123456",
database: System.get_env("DATABASE_DATABASE") || "zhihu_demo_dev",
hostname: System.get_env("DATABASE_HOSTNAME") || "localhost",
port: System.get_env("DATABASE_PORT") || "5432",
show_sensitive_data_on_connection_error: true,
pool_size: 10
这里的 System.get_env/1
可以获取系统环境变量,方便部署的时候配置和代码隔离(想想真省心)。上面只配置了 dev 环境,其余环境配置方法是一样的,修改数据库配置为正式/测试/开发环境的数据库就行。
然后我们创建一个 ZhihuDemo.Repo
用于数据库的读取操作:
defmodule ZhihuDemo.Repo do
use Ecto.Repo,
otp_app: :zhihu_demo,
adapter: Ecto.Adapters.Postgres
end
因为数据库是需要保持连接的,所以 Ecto
需要在项目启动的时候拥有一个自己的进程。我们需要在 Application
里面添加 ZhihuDemo.Repo
:
defmodule ZhihuDemo.Application do
# See https://hexdocs.pm/elixir/Application.html
# for more information on OTP Applications
@moduledoc false
use Application
def start(_type, _args) do
# List all child processes to be supervised
children = [
# Start the Ecto repository
ZhihuDemo.Repo
]
# See https://hexdocs.pm/elixir/Supervisor.html
# for other strategies and supported options
opts = [strategy: :one_for_one, name: ZhihuDemo.Supervisor]
Supervisor.start_link(children, opts)
end
end
至此,Ecto
的环境就算搭建完毕,没有任何花里胡哨的操作和依赖,就这么干净存粹(此处点名批评 Java Spring
,配置就得让一个新手 debug
一整天)
Ecto Changeset
在讲 Ecto
的数据库操作前,我们先捋一下 Ecto
对于一个查询语句的处理逻辑。
Ecto
会将所有的信息(用户输入的 sql
语句、数据校验结果、约束信息等)都封装成一个 %Ecto.Changeset{}
,这个 changset
名副其实得记录了所有得变更记录。我们看一下这个 module
的描述:
Changesets allow filtering, casting, validation and definition of constraints when manipulating structs.
简单翻译一下:changeset
允许过滤、cast
(这咋翻译??cast
也能翻译?额。。大概就是把一类别数据转化成另一类别数据的一种操作吧,比如 Java
里面的 int number = (int)1.0
就是把 float
cast
成 int
;Ecto
的 cast
类似,主要是根据数据类型做匹配) 、验证数据合法性、校验数据约束关系这些个操作。
我们看一个实际的例子:
#Ecto.Changeset<
action: :insert,
changes: %{
avatar: "https://forktea.com/path_of_image.jpg",
capacity: 1500,
inserted_at: ~N[2020-05-06 19:43:23],
name: "超大杯",
origin_price: 100,
sale_price: 80,
updated_at: ~N[2020-05-06 19:43:23]
},
errors: [
size: {"is invalid", [type: ZhihuDemo.Cup.CupSize, validation: :cast]}
],
data: #ZhihuDemo.Cup<>,
valid?: false
>
这个 changeset
就记录了一个 insert
操作的变更信息,在校验数据的时候发现字段 size
不合法,所以 errors
里面就标示出了这样的错误信息。当 Ecto.Repo
需要执行数据库操作的时候发现 valid?
为 false
且 errors
不为空是会把里面的错误信息当作异常抛出去的,Phoenix
这类 mvc
框架对 Ecto
有支持后就自动会把 Ecto
抛出的错误信息格式化为 HttpResponse
传给用户(当然你也可以接住这些异常然后自定义处理方式)。
正常的 changeset
大概长这样:
#Ecto.Changeset<
action: nil,
changes: %{
avatar: "https://forktea.com/path_of_image.jpg",
capacity: 1500,
name: "超大杯",
origin_price: 100,
sale_price: 80,
size: :huge
},
errors: [],
data: #ZhihuDemo.Cup<>,
valid?: true
>
当 Ecto
拿到这样一个 changeset
后就就知道该干啥了,如果是要执行 insert
操作,就会根据 #ZhihuDemo.Cup<>
这个结构来匹配出对应的 sql
语句进行 insert
操作:
INSERT INTO "cups" ("avatar","capacity","count_like","count_share","inserted_at","name","order_seq","origin_price","sale_price","size","status","updated_at") VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13) RETURNING "id" ["https://forktea.com/path_of_image.jpg", 1500, 0, 0, ~N[2020-05-06 19:50:40], "超大杯", 0, 100, 80, 13, 1, ~N[2020-05-06 19:50:40]]
聪明的宝宝们肯定已经看出来了,Ecto
自个儿就把传入进来的数据参数化了,确保绝壁不会在 Ecto
手里有 sql
注入这种低级错误。
总结一句话:Ecto
将用户想干的事情都写到 changeset
这个小本本里面,然后处理这个 changeset
,将它转化为对应数据库支持的 sql
进行执行。
Ecto 增删查改
我们先建立一个表。我们有一个用户表 user
,字段有 nickname
、avatar
、email
、password
等。如果是 Phoenix
框架下,我们可以用这样的命令创建一个 ecto context
信息,会自动生成相关的代码文件:
#格式:mix phx.gen.context <模块名称> <数据表模块名称> <数据表名> [<字段名:类型>]...
$shell> mix phx.gen.context Accounts User users nickname:string avatar:string email:string password:string
然后我们就有了这样一个 schema
信息:
defmodule Accounts.User do
use Ecto.Schema
import Ecto.Changeset
schema "users" do
field :nickname, :string
field :avatar, :string
field :email, :string
field :password, :string
timestamps()
end
@doc false
def changeset(user, attrs) do
user
|> cast(attrs, [:nickname, :avatar, :email, :password])
|> validate_required([:nickname, :avatar])
end
end
同时附赠 migration
记录(数据库结构变更记录),通常在目录 priv/repo/migrations
下:
defmodule ZhihuDemo.Repo.Migrations.CreateUsers do
use Ecto.Migration
def change do
create table(:users) do
add :nickname, :string
add :avatar, :string
add :email, :string
add :password, :string
timestamps()
end
end
end
(很多项目其实是不带数据库结构变更记录的,这样的话做版本回滚的时候必然凉凉,Ecto
自带变更记录和回滚操作,比起很多框架自个儿开发的用起来必然舒服得多)
同时生成的文件里面还附赠了一个 lib/zhiu_demo/accounts.ex
文件,里面定义了几个简单的增删查改操作。因为我们要仔细讲讲如何使用 Ecto
的增删查改操作,所以这个文件我们就略过不讲。
Ecto INSERT/UPDATE
我等会继续写。