Interact with Database
To interact with the database, after configuring the metadata information of the entity classes, we also need to configure the database connection information. Currently, sqala supports creating a JDBC context to manage connections.
We construct a query context as follows:
import sqala.jdbc.*
import javax.sql.DataSource
// Any connection pool, such as Hikari, Druid, DBCP, C3P0, etc.
val dataSource: DataSource = ???
// The second parameter is the database dialect
// Supported dialects include:
// MysqlDialect
// PostgresqlDialect
// SqliteDialect
// OracleDialect
// MssqlDialect
// DB2Dialect
// H2Dialect
val db = JdbcContext(dataSource, MysqlDialect)
Then we need to configure a log handler to print the corresponding SQL statements when executing queries. Any function of type String => Unit
can be used as a log handler. Here, we use the mainstream logging library slf4j
as an example. In actual use, it can be replaced with various logging frameworks:
import sqala.jdbc.*
class Service:
val logger = LoggerFactory.getLogger(Service.class)
given Logger = Logger((s: String) => logger.info(s))
If logging is not needed, simply write:
import sqala.jdbc.*
class Service:
given Logger = Logger(_ => ())
After configuring the connection information, you can connect to the database and execute queries.
We do not need to manually write deserialization code for entity classes. If you encounter compilation errors, try using CustomField
.
Query Data
Use the fetch
method to query data, which returns a List:
val q =
from[Department].filter(_.id > 1)
val result: List[Department] = db.fetch(q)
We can use the fetchTo
method to map the results to types not automatically derived by sqala, but this may result in runtime type conversion errors:
val result: List[SomeEntity] = db.fetchTo[SomeEntity](q)
Query First Row
The find
method returns an Option
type result, which is the first row in the query result set:
val q = query:
from[Department].filter(_.id > 1)
val result: Option[Department] = db.find(q)
The findTo
method is similar to fetchTo
:
val result: Option[SomeEntity] = db.findTo[SomeEntity](q)
Query Count
Querying the count of data is a common operation. We can query count via the fetchSize
method:
val q = query:
from[Department].filter(_.id > 1)
val result: Long = db.fetchSize(q)
For better performance, when calling fetchSize
, sqala will optimize the incoming query when possible:
1. First, remove `ORDER BY` and `LIMIT` clauses.
2. If query contains `GROUP BY` or `SELECT DISTINCT`, make the query a subquery of a `COUNT(*)` query.
3. Otherwise, replace fields in `SELECT` with `COUNT(*)`
Query Existence
Use fetchExists
to query existence:
val q = query:
from[Department].filter(_.id > 1)
val result: Boolean = db.fetchExists(q)
Paging
Use the page
method for pagination query, which returns a Page type result. The Page type is defined as follows:
case class Page[T](
pageTotal: Int, // Total pages
querySize: Long, // Query COUNT(*) returned count
pageNo: Int, // Current page number
pageSize: Int, // Page size
data: List[T] // Paginated data
)
The parameters of page
are: query statement, page size, page number, and whether to query count (default is true):
val q = query:
from[Department]
val result: Page[Department] = db.page(q, 10, 1)
The last parameter controls whether to query the count. If the count is queried in every pagination query, it may waste database resources. For example, in actual business scenarios, we can query the count only on the first page and return 0 in other cases:
val q =
from[Department]
val pageSize = 10
val pageNo = 1
val result: Page[Department] = db.page(q, pageSize, pageNo, pageNo == 1)
Execute Statement
For non-query statements, use the execute
method to execute the statement, which returns an Int type result, representing the number of affected rows:
val result: Int = db.execute(insert[Department](d => (d.managerId, d.name)) values (1, "IT"))
Insert Data Using Objects
The insert
method can use entity objects to generate insert statements and execute them, returning the number of affected rows (fields marked with @autoInc
annotation will be skipped):
val department = Department(0, 1, "IT")
val result: Int = db.insert(department)
The insertAndReturn
method can use entity objects to generate insert statements, execute them, and return a new entity object with non-auto-increment primary key field values and database-generated auto-increment primary key bound. Since fields marked with @autoInc
will be skipped, the auto-increment primary key field of the inserted entity class can be filled with any irrelevant value:
val department = Department(0, 1, "IT")
val inserted = db.insertAndReturn(department)
The insertBatch
method is used for batch insertion, with the parameter being a List
of entity objects. After insertion, it returns the number of affected rows:
val departments = Department(0, 1, "IT") :: Department(0, 2, "Legal") :: Nil
val result: Int = db.insertBatch(departments)
Update Data Using Objects
The update
method can use entity objects to generate an update statement that updates other fields based on the primary key field, and execute it, returning the number of affected rows:
val department = Department(1, 10, "IT")
val result: Int = db.update(department)
If you want to skip updating fields with None
value, you can pass skipNone = true
:
val department = Department(1, 10, "IT")
val result: Int = db.update(department, skipNone = true)
To avoid generating incorrect inserts, if all non-primary key field values are None
, this update request will not be sent to the database.
The save
method can use entity objects to generate insert or update statements based on whether the primary key exists:
val department = Department(1, 10, "IT")
val result: Int = db.save(department)
The dialects generated by each database may vary.
Cursor Query
In scenarios where large amounts of data need to be processed (such as exporting data to files), if we load all data into memory at once, it may cause excessive memory usage. If pagination queries are used, it may lead to inefficiency. Therefore, sqala supports JDBC cursor queries. Use cursorFetch
to enable cursor queries:
val q = query:
from[Department]
db.cursorFetch(q, 100): c =>
// Operations on data
...
The first parameter of cursorFetch
is the query statement, and the second parameter is the number of entries fetched in each batch. You can choose an appropriate size based on the actual situation.
Then pass in a function that operates on each batch of data, with the type Cursor[T] => R
.
The Cursor
type is defined as:
case class Cursor[T](
batchNo: Int, // Batch number
batchSize: Int, // Batch size, i.e., the second parameter of cursorFetch
data: List[T] // Data in a batch
)
Return SQL
Use the sql
method to return the generated SQL:
val q = query:
from[Department].filter(_.id > 1)
val (sql, args) = q.sql(MysqlDialect)
Transaction
Use executeTransaction
method to create an transaction to execute. executeTransaction
is a method carrying context. If an exception occurs inside, the transaction will be rolled back and the exception will be thrown. If no exception occurs, the return value inside will be returned:
try {
val result = db.transaction {
execute(...)
execute(...)
}
println(result)
} catch {
case e: Exception => println("Query error")
}
A very important point is: do not use transaction.
to explicitly specify the database connection context for query methods executed inside executeTransaction
.
Using Scala3's context abstraction mechanism, we can conveniently propagate the transaction context between different methods, and this operation is type-safe:
import sqala.jdbc.*
def insertDepartment(row: Department)(using JdbcTransactionContext): Int =
transaction.executeReturnKey(insert(row)).head.toInt
def deleteDepartment(id: Int)(using JdbcTransactionContext): Int =
transaction.execute(delete[Department].where(d => d.id == id))
def insertAndDelete(row: Department)(using JdbcTransactionContext): Int =
val id = insertDepartment(row)
deleteDepartment(id)
val department: Department = ???
try {
db.executeTransaction {
insertAndDelete(department)
}
} catch {
case e: Exception =>
}
By using using JdbcTransactionContext
, a transaction context will be added to functions that need to execute transactions. If called outside the executeTransaction
method, a compilation error will occur. Methods marked with using JdbcTransactionContext
can share the same transaction within transaction
.
Additionally, the transactionWithIsolation
method can be used to specify the transaction isolation level.
try {
val result = db.executeTransactionWithIsolation(TransactionIsolation.ReadUncommitted) {
transaction.execute(...)
transaction.execute(...)
}
println(result)
} catch {
case e: Exception => println("Query Error")
}
TransactionIsolation
is an Enum, values contains: None
、ReadUncommitted
、ReadCommitted
、RepeatableRead
和Serializable
。