source(file.path(usethis::proj_get(), "vignettes", "_common.R"))
cat(knitr::knit_child('./vignettes/details/_Repository.Rmd', quiet = TRUE))
The code of the abstract base class of Repository is
path_snippet <- usethis::proj_path("R", "object_relational-Repository.R") snippet <- readLines(path_snippet) snippet <- snippet |> discard_comments() |> discard_null() |> discard_empty_lines()
message("By passing the input argument `inherit = Singleton`, the `AbstractRepository` inherits the qualities of the [**Singleton** pattern](https://tidylab.github.io/R6P/articles/patterns/Singleton.html).")
The given implementing of AbstractRepository
requires you to define four
functions:
(1) initialize
establishes a database connection of some sort;
(2) add
adds one or more domain objects into the database;
(3) del
deletes one or more domain objects from the database; and
(4) get
retrieve one or more domain objects from the database.
message("In general, the **Repository** patterns requires at least the `add` and `get` operations. However, you may rename those operations to fit your context. For example, if you use **Repository** to access various tables in a database, `write_table` and `read_table` might be better names.")
warning("It is up to you to devise a policy that defines (A) what to do when the same entity is added to the Repository; and (B) what to do when a query matches no results.")
Each Repository implementation is project specific. The following implementation is a Repository of car models with their specifications.
From the caller perspective, both implementations behave identically -- they have the same queries. Nevertheless, under the hood the two implementations employ different storage approaches.
collections
message("Transient implementations are a temporal solution that is good for testing and rapid prototyping.")
Transient implementations contribute to rapid development because:
They can be used before you establish/get access to a real database.
They are fast to establish in comparison to DBMS
Transient implementations are useful during testing because they are independent of the real database (if any), that means:
They start as empty storage allowing the programmer to test specific behaviour of the caller.
stop("Transient implementations are not recommended during the production stage. Transient storage is lost when a session is rebooted. You should think about what are the ramifications of losing all the data put into storage.")
First, we define the class constructor, initialize
, to establish a transient
data storage. In this case we use a dictionary from the collections
package.
Second, we define the add
, del
and get
functions that operate on the
dictionary.
As an optional step, we define the NULL object. In this case, rather then the
reserved word NULL
, the NULL object is a data.frame with 0 rows and predefined
column.
TransientRepository <- R6::R6Class( classname = "Repository", inherit = R6P::AbstractRepository, public = list( initialize = function() { private$cars <- collections::dict() }, add = function(key, value) { private$cars$set(key, value) invisible(self) }, del = function(key) { private$cars$remove(key) invisible(self) }, get = function(key) { return(private$cars$get(key, default = private$NULL_car)) } ), private = list( NULL_car = cbind(uid = NA_character_, datasets::mtcars)[0, ], cars = NULL ) )
Adding customised operations is also possible via the R6 set
function. The
following example, adds a query that returns all the objects in the database
TransientRepository$set("public", "get_all_cars", overwrite = TRUE, function() { result <- private$cars$values() |> dplyr::bind_rows() if (nrow(result) == 0) { return(private$NULL_car) } else { return(result) } })
In this example, we use the mtcars
dataset with a uid
column that uniquely
identifies the different cars in the Repository:
mtcars <- datasets::mtcars |> tibble::rownames_to_column("uid") head(mtcars, 2)
Here is how the caller uses the Repository
:
## Instantiate a repository object repository <- TransientRepository$new() ## Add two different cars specification to the repository repository$add(key = "Mazda RX4", value = dplyr::filter(mtcars, uid == "Mazda RX4")) repository$add(key = "Mazda RX4 Wag", value = dplyr::filter(mtcars, uid == "Mazda RX4 Wag")) ## Get "Mazda RX4" specification repository$get(key = "Mazda RX4") ## Get all the specifications in the repository repository$get_all_cars() ## Delete "Mazda RX4" specification repository$del(key = "Mazda RX4") ## Get "Mazda RX4" specification repository$get(key = "Mazda RX4")
DBI
First, we define the class constructor, initialize
, to establish an SQLite
database.
Second, we define the add
, del
and get
functions that operate on the
dictionary.
As an optional step, we define the NULL object. In this case, rather then the
reserved word NULL
, the NULL object is a data.frame with 0 rows and predefined
column.
PersistentRepository <- R6::R6Class( classname = "Repository", inherit = AbstractRepository, public = list( #' @param immediate (`logical`) Should queries be committed immediately? initialize = function(immediate = TRUE) { private$immediate <- immediate private$conn <- DBI::dbConnect(RSQLite::SQLite(), dbname = ":memory:") DBI::dbCreateTable(private$conn, "mtcars", private$NULL_car) }, add = function(key, value) { car <- private$NULL_car |> tibble::add_row(value) self$del(key = key) DBI::dbAppendTable(private$conn, "mtcars", car) invisible(self) }, del = function(key) { statement <- paste0("DELETE FROM mtcars WHERE uid = '", key, "'") DBI::dbExecute(private$conn, statement, immediate = private$immediate) invisible(self) }, get = function(key) { statement <- paste0("SELECT * FROM mtcars WHERE uid = '", key, "'") result <- DBI::dbGetQuery(private$conn, statement) if (nrow(result) == 0) { return(private$NULL_car) } else { return(result) } } ), private = list( NULL_car = cbind(uid = NA_character_, datasets::mtcars)[0, ], immediate = NULL, conn = NULL ) )
Adding customised operations is also possible via the R6 set
function. The
following example, adds a query that returns all the objects in the database
PersistentRepository$set("public", "get_all_cars", overwrite = TRUE, function() { statement <- "SELECT * FROM mtcars" result <- DBI::dbGetQuery(private$conn, statement) if (nrow(result) == 0) { return(private$NULL_car) } else { return(result) } })
In this example, we use the mtcars
dataset with a uid
column that uniquely
identifies the different cars in the Repository:
mtcars <- datasets::mtcars |> tibble::rownames_to_column("uid") head(mtcars, 2)
Here is how the caller uses the Repository
:
## Instantiate a repository object repository <- PersistentRepository$new() ## Add two different cars specification to the repository repository$add(key = "Mazda RX4", value = dplyr::filter(mtcars, uid == "Mazda RX4")) repository$add(key = "Mazda RX4 Wag", value = dplyr::filter(mtcars, uid == "Mazda RX4 Wag")) ## Get "Mazda RX4" specification repository$get(key = "Mazda RX4") ## Get all the specifications in the repository repository$get_all_cars() ## Delete "Mazda RX4" specification repository$del(key = "Mazda RX4") ## Get "Mazda RX4" specification repository$get(key = "Mazda RX4")
Repository at Martin Fowler Blog
Add the following code to your website.
For more information on customizing the embed code, read Embedding Snippets.