简化:将代码移到数据库函数中

·527 字·3 分钟
PostgreSQL database json api
n3xtchen
作者
n3xtchen
Sharing Funny Tech With You

原文引用: Simplify: move code into database functions

如果你是一个网页或者接口开发者,并且使用数据库,那这边适合你。

我发现了一种不同寻常和有用的方式构建代码。这对我来说是非常的与众不同,因此我必须分享它。

事情是怎样的呢? #

大部分 Web 开发者-无论是依赖还是不依赖框架(如 Rails,Django,Laravel,Sinatra,Flask 或者是 Symfony)-都适用同样的模式工作:

  • 核心的数据库只是作为数据的存储
  • 所有逻辑都在 Ruby/Python/PHP/Javascript 类中。

为什么这样不好呢?? #

这样可能会导致一些潜在的问题:

  • 所有东西都必须通过 Ruby/Python/PHP/Javascript 类来实现-也包括 shell 脚本以及其他不属于网站的部分;
  • 所有东西都不能直接访问数据;为了这么做,你必须要把逻辑定义在其他语言中;
  • 数据被当作愚蠢的存储器,即使数据库足够智能,可以完成大部分的逻辑;
  • 如果你增加业务逻辑到数据库中,那你需要同时变更代码和数据结构;如果它的规则也变化,那就需要修改更多的地方;
  • 两个系统-数据和围绕它的代码-相互捆绑又彼此独立;
  • 如果需要使用其他语言(比如从 Ruby 到 Javascript,或者 Python 到 Elixir),你就必须要重写所有的东西。

简单 VS 复杂 #

你应该去听听 Rich Hichkey 35 分钟令人惊叹的演讲 Simplicity Matters by Rich Hickey

对于这篇文章,与他的观点不谋而合:

  • 复杂 是客观存在。它意味着很多东西都捆绑在一起;
  • 简单 也是客观存在的。它可以是一种原材料,复杂的对立面;
  • 它门都和 简单 无关。你可以很容易的安装和自己绑定在一些非常复杂的东西上(如 ORM),有时候创建一个简单的东西却很困难;
  • 类,模型以及方法(OOP)是一个不必要并发症;
  • 信息是简单的,但是不要把它们隐藏在一个宏语言后面;
  • 直接用数值工作:哈希数组;
  • 如果一个 JSON 接口-一个哈希数组-作为最终接口,那就更有理由跳过抽象成,直接使用数值工作

为什么这个很触动我 #

我从 1997 年开始,一直都使用相同的数据库:同一个数据,值和 SQL 表。但是代码却标更了很多次。

在 1997 年,我使用 Perl。1998 年,我转到 PHP。2004 年,我使用 Rails 重写。2007 年,我又转回道 PHP。在 2009 年,使用极简的 Ruby。2012 年,使用客户端的 Javascript

每一次,我都要围绕数据库重写所有的逻辑:如何添加一个用户信息到数据库,如果验证支票的有效性,以及如何标识一个订单是已付费的等等。

但是 在这整个过程,我信任的 PostgreSQL 数据库是唯一不变的

因为大部分都是 数据逻辑,而不是业务逻辑,所以它应该在数据库中。

因此,我把我的数据逻辑直接存储在 PostgreSQL 中,因为我还计划使用它很多年,但是持续计划使用编程做实验(TO-DO:Haskell,Elixir,Racket,Lua)。

那应该是怎样的呢? #

Web 开发者已经把数据库作为愚蠢的存储,但是实际上它已经很智能了。

在数据库中就能简单德实现所有的逻辑。

但是把他捆绑在外部的代码就会变的很复杂。

一旦你把所有的逻辑都放到数据库中,那外部的代码就消失了!

然后数据库是自包含的,不用绑定任何东西。

你的外部接口可以很容易转到 Javascript,Haskell,Elixir 或者其他任何东西,因为你的核心逻辑都都在数据库中。

那怎么做呢? #

表约束 #

最简单的就是从约束(Constraints)开始:

CREATE TABLE people (
  id serial primary key,
  name text NOT NULL CONSTRAINT no_name CHECK (LENGTH(name) > 0),
  email text UNIQUE CONSTRAINT valid_email CHECK (email ~ '\A\S+@\S+\Z')
);
CREATE TABLE tags (
  person_id integer NOT NULL REDERENCES people(id) ON DELETE CASCADE,
  tag varchar(16) CONSTRAINT tag_format CHECK (statkey ~ '\A[a-z0-9._-]+\Z')
);

在这里定义了数据的有效性验证。

