如何使用 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()
值(例如,mpg
、disp
和 drat
).
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_db
和 collect()
围绕通过 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_db
和 collect()
步骤。由于只有 query
部分在两个条件之间发生变化,我们只需要改变它。现在 mtcars_db
和 collect()
阶段只是示例,它们本身可以包含多个步骤,这对于两个查询都是通用的。
我的回答没有以 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::map
或 lapply
的函数。这将大大简化函数实现
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
我正在尝试使用 {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()
值(例如,mpg
、disp
和 drat
).
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_db
和 collect()
围绕通过 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_db
和 collect()
步骤。由于只有 query
部分在两个条件之间发生变化,我们只需要改变它。现在 mtcars_db
和 collect()
阶段只是示例,它们本身可以包含多个步骤,这对于两个查询都是通用的。
我的回答没有以 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::map
或 lapply
的函数。这将大大简化函数实现
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