在 JavaScript 中构建类似 LINQ 的查询 API

Building a LINQ-like query API in JavaScript

我想编写一个像 C# 的 IQueryable<T> 接口一样工作的 JavaScript class 来启用针对远程数据源的格式良好的查询。换句话说,我希望能够编写以下内容(使用 ES6 箭头语法):

datasource.query(Sandwich)
    .where(s => s.bread.type == 'rye')
    .orderBy(s => s.ketchup.amount)
    .take(5)
    .select(s => { 'name': s.name });

然后把它变成类似

的东西
SELECT s.name AS name
FROM sandwich s
JOIN bread b ON b.sandwich_id = s.id
JOIN ketchup k on k.sandwich_id = s.id
WHERE b.type = 'rye'
ORDER BY k.amount
LIMIT 5;

SQL 查询(或使用的任何查询语言)实际发送到服务器。 (在客户端进行过滤是不可行的,因为服务器可能 return 吨数据。)

在 C# 中,Expression class 支持此功能,它允许您从 lambda 函数构建表达式树。但据我所知,JavaScript 没有等价物。我最初的计划是将 f.toString() 提供给 Esprima 的解析器,以便将函数 f 作为参数传递给 select()where() 等,并使用该表达式树。只要表达式仅引用文字,这种方法就很有效,但是当你尝试像

var breadType = 'rye';

datasource.query(Sandwich)
    .where(s => s.bread.type == breadType)
...

它失败了,因为您将拥有一个无法用值替换的标记 breadType。据我所知,JavaScript 无法自省函数闭包并在外部获取 breadType 的值。

我的下一个想法是,由于 Esprima 会给我一个标记列表,我可以就地修改函数体,如

return {
    'breadType': breadType
};

然后调用它,利用即使 I 无法访问闭包,函数本身也可以的事实。但是就地修改函数的代码似乎也是不可能的。

另一种不需要 Esprima 的方法是将哨兵对象作为参数传递给内部函数 f 并覆盖其比较运算符,这就是 SQLAlchemy 的 [=25] =] 在 Python 工作。但是 Python 提供运算符重载而 JavaScript 不提供,所以这也失败了。

这给我留下了两个较差的解决方案。一种是做这样的事情:

var breadType = 'rye';

datasource.query(Sandwich)
    .where(s => s.bread.type == breadType)
    .forValues(() => {
        'breadType': breadType
    });

换句话说,我可以强制调用者手动提供闭包上下文。但这很蹩脚。

另一种方法是做哨兵对象的事情,但使用函数而不是运算符,因为运算符不能重载:

var breadType = 'rye';

datasource.query(Sandwich)
    .where(s => s.bread.type.equals(breadType));

ES6 的 Proxy 对象将使实现变得简单,但它仍然不如带有常规运算符的版本。

很抱歉 post。我的最终问题是 是否可以使用第一个代码块中显示的理想语法实现此目的,如果可以,如何实现。谢谢!

不,由于您概述的原因,这确实是不可能的。如果你想支持将任意闭包作为参数传递,那么你唯一的选择就是执行函数。您不能将它们转换为 SQL 语句,在某种程度上,无论您对文件执行多少静态代码分析,这总是会失败。

我想你最好的选择是模板文字,你可以在其中使用

var breadType = 'rye';
datasource.query(Sandwich, `
    .where(s => s.bread.type == ${breadType})
    .orderBy(s => s.ketchup.amount)
    .take(5)
    .select(s => { 'name': s.name })
`)

以便您可以根据需要保留语法,但必须显式提供所有外部变量。