# 数据库交互

上面我们了解了查询构造器,并知道了如何利用它来生成sql语句。

但实际项目中,我们通常需要连接到数据库,并修改数据库中的数据或是获取到查询结果。

目前,easysql添加了jdbc子项目,基于jdbc做了数据库交互的实现(以后会逐步添加其他的数据库驱动实现)

下面我们以jdbc子项目为例,来介绍easysql与数据库交互的方式:

首先,我们需要一个数据库连接池,使用者可以自行替换成实现了DataSource接口的连接池,如Hikari、Druid、C3P0等。

import easysql.database.*

// 此处省略连接池配置
val dataSource: DataSource = ???

// 第一个参数为数据库类型的枚举,第二个参数为连接池,用户可自行根据项目需要替换
// 比如MySQL和使用了MySQL方言的数据库如Doris等,此处便可以使用DB.MYSQL方言
val db = new JdbcConnection(DB.MYSQL, dataSource)

然后我们需要配置日志管理,任意String => Unit都可以作为日志的接收器,此处我们使用given来创建(以java内置的Logger,以及println为例):

import easysql.database.*

// java自带Logger
val logger = java.util.logging.Logger.getLogger("")
given Logger = logger.info

// println
given Logger = println

配置好之后,我们可以在此之上操作数据库:

# 执行sql

对于insert、update、delete等修改数据的sql,我们可以使用run方法执行,并返回Int类型的受影响行数:

import easysql.database.*
import easysql.dsl.*

val users = List(User(1, Some("x")), User(2, Some("y")))
val insert = insert(users)
val result: Int = db.run(insert)

如果insert操作时,数据库中有自增主键,我们可以使用runAndReturnKey方法,返回一个List[Long]类型的结果集:

import easysql.database.*
import easysql.dsl.*

val users = List(User(1, Some("x")), User(2, Some("y")))
val insert = insert(users)
val result: List[Long] = db.runAndReturnKey(insert)

# 查询结果集

使用query来将查询结果映射到List:

import easysql.database.*
import easysql.dsl.*

val s1 = from(user)
val result1: List[Option[User]] = db.query(s1)

val s2 = select (user, post) from user join post on user.id === post.userId

val result2: List[(Option[User], Option[Post])] = db.query(s2)

查询结果会根据select的参数自动推断,不需要手动指定。

单表或单列的查询,如果返回结果是List[Option[T]],是因为有些时候,空行是有业务含义的,如果你的应用场景不需要这个额外的Option,那么可以用querySkipNoneRows来简化返回结果类型:

import easysql.database.*
import easysql.dsl.*

val s = from(user)
val result: List[User] = db.querySkipNoneRows(s)

# 查询单条结果

使用find来将符合条件的第一条结果映射到Option:

import easysql.database.*
import easysql.dsl.*

val s = select (user) from user

val result: Option[Option[User]] = db.find(s)

# 分页查询

使用page,返回结果是一个Page类型,其定义如下:

case class Page[T](totalPage: Long, totalCount: Long, currentPage: Long, pageSize: Long, data: List[T])

分页查询参数中除了需要传入查询dsl之外,还需要依次传入一页的结果集条数,页数,和是否需要查询count;

其中最后一个参数,默认值为true,为true时会附带执行一个查询count的sql,如无必要,请传入false,以便提升效率:

import easysql.database.*
import easysql.dsl.*

val s = select (user) from user

val result: Page[Option[User]] = db.page(s)(10, 1, false)

# 查询count

使用fetchCount方法来查询结果集大小,返回结果是Long类型。

此处会对生成的sql语法树进行复制,并去除对于查询count无用的order by和limit信息,并把select列表替换成COUNT(*),以提高查询效率,适用于查询中没有group by的情况

import easysql.database.*
import easysql.dsl.*

val s = select (user.id, user.name) from user orderBy user.id.asc limit 10 offset 10

val count: Long = db.fetchCount(s)

此处实际生成的sql为:

SELECT COUNT(*) AS count FROM user

# 数据库事务

使用transaction函数来产生一个事务,transaction是一个高阶函数。

高阶函数中如果出现了异常,将会进行回滚,如无异常,将会提交事务,函数结算后回收数据库连接,并返回函数中的返回值,我们也可以通过try catch处理异常:

import easysql.database.*

val result: Int = try {
    db.transaction {
        run(...) // 高阶函数里可以执行一些查询
    }
} catch case e: Exception => -1

我们也可以手动传入java.sql.Connection中定义的隔离级别,比如:

import easysql.database.*

db.transactionIsolation(TRANSACTION_READ_UNCOMMITTED) {
    run(...)
}

在很多应用中,为了方便处理,我们可能需要事务的上下文在不同的方法中传播,这种情况也非常容易,只需要在方法中使用using添加一个隐式参数即可:

import easysql.database.*
import easysql.dsl.*

def add(user: User)(using JdbcTransaction) = runAndReturnKey(insert(user)).head.toInt

def remove(id: Int)(using JdbcTransaction) = run(delete[User](id))

def addAndRemove(user: User)(using JdbcTransaction) = {
    val id = add(user)
    remove(id)
}

val user = User(1, Some("x"))

val result: Int = try {
    db.transaction {
         addAndRemove(user)  
    }
} catch case e: Exception => -1