如何使用 dbplyr 和 dplyr 构建用于查询数据库的包装函数,使查询有所不同

How to build a wrapper function for querying database using dbplyr and dplyr, having the query vary

我正在尝试使用 {dplyr}{dbplyr} 构建用于查询 SQL 数据库的包装函数。它始终是同一个数据库,通过同一个连接访问。唯一不同的是查询。

让我们使用基于 Hadley 书中代码的示例 here:

library(DBI)
library(dplyr, warn.conflicts = FALSE)

con <- DBI::dbConnect(RSQLite::SQLite(), filename = ":memory:")
mtcars_db <- dplyr::copy_to(con, mtcars)

mtcars_db %>%
  filter(cyl > 2) %>%
  select(mpg:hp) %>%
  head(10) %>%
  collect()
#> # A tibble: 10 x 4
#>      mpg   cyl  disp    hp
#>    <dbl> <dbl> <dbl> <dbl>
#>  1  21       6  160    110
#>  2  21       6  160    110
#>  3  22.8     4  108     93
#>  4  21.4     6  258    110
#>  5  18.7     8  360    175
#>  6  18.1     6  225    105
#>  7  14.3     8  360    245
#>  8  24.4     4  147.    62
#>  9  22.8     4  141.    95
#> 10  19.2     6  168.   123

reprex package (v2.0.0)

于 2021-09-13 创建

或者,我们可能想要一个不同的查询,例如获取多个列的 min() 值(例如,mpgdispdrat ).

library(DBI)
library(dplyr, warn.conflicts = FALSE)

con <- DBI::dbConnect(RSQLite::SQLite(), filename = ":memory:")
mtcars_db <- dplyr::copy_to(con, mtcars)

mtcars_db %>%
  summarise(min_mpg = min(mpg), min_disp = min(disp), min_drat = min(drat)) %>%
  collect()
#> # A tibble: 1 x 3
#>   min_mpg min_disp min_drat
#>     <dbl>    <dbl>    <dbl>
#> 1    10.4     71.1     2.76

reprex package (v2.0.0)

于 2021-09-13 创建

鉴于上述结构 (mtcars_db -> "query" -> collect()) 我想构建一个包装函数 get_data_from_db() 可以灵活地接受不同的查询。

我的失败尝试

library(dplyr, warn.conflicts = FALSE)

get_data_from_db <- function(kind_of_query) {
  
  con <- DBI::dbConnect(RSQLite::SQLite(), filename = ":memory:")
  mtcars_db <- dplyr::copy_to(con, mtcars)
  
  if (kind_of_query == "from_hadley_book") {
    my_query <-
      rlang::expr(
        filter(cyl > 2) %>%
          select(mpg:hp) %>%
          head(10)
      )
  }
  
  if (kind_of_query == "mins_for_mpg_disp_drat") {
    my_query <- 
      rlang::expr(
      summarise(min_mpg = min(mpg), min_disp = min(disp), min_drat = min(drat))
      )
  }
  
  mtcars_db %>%
    eval(my_query) %>%
    collect()
}

get_data_from_db("from_hadley_book")
#> Error in eval(., my_query): invalid 'envir' argument of type 'language'
get_data_from_db("mins_for_mpg_disp_drat")
#> Error in eval(., my_query): invalid 'envir' argument of type 'language'

reprex package (v2.0.0)

于 2021-09-13 创建

我只是尝试使用 rlang::expr(),然后使用 eval(),但对于解决此问题,此策略通常可能不正确。很乐意学习如何使用任何相关方法修复 get_data_from_db()


编辑


我想问一下与这个问题相同上下文的另一种情况。

让我们以 get_data_from_db() 及其参数 kind_of_query 为例。如果我希望传递给 kind_of_query 的内容具有更大的灵活性,以便我可以将 dplyr 动词链 传递给参数,该怎么办?

也就是说,不是get_data_from_db("from_hadley_book")我怎么能做到get_data_from_db(kind_of_query = filter(cyl > 2) %>% select(mpg:hp) %>% head(10))

