An Engine
is a connection to a database. Under the hood,
engines call DBI::dbConnect()
to create and manage
connections. This gives you flexibility to use any database supported by
the DBI
package. Connecting to a database with
Engine
is effectively the same as using
DBI::dbConnect()
directly.
So why wrap it in an R6 object?
- To manage opening and closing of connections
- To optionally support connection pooling
- To abstract some repetitive or database-specific boilerplate
Creating an Engine
When creating an Engine, there are two additional arguments worth knowing:
persist = TRUE
: Keeps the connection open between operations likemodel()
orexecute()
. This is required when using an in-memory SQLite database, otherwise the data will be lost between calls.use_pool = TRUE
: Enables connection pooling (via thepool
package), which improves performance in environments like Shiny by reusing open connections across sessions.
library(oRm)
engine <- Engine$new(
drv = RSQLite::SQLite(),
dbname = ":memory:",
persist = TRUE
)
engine
#> <Engine>
#> Public:
#> clone: function (deep = FALSE)
#> close: function ()
#> conn: NULL
#> conn_args: list
#> dialect: sqlite
#> execute: function (sql)
#> get_connection: function ()
#> get_query: function (sql)
#> get_transaction_state: function ()
#> initialize: function (..., conn_args = list(), use_pool = FALSE, persist = FALSE)
#> list_tables: function ()
#> model: function (tablename, ..., .data = list())
#> persist: TRUE
#> set_transaction_state: function (state)
#> use_pool: FALSE
#> Private:
#> detect_dialect: function ()
#> exit_check: function ()
#> in_transaction: FALSE
Common Methods
The full API is documented in the Engine reference page, but here are the most commonly used methods:
model()
Creates aTableModel
object from the Engine. See the TableModel vignetteget_connection()
Returns the underlying DBI connection. You can use this directly for raw SQL or withdplyr::tbl()
for custom queries.execute(sql)
A lightweight wrapper aroundDBI::dbExecute()
using the Engine’s connection.get_query(sql)
Executes a query and returns the results as a data.frame usingDBI::dbGetQuery()
.
# Run a raw SQL statement
engine$execute("CREATE TABLE things (id INTEGER PRIMARY KEY, name TEXT)")
# Retrieve a result
df <- engine$get_query("SELECT * FROM things")
Using with.Engine
with.Engine
is an S3 method for managing transaction
state. It allows you to run code within a database transaction, and if
an error occurs during execution, it will automatically roll back the
transaction.
This provides a safeguard against partial writes or inconsistent
states. If you’re familiar with Python’s sqlalchemy
, the
usage will feel familiar.
with(engine, {
user = Users$record(name = "John Doe")
user$create()
})
You can also choose to manually commit or roll back transactions. This gives you full control over error handling:
with(engine, {
user = Users$record(name = "Jane Doe")
tryCatch({
user$create()
}, error = function(e) {
print(paste("An error occurred:", e$message))
rollback()
})
commit()
}, auto_commit = FALSE)
This approach helps ensure that your data remains in sync with the database state. If any part of the transaction fails, with.Engine() will automatically clean up and alert you with a meaningful error.
Dialects
Engines also store the dialect of the database they connect to. This is used to translate SQL statements into the correct syntax for the specific database backend.
Different databases handle data types, operators, and functions in
slightly different ways. oRm
’s dialect system isn’t
exhaustive, but it covers key differences that affect features like
flushing or value-returning inserts.
For example: - PostgreSQL supports RETURNING *
to fetch
inserted rows immediately. - SQLite does not support
RETURNING
, so oRm
falls back to using
last_insert_rowid()
.
These dialect-specific behaviors are handled automatically, so your code can stay consistent regardless of the database in use.