Database bulk update and inline editing in a Shiny Application
Want to share your content on R-bloggers? click here if you have a blog, or here if you don't.
Ava Yang, Data Scientist
There are times when it costs more than it should to leverage javascript, database, html, models and algorithms in one language. Now maybe is time for connecting some dots, without stretching too much.
- If you have been developing shiny apps, consider letting it sit on one live database instead of manipulating data I/O by hand?
- If you use DT to display tables in shiny apps, care to unleash the power of interactivity to its full?
- If you struggle with constructing SQL queries in R, so did we.
Inspired (mainly) by the exciting new inline editing feature of DT, we created a minimal shiny app demo to show how you can update multiple values from DT and send the edits to database at a time.
As seen in the screenshot, after double clicking on a cell and editing the value, Save and Cancel buttons will show up. Continue editing, the updates are stored in a temporary (reactiveValue) object. Click on Save if you want to send bulk updates to database; click on Cancel to reset.
Global
On the global level, we use pool
to manage database connections. A database connection pool object is constructed. With the onStop()
function, the pool object gets closed after a session ends. It massively saves you from worrying about when to open or close a connection.
# Define pool handler by pool on global level pool <- pool::dbPool(drv = dbDriver("PostgreSQL"), dbname="demo", host="localhost", user= "postgres", password="ava2post") onStop(function() { poolClose(pool) }) # important!
The next job is to define a function to update database. The glue_sql
function puts together a SQL query in a human readable way. Writing SQL queries in R was bit of a nightmare. If you used to assemble a SQL clause by sprintf
or past
, you know what I'm talking about. The glued query is then processed by sqlInterpolate
for SQL injection protection before being executed.
updateDB <- function(editedValue, pool, tbl){ # Keep only the last modification for a cell editedValue <- editedValue %>% group_by(row, col) %>% filter(value == dplyr::last(value)| is.na(value)) %>% ungroup() conn <- poolCheckout(pool) lapply(seq_len(nrow(editedValue)), function(i){ id = editedValue$row[i] col = dbListFields(pool, tbl)[editedValue$col[i]] value = editedValue$value[i] query <- glue::glue_sql("UPDATE {`tbl`} SET {`col`} = {value} WHERE id = {id} ", .con = conn) dbExecute(conn, sqlInterpolate(ANSI(), query)) }) poolReturn(conn) print(editedValue) return(invisible()) }
Server
We begin with server.R from defining a couple of reactive values: data for most dynamic data object, dbdata for what's in database, dataSame for whether data has changed from database, editedInfo for edited cell information (row, col and value). Next, create a reactive expression of source data to retrieve data, and assign it to reactive values.
# Generate reactive values rvs <- reactiveValues( data = NA, dbdata = NA, dataSame = TRUE, editedInfo = NA ) # Generate source via reactive expression mysource <- reactive({ pool %>% tbl("nasa") %>% collect() }) # Observe the source, update reactive values accordingly observeEvent(mysource(), { # Lightly format data by arranging id # Not sure why disordered after sending UPDATE query in db data <- mysource() %>% arrange(id) rvs$data <- data rvs$dbdata <- data })
We then render a DataTable object, create its proxy. Note that the editable parameter needs to be explicitly turned on. Finally with some format tweaking, we can merge the cell information, including row id, column id and value, with DT proxy and keep all edits as a single reactive value. See examples for details.
# Render DT table and edit cell # # no curly bracket inside renderDataTable # selection better be none # editable must be TRUE output$mydt <- DT::renderDataTable( rvs$data, rownames = FALSE, editable = TRUE, selection = 'none' ) proxy3 = dataTableProxy('mydt') observeEvent(input$mydt_cell_edit, { info = input$mydt_cell_edit i = info$row j = info$col = info$col + 1 # column index offset by 1 v = info$value info$value <- as.numeric(info$value) rvs$data[i, j] <<- DT::coerceValue(v, purrr::flatten_dbl(rvs$data[i, j])) replaceData(proxy3, rvs$data, resetPaging = FALSE, rownames = FALSE) rvs$dataSame <- identical(rvs$data, rvs$dbdata) if (all(is.na(rvs$editedInfo))) { rvs$editedInfo <- data.frame(info) } else { rvs$editedInfo <- dplyr::bind_rows(rvs$editedInfo, data.frame(info)) } })
Once Save button is clicked upon, send bulk updates to database using the function we defined above. Discard current edits and revert DT to last saved status of database when you hit Cancel. Last chunk is a little trick that generates interactive UI buttons. When dynamic data object differs from the database representative object, show Save and Cancel buttons; otherwise hide them.
# Update edited values in db once save is clicked observeEvent(input$save, { updateDB(editedValue = rvs$editedInfo, pool = pool, tbl = "nasa") rvs$dbdata <- rvs$data rvs$dataSame <- TRUE }) # Observe cancel -> revert to last saved version observeEvent(input$cancel, { rvs$data <- rvs$dbdata rvs$dataSame <- TRUE }) # UI buttons output$buttons <- renderUI({ div( if (! rvs$dataSame) { span( actionButton(inputId = "save", label = "Save", class = "btn-primary"), actionButton(inputId = "cancel", label = "Cancel") ) } else { span() } ) })
UI
The UI part is exactly what you normally do. Nothing new.
Bon Appétit
- Set up a database instance e.g. PostgreSQL, SQLite, mySQL or MS SQL Server etc.
- Download/clone the GitHub repository
- Run through script
app/prep.R
but change database details to one's own. It writes to DB our demo dataset which is the nasa dataset from dplyr with an index column added -
Also update database details in
app/app.R
and runshiny::runApp("app")
Acknowledgements
Workhorse functionality is made possible by:
- DBI: R Database Interface
- RPostgreSQL: R Interface to PostgreSQL (one of many relational database options)
- pool: DBI connection object pooling
- DT: R Interface to the jQuery Plug-in DataTables (requires version >= 0.2.30)
- Shiny: Web Application Framework for R
- dplyr: Data manipulation
- glue: Glue strings to data in R. Small, fast, dependency free interpreted string literals (requires version >= 1.2.0.9000. Blank cell crashes the app with version 1.2.0)
R-bloggers.com offers daily e-mail updates about R news and tutorials about learning R and many other topics. Click here if you're looking to post or find an R/data-science job.
Want to share your content on R-bloggers? click here if you have a blog, or here if you don't.