people 这张表中,name 不能为空,email 必须比符合规范(包含 @ 和 . ,不能包含空格)。tags.person_id 必须存在 people 中,但是如果 people 中删除了,tags 表中对应的也会删除;另外 tag 必须要符合规范,只能是小写字母,数字,点,下划线或者破折号。

触发器(Triggers) #

如果一些操作必须在你修改数据的之前或者之后触发,那就要使用 trigger

CREATE FUNCTION clean() RETURNS TRIGGER AS $$
BEGIN
  NEW.name = btrim(regexp_replace(NEW.name, '\s+', ' ', 'g'));
  NEW.email = lower(regexp_replace(NEW.email, '\s', '', 'g'));
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER clean BEFORE INSERT OR UPDATE OF name, email ON people
  FOR EACH ROW EXECUTE PROCEDURE clean();

这个例子在我们添加数据到数据库之前格式化输入,以防有些人不小心把空格输到邮箱中,或者换行符到他的名字总。

函数(Functions) #

编写一些可复用的函数,来代替反复使用的代码。

CREATE FUNCTION get_person(a_name text, a_email text) RETURNS SETOF people AS $$
BEGIN
  IF NOT EXISTS (SELECT 1 FROM people WHERE email = a_email) THEN
    RETURN QUERY INSERT INTO people (name, email)
      VALUES (a_name, a_email) RETURNING people.*;
  ELSE
    RETURN QUERY SELECT * FROM people WHERE email = a_email;
  END IF;
END;
$$ LANGUAGE plpgsql;

这是我经常食用的:给定某人的用户名和邮箱,如果他不在我们的数据库中,添加他。然后,从数据库中返回这个用户的信息。

用于 JSON 的视图 #

替代外部代码把你的数据转化成 JSON,你可以 直接在数据库中创建 JSON

在这里,使用 视图来作为 JSON 的结构模版。在视图中,使用 json_agg 来嵌入值。

CREATE VIEW person_view AS
  SELECT *, (SELECT json_agg(t) AS tags FROM
    (SELECT tag FROM tags WHERE person_id=people.id) t)
  FROM people;

这个将在下面 API 的函数中用到。

API 函数 #

外部代码只能通过这些函数访问数据库。

他们都只返回 JSON。

CREATE FUNCTION update_password(p_id integer, nu_pass text, OUT js json) AS $$
BEGIN
  UPDATE people SET password=crypt(nu_pass, gen_salt('bf', 8)) WHERE id = p_id;
  js := row_to_json(r) FROM (SELECT * FROM person_view WHERE id = p_id) r;
END;
$$ LANGUAGE plpgsql;

CREATE FUNCTION people_with_tag(a_tag text, OUT js json) AS $$
BEGIN
  js := json_agg(r) FROM
    (SELECT * FROM person_view WHERE id IN
      (SELECT person_id FROM tags WHERE tag = a_tag)) r;
END;
$$ LANGUAGE plpgsql;

任何和数据相关的操作,你都可以使用 PostgreSQL 内置的 存储过程语言

PL/pgSQL 不是一个最简洁的语言,但是为了简化数据库操作,还是值得的。

如果你喜欢 Javascript,你可能需要 PLv8,这有个 关于它好的分享

现在,如果你需要一个 REST API: #

require 'pg'
require 'sinatra'
DB = PG::Connection.new(dbconfig)

def qry(sql, params=[])
  @res = DB.exec_params('SELECT js FROM ' + sql, params)
end

after do
  content_type 'application/json'
  body @res[0]['js']
end

get '/people' do
  qry('get_people()')
end

get %r{/people/([0-9]+)} do |id|
  qry('get_person($1)', [id])
end

put %r{/people/([0-9]+)} do |id|
  qry('update_password($1, $2)', [id, params[:password]])
end

get '/people/tagged' do
  qry('people_with_tag($1)', [params[:tag]])
end

或者你需要一个客户端库: #

require 'pg'
require 'json'
DB = PG::Connection.new(dbconfig)

def js(func, params=[])
  res = DB.exec_params('SELECT js FROM ' + func, params)
  JSON.parse(res[0]['js'])
end

def people
  js('get_people()')
end

def person(id)
  js('get_person($1)', [id])
end

def update_password(id, newpass)
  js('update_password($1, $2)', [id, newpass])
end

def people_tagged(tag)
  js('people_with_tag($1)', [tag])
end

就到这里! #

无论是 REST API 还是客户端库,你所需要做的就是传递参数给数据库函数,返回 JSON。

我并不打算说服所有人都这么做。但是我只希望它能对你有用处,或者至少听起来有趣。