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.
collectionsmessage("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")
DBIFirst, 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.