基本上这意味着 get_data_from_db() 只是一个包装器,它将 mtcars_dbcollect() 围绕通过 kind_of_query 参数传递的查询“夹在中间”。

所以这个 get_data_from_db() 的“灵活”版本看起来像:

get_data_from_db <- function(kind_of_query) {
  
  con <- DBI::dbConnect(RSQLite::SQLite(), filename = ":memory:")
  mtcars_db <- dplyr::copy_to(con, mtcars)
  
  mtcars_db %>%
    eval(kind_of_query) %>%
    collect()
}

## calling the function
get_data_from_db(kind_of_query = 
                   filter(cyl > 2) %>% 
                   select(mpg:hp) %>% 
                   head(10)
                 )

知道如何实现吗?

我认为问题的目的是避免在 mtcars_db -> "query" -> collect() 过程中针对两种不同的情况重复 mtcars_dbcollect() 步骤。由于只有 query 部分在两个条件之间发生变化,我们只需要改变它。现在 mtcars_dbcollect() 阶段只是示例,它们本身可以包含多个步骤,这对于两个查询都是通用的。

我的回答没有以 OP 想要的方式回答问题,但如果我必须这样做,我会这样做。公共阶段可以保存在一个变量中,然后可以根据传递的条件在 if 中使用。

get_data_from_db <- function(kind_of_query) {
  
  con <- DBI::dbConnect(RSQLite::SQLite(), filename = ":memory:")
  mtcars_db <- dplyr::copy_to(con, mtcars)
  
  common_piped_data <- mtcars_db
  if (kind_of_query == "from_hadley_book") {
    my_query <- common_piped_data %>% filter(cyl > 2) %>%  select(mpg:hp) %>%  head(10)
      
  }
  
  if (kind_of_query == "mins_for_mpg_disp_drat") {
    my_query <- common_piped_data %>% 
                  summarise(min_mpg = min(mpg), min_disp = min(disp), min_drat = min(drat))
  }
  my_query %>% collect()

}

运行函数-

get_data_from_db("from_hadley_book")
# A tibble: 10 x 4
#     mpg   cyl  disp    hp
#   <dbl> <dbl> <dbl> <dbl>
# 1  21       6 160     110
# 2  21       6 160     110
# 3  22.8     4 108      93
# 4  21.4     6 258     110
# 5  18.7     8 360     175
# 6  18.1     6 225     105
# 7  14.3     8 360     245
# 8  24.4     4 146.7    62
# 9  22.8     4 140.8    95
#10  19.2     6 167.6   123

get_data_from_db("mins_for_mpg_disp_drat")
# A tibble: 1 x 3
#  min_mpg min_disp min_drat
#    <dbl>    <dbl>    <dbl>
#1    10.4     71.1     2.76

管道运算符 %>% 需要一个函数,其第一个参数是要通过管道传输的对象,请参阅 introducing magrittr。您提供了一个表达式而不是函数,这解释了错误消息。

为了能够提供文字 dplyr 查询,您可以 deparse 输入表达式,将 pipe 构建为字符串,然后 parse 返回: :

get_data_from_db <- function(kind_of_query) {
  
  con <- DBI::dbConnect(RSQLite::SQLite(), filename = ":memory:")
  mtcars_db <- dplyr::copy_to(con, mtcars)
  
  query <- deparse(substitute(kind_of_query))
  
  expr <- paste("mtcars_db %>%", query,"%>% collect()")
  eval(parse(text=expr))
}

## calling the function
get_data_from_db(kind_of_query = 
                   filter(cyl > 2) %>% 
                   select(mpg:hp) %>% 
                   head(10)
)

# A tibble: 10 x 4
     mpg   cyl  disp    hp
   <dbl> <dbl> <dbl> <dbl>
 1  21       6  160    110
 2  21       6  160    110
 3  22.8     4  108     93
 4  21.4     6  258    110
 5  18.7     8  360    175
 6  18.1     6  225    105
 7  14.3     8  360    245
 8  24.4     4  147.    62
 9  22.8     4  141.    95
10  19.2     6  168.   123

这应该可以满足您对这种特定情况的需求,请记住解析通常不是推荐的解决方案:

fortunes::fortune(106)

If the answer is parse() you should usually rethink the question.
   -- Thomas Lumley
      R-help (February 2005)

@Waldi 抓住了问题的关键,管道需要一个函数而不是表达式作为 rhs。在列表案例中的 specific/choose 中,您可以控制表达式的构建,因此这是可管理的。您可以使用 magrittr 语义和点占位符从 kind_of_query 构建。这又可用于使用 rlang::quo!! 运算符创建完整的表达式 (query)。

get_data_from_db <- function(kind_of_query) {
  
  con <- DBI::dbConnect(RSQLite::SQLite(), filename = ":memory:")
  on.exit(DBI::dbDisconnect(con))
  mtcars_db <- dplyr::copy_to(con, mtcars)
  
  if (kind_of_query == "from_hadley_book") {
    my_query <-
      rlang::expr(
        {
            filter(., cyl > 2) %>%
            select(mpg:hp) %>%
            head(10)
        }
      )
  }
  
  if (kind_of_query == "mins_for_mpg_disp_drat") {
    my_query <- 
      rlang::expr(
        {summarise(., min_mpg = min(mpg), min_disp = min(disp), min_drat = min(drat))}
      )
  }
  
  query <- quo(
    mtcars_db %>%  
      !!my_query %>%
      collect()
  )
  
  eval_tidy(query)
  
}

这实际上是一种过于复杂的方法。如果您正在为 kind_of_query 编写表达式,您不妨通过编写一个函数来简化它。

get_data_from_db2 <- function(kind_of_query) {
  
  con <- DBI::dbConnect(RSQLite::SQLite(), filename = ":memory:")
  on.exit(DBI::dbDisconnect(con))
  mtcars_db <- dplyr::copy_to(con, mtcars)
  
  if (kind_of_query == "from_hadley_book") {
    my_fx <- function(x){
      x %>% 
        filter(cyl > 2) %>%
        select(mpg:hp) %>%
        head(10)
    }
  }
  
  if (kind_of_query == "mins_for_mpg_disp_drat") {
    my_fx <- function(x){
      summarise(x, min_mpg = min(mpg), min_disp = min(disp), min_drat = min(drat))
    }
  }
  
    mtcars_db %>%  
      my_fx %>%
      collect()
    
}

一般情况下都会出现问题。在当前提议的接口中,您正试图将参数值注入用户定义的表达式中。 !! 运算符强制求值,因此在构建新表达式时,用户表达式被插入到 () 中以在从管道的 lhs 传递任何内容之前强制其求值。然后,按照@Waldi 的建议,操作表达式可能需要 deparse 或抽象语法树的一些低级操作。

如果可能的话,更简单的解决方案是让您的用户传入一个类似于 purrr::maplapply 的函数。这将大大简化函数实现

get_data_from_db_general <- function(kind_of_query) {
  
  con <- DBI::dbConnect(RSQLite::SQLite(), filename = ":memory:")
  on.exit(DBI::dbDisconnect(con))
  mtcars_db <- dplyr::copy_to(con, mtcars)

  mtcars_db %>%
    kind_of_query %>%
    collect()
}

get_data_from_db_general(
  kind_of_query = function(x){
    x %>%
      filter(cyl > 2) %>%
      select(mpg:hp) %>%
      head(10)
  }
)

# A tibble: 10 x 4
     mpg   cyl  disp    hp
   <dbl> <dbl> <dbl> <dbl>
 1  21       6  160    110
 2  21       6  160    110
 3  22.8     4  108     93
 4  21.4     6  258    110
 5  18.7     8  360    175
 6  18.1     6  225    105
 7  14.3     8  360    245
 8  24.4     4  147.    62
 9  22.8     4  141.    95
10  19.2     6  168.   123