1920 前端怎么_开个通用的 HTTP 接口,让前端写 SQL 咋样?

陶文:如何让前端程序员没有后端也能完成项目? 简单概述了 Backend as a "Database"。对于前端怎么查询后端并没有详细展开。那么后端是不是应该开个通用的 HTTP 接口,这样前端同学直接写 SQL 就好了呢?

最近有一个开源项目 Tencent/APIJSON 就是这么搞的。GraphQL 其实也是差不多,写起来是这个样子

{
  Movie(filter: { OR: [{ year_lt: 1920 }, { title_contains: "River Runs" }] }) {
    title
    year
  }
}

某种程度上这些都是 "SQL over HTTP" 的搞法。如果不这么搞,那么我们应该暴露什么样的 API 给前端呢?

写操作

ORM 的写法一般是

const doc = scene.load(Document, { id: 123 });
doc.content = 'hello';
scene.commit();

通过拦截对象的改动,知道修改什么。然后在 scene.commit 的时候,提交到数据库。这种写法仅仅适用于给用户编辑个自己拥有的文档的场景。我们显然不能允许用户拿这个接口去下订单。

scene.call(PlaceOrder, { products: xxx ... });

所以写操作没啥花样的。GraphQL 称之为 mutation。其实就是 RPC,远程方法调用。

简单的单表读操作

很自然的,我们会想到如下的方式去封装读接口:

// 查询多条
scene.query(Document);
scene.query(Document, { author: 'xyz' });
// 查询有且仅一有一条
scene.load(Document, { id: 123 });
scene.load(RefundPolicy);
// 查询可能有一条
scene.tryLoad(Refund, { orderId: 123 ]); // 返回值可能是 undefined

从 HTTP 接口的角度就两个参数

  • Target:查询哪种表
  • KeyValues:表达查询条件的 key - value 对

但是这样的查询接口显然没有办法满足如下的需求:

  • 大于小于等 operator:GraphQL 的做法是 year_lt: 1920 来代表 year < 1920。这个做法在 SQLAlchemy 等 ORM 上就在用
  • OR 条件:多个 key/value 对是 OR 的关系
  • Limit/Offset
  • 各种 Join / Group By / Having

强行把这些需求加到查询接口上,就是得到 APIJSON 或者 GraphQL 这样的东西。

预先定义 SQL

解决办法就像 MyBatis 那样,提前把 SQL 定义好。

@Biz.view`SELECT id, userId, created_at, SUM(OrderItem.cost) AS total
FROM ${Order}
LEFT JOIN ${OrderItem} on Order.id = OrderItem.orderId`
@Biz.source(Starriness, { dataSource: 'mysql' })
export class OrderWithTotal extends Biz.SqlView {
    public readonly id: string;
    public readonly userId: string;
    public readonly created_at: Date;
    public readonly total: number;
}

这样 OrderWithTotal 这张虚拟的表就代表了一条 SQL。

scene.query(OrderWithTotal)

这样就执行了一条 SQL 查询。SQL 也可以添加一些变量,让查询的时候指定

@Biz.view`SELECT id, userId, created_at, SUM(OrderItem.cost) AS total
FROM ${Order}
LEFT JOIN ${OrderItem} on Order.id = OrderItem.orderId
WHERE :lowerTotal <= total AND total <= :upperTotal`
@Biz.source(Starriness, { dataSource: 'mysql' })
export class OrderWithTotal extends Biz.SqlView {
    public readonly id: string;
    public readonly userId: string;
    public readonly created_at: Date;
    public readonly total: number;
 
    public lowerTotal: number;
    public upperTotal: number;
 
    @viewFilter('OrderItem.type')
    public type: string;
}

通过传入 lowerTotal, upperTotal, type 就可以做一些自定义

scene.query(OrderWithTotal, { type: 'preorder', lowerTotal: 100, upperTotal: 200 });

通过预定义 SQL,基本上可以满足各种 OLAP 的需求。但是这种写法对于 OLTP 来说是过于麻烦了的。我们不希望 SELECT *,然后把表的字段又重新定义一遍。

SELECT * 的查询

为了快速表达 SELECT * 这样的查询,我们仅需要写 where 部分

@Biz.where('seatCount >= :from AND seatCount <= :to')
@Biz.orderBy('created_at', 'ASC')
export class Reservation_SeatInRange extends Biz.Subset<Reservation> {
    public from: number;
    public to: number;
}

当查询 Reservation_SeatInRange 的时候,仅仅指定条件,拿到的是 Reservation。也就是省掉了 SQL 的 SELECT 部分

const revervations = scene.query(Reservation, { from: 2, to: 5 })

总体的写法和 MyBatis Dynamic SQL 是非常类似的。

解决多次网络请求带来的延迟问题

如果你需要查询 a.b.c.d,前端需要发多次请求到后端拉取数据。拿到了 b 之后才能去拿 c,拿到了 c 之后,才能去拿 d。一次公网上的请求至少 100ms,多次往返就会造成肉眼可见的延迟。

这个问题也是 GraphQL 要解决的初衷,也就是快速从后端加载一个对象图到前端。这个问题有三个不同程度的解决方案

  • 加载对象图的根,并且 prefetch 这个根下面的所有子对象。这个在 MyBatis 中叫 collection property,在 Hibernate 中叫 eager loading。
  • 只加载一部分对象图,用条件进行过滤:这个是 GraphQL 的能力,可以在每个分叉都做过滤
  • 在对象图中做 pattern search:这个是图数据库解决的问题。图数据库的查询语言都有比较复杂的 pattern 声明能力。

大部分 OLTP 业务都不涉及到很大的对象图加载到前端显示的问题。尤其是在手机屏幕上显示。如果需要从大量数据中做转换和聚合,那个应该归纳到 OLAP 查询,用 SQL 去解决。所以剩余的需求就是对于 a.b.c.d 这样的对象图做 prefetch 的问题。

  • 声明对象图:无论是 MyBatis 还是 Hibernate,都有这样的 one-to-many 的关联关系的声明
  • 查询的时候指定 prefetch 对象图

声明大概是这样

export class Order extends Biz.ActiveRecord {
  @Biz.query
  public readonly orderProductItems: OrderProductItem[];
  @Biz.query
  public readonly orderShipment: Ordershipment;
}

然后查询一条订单的时候,我们指定 prefetch

const order = scene.load(Order, { id: 123 }, { orderProductItems: true })
console.log(order.orderProductItems) // already prefetched

这个 prefetch 参数,其实就是一种简化后的 GraphQL

scene.load(Order, { id: 123 }, { 
  orderShipment: true,
  orderProductItems: {
    product: {
      productImages: true
    }
  } 
})

用 true 表示需要 prefetch 这个关联关系,用嵌套的 object 表达还有进一步 prefetch。比 GraphQL 减少的功能是没有办法对关联的关系指定过滤条件,只能全部 prefetch 出来。

用 Hibernate 的写法:

  Criteria criteria = sessionFactory.getCurrentSession().createCritiera(Person.class);
  criteria.add(Restrictions.idEq(personId);
  criteria.setFetchMode("roles", FetchMode.SUBSELECT);

大概的意思是差不多的。但是相比 Hibernate 这种后端数据库访问库,前后端通信更需要解决这种多次网络 roundtrip 的问题。

查询语法小结

  • 提前定义好 SQL,而不是 ad-hoc query。基本上照搬了 MyBatis 的做法
  • 网络请求参数极其简单,就是 Target / KeyValues
  • 对象图的加载带来的网络延迟问题是必须解决的。对象图关系提前声明,添加 Prefetch 参数指定要预取的对象图

前端查询后端,和后端通过 MyBatis / Hibernate 查询数据库是解决差不多的问题。都是在尽量方便的情况下减少 I/O 的开销。都是在把一个关系型数据库当对象图数据库在用。太阳底下真没啥新雪。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值