选择

“选择 (selection) ”(或“防火墙 (firewall) ”)模式是指当你有一个从其他 Qbase 读取的查询 Qsel,并从 Qsel 返回的 Qbase 中提取一些小信息。

具体地说,Qsel 不组合来自其他查询的值。因此,在某种意义上,Qsel 是多余的 —— 你可以自己从 Qbase 中提取信息,而不需要 Salsa 机制。但 Qsel 的作用在于,它限制了更改 Qbase 时所需的重新执行的数量。

示例:基本查询

例如,假设你有一个 parse 查询,它解析请求的输入文本,并返回一个 ParsedResult,其中包含头信息 (header) 和主体信息 (body):

#[derive(Clone, Debug, PartialEq, Eq)]
struct ParsedResult {
    header: Vec<ParsedHeader>,
    body: String,
}

#[derive(Clone, Debug, PartialEq, Eq)]
struct ParsedHeader {
    key: String,
    value: String,
}

#[salsa::query_group(Request)]
trait RequestParser {
    /// The base text of the request.
    #[salsa::input]
    fn request_text(&self) -> String;

    /// The parsed form of the request.
    fn parse(&self) -> ParsedResult;
} 

示例:选择查询

现在,你有许多派生查询,它们只查看标题。例如,可以提取 Content-Type 的 header:

#[salsa::query_group(Request)]
trait RequestUtil: RequestParser {
    fn content_type(&self) -> Option<String>;
}

fn content_type(db: &dyn RequestUtil) -> Option<String> {
    db.parse()
        .header
        .iter()
        .find(|header| header.key == "content-type")
        .map(|header| header.value.clone())
} 

为什么更推荐选择查询?

这个 content_type 查询是选择模式的一个实例。它只从 ParsedResult 中“选择”一小部分信息。你可能根本没有将其作为查询,而是将其作为 ParsedResult 上的方法。

但是对 content_type 使用查询有一个好处:现在如果有仅依赖于 content_type (或者可能依赖于通过类似模式提取的其他 header)的下游查询,那么当请求改变时,这些查询将不必重新执行,除非 content-type 的 header 改变。考虑以下依赖关系图:

request_text  -->  parse  -->  content_type  -->  (other queries)

request_text 发生变化时, Salsa 总是必须重新执行 parse

  • 如果这产生新的解析结果,Salsa 还将重新执行 content_type
  • 但是如果 content_type 的结果没有改变,那么 Salsa 不会重新执行其他查询

更多级别的选择

事实上,在我们的示例中,可能会考虑引入另一个级别的选择。与其让 content_type 直接访问 parse 的结果,不如插入一个只提取 header 的选择查询:

#[salsa::query_group(Request)]
trait RequestUtil: RequestParser {
    fn header(&self) -> Vec<ParsedHeader>;
    fn content_type(&self) -> Option<String>;
}

fn header(db: &dyn RequestUtil) -> Vec<ParsedHeader> {
    db.parse().header
}

fn content_type(db: &dyn RequestUtil) -> Option<String> {
    db.header()
        .iter()
        .find(|header| header.key == "content-type")
        .map(|header| header.value.clone())
} 

这将产生如下所示的依赖关系图:

request_text  -->  parse  -->  header -->  content_type  -->  (other queries)

这样做的好处是,只影响 body 或只消耗请求的一小部分的更改根本不需要重新执行 content_type。如果有许多依赖的 headers,这尤其有用。

关于克隆和效率

在本例中,我们使用了 VecString 等常见的 Rust 类型,并且相当频繁地克隆 (clone) 它们。

这在 Salsa 中会运行得很好,但可能不是最有效的。这是因为每个克隆都将产生结果的深副本 (deep copy)。

一个简单的解决方法是,你可以将在数据结构是使用 Arc(例如,Arc<Vec<ParsedHeader>>),这会降低克隆成本。