用JavaScript构建一个类似LINQ的查询API

本文关键字:LINQ API 查询 一个 构建 JavaScript | 更新日期: 2023-09-27 18:00:51

我想编写一个类似C#的IQueryable<T>接口的JavaScript类,以实现针对远程数据源的格式良好的查询。换句话说,我希望能够编写以下内容(使用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查询(或使用的任何查询语言(实际上被发送到服务器。(在客户端进行过滤是不可行的,因为服务器可能会返回大量数据。(

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

var breadType = 'rye';
datasource.query(Sandwich)
    .where(s => s.bread.type == breadType)
...

它失败了,因为您将有一个无法用值替换的标记breadType。据我所知,JavaScript没有办法内省函数闭包,也没有办法在外部获取事实之后的breadType值。

我的下一个想法是,由于Esprima会给我一个令牌列表,我可以将函数体修改为类似的东西

return {
    'breadType': breadType
};

然后调用它,利用这样一个事实,即使I不能访问闭包,函数本身也可以。但是,在适当的位置修改函数的代码似乎也是不可能的。

另一种不需要Esprima的方法是将sentinel对象作为参数传递给内部函数f,并覆盖其比较运算符,这就是SQLAlchemy的filter()在Python中的工作方式。但是Python提供了运算符重载,而JavaScript没有,所以这也失败了。

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

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

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

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

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

ES6的Proxy对象将使其易于实现,但它仍然不如具有常规运算符的版本。

抱歉发了这么长的邮件。我的最终问题是是否可以用第一个代码块中显示的理想语法实现这一点,如果可以,如何实现。谢谢

用JavaScript构建一个类似LINQ的查询API

不,由于您概述的原因,这确实是不可能的。如果您想支持将任意闭包作为参数传递,那么您唯一的选择就是执行这些函数。您无法将它们转换为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 })
`)

这样您就可以保持您想要的语法,但必须显式地提供所有外部变量。