同步操作将从 wz/easysql-scala 强制同步,此操作会覆盖自 Fork 仓库以来所做的任何修改,且无法恢复!!!
确定后同步将在后台操作,完成时将刷新页面,请耐心等待。
easysql-scala是easysql项目 https://gitee.com/wangzhang/easysql 的Scala3衍生版本。
利用Scala3的dsl构造能力,提供了比前者更简洁的调用方式;
使用Scala3强大的类型系统提供严格的类型检查;
以及使用宏(macro)来提供更高的sql生成性能,和额外的编译期检查。
我们可以使用接近于原生sql的dsl构建跨数据库的sql语句,无需任何代码以外的配置,就能构造出复杂的查询,比如:
val select = (select (*)
from User
leftJoin Post on User.id === Post.uid
orderBy User.id.asc
limit 10 offset 10)
使用这种dsl,有下面的好处:
支持mysql、postgres sql、oracle、sqlserver、sqlite在内的多种数据库,并且封装出了统一的api。(mysql与pgsql为第一优先级支持)
此版本没有考虑与Java的兼容性,如果需要使用Java调用,请使用Kotlin版。
我们编写一个object,继承TableSchema,例如:
object User extends TableSchema {
override val tableName: String = "user"
val id: TableColumnExpr[Int] = intColumn("id")
val name: TableColumnExpr[String] = varcharColumn("name")
}
在伴生对象中添加override val tableName: String,其值为表名。
给伴生对象的属性赋值成column()类型,在column()函数中添加数据库列名。
column类型支持:intColumn、longColumn、varcharColumn、floatColumn、doubleColumn、booleanColumn、dateColumn。
然后我们就可以使用select方法创建一个Select实例,并编写一个简单的查询:
val s = select (User.id) from User
上面代码中的User和User.id即是来自我们定义好的object,藉此我们可以获得类似原生sql的编写体验。
我们对上文的object加以改造,对主键字段填加.primaryKey调用(此时字段类型将不再是TableColumnExpr,而是PrimaryKeyColumnExpr)。
object User extends TableSchema {
val id: PrimaryKeyColumnExpr[Int] = intColumn("id").primaryKey
val name: TableColumnExpr[String] = varcharColumn("name")
}
如果是自增主键,使用.incr(一张表支持多个主键但只支持一个自增主键):
object User extends TableSchema {
val id: PrimaryKeyColumnExpr[Int] = intColumn("id").incr
val name: TableColumnExpr[String] = varcharColumn("name")
}
对可空类型的字段,我们添加.haveNull,并在类型参数中添加(| Null):
object User extends TableSchema {
val id: PrimaryKeyColumnExpr[Int] = intColumn("id").incr
val name: TableColumnExpr[String | Null] = varcharColumn("name").haveNull
}
后续的查询构造器中,将会利用object中的配置,进行类型检查。
接下来我们将开始了解查询构造器,为了更好地使用查询构造器,也为了使用者更深入的了解sql的本质,我们先来了解表达式和运算符:
我们首先来介绍库提供的表达式和各种运算符。
表达式拥有共同的父类Expr,而大多数表达式的参数中也可以接收Expr类型,因此,表达式之间可以互相嵌套,便可以提供高抽象能力。
表达式类型中如果代入进了字符串,会在生成sql时自动转义特殊符号,以此来防止sql注入。
字段是最基本的表达式,比如上文object中的属性User.id就是一个字段类型,可以代入到其他表达式或查询语句中:
select (User.id) from User
如果我们需要查询全部字段,那么可以像下面这样调用:
select (*) from User
这个表达式的定义如下:
def * = AllColumnExpr()
假如你的项目中,表名字段名是动态的,并不能构建出实体类,那可以使用另一种方式生成字段表达式:
col[Int]("c1")
其中的类型参数为字段的实际类型(也可以不传类型参数,但这会失去类型安全性,请使用者自行斟酌),可空字段可以写成:
col[Int | Null]("c1")
将这种字段代入查询:
select (col[Int]("c1")) from User
如果col()中的字符串包含.,那么.左侧会被当做表名,右侧会被当做字段名;如果包含*,那么会产生一个sql通配符。
表达式类型可以使用中缀函数as来起别名,我们在此以字段类型为例,后文的其他表达式类型也支持这个功能:
select(User.id as "c1").from(User)
带入进查询时,需要使用小括号包裹。
在某些需求中,可能会将某一种常量来作为查询结果集的一列,比如:
SELECT 1 AS c1
我们可以使用const()来生成常量类型的表达式:
select(const(1) as "c1")
当然,利用Scala强大的编译器,只要在使用时引入dsl.constToExpr这个隐式转换,此处就可以省略掉const():
import dsl.constToExpr
select(1 as "c1")
隐式转换后也可以在运算符左侧使用此值:
import dsl.constToExpr
select (*) from User where 1 < User.id
constToExpr虽然方便,但并非是类型安全的,如果需要严格的类型检查,我们可以根据需要导入intToExpr、stringToExpr等隐式转换。
内置了count、countDistinct、sum、avg、max、min这些标准的聚合函数,比如:
select(count(*) as "col1", sum(User.id) as "col2").from(User)
上文的*返回的也是一个Expr的子类,所以我们可以像上面那样来代入到count函数中。
库内置了==、===(由于==与内置库函数同名,所以使用==可能在某些情况下会产生预期之外的结果,所以更推荐使用===代替==)、<>、>、>=、<、<=、&&(AND)、||(OR)、^(XOR)等逻辑运算符,我们可放入where条件中:
select (*) from User where User.id === 1
因为Scala的强大,在一些复杂条件中,我们不需要像easysql的Kotlin版本那样小心翼翼。
因为Kotlin中这些运算符只是普通函数,并不能指定结合性,所以在较复杂的条件中,我们需要使用小括号来将每一组子条件括起来:
select().from(User).where((User.name eq "小黑") and ((User.id gt 1) or (User.gender eq 1)))
而Scala版就更简洁:
select (*) from User where User.name === "小黑" && (User.id > 1 || User.gender === 1)
如果使用AND来拼接条件,也可以使用多个where。
除了上文的逻辑运算符外,还支持in、notIn、like、notLike、isNull、isNotNull、between、notBetween:
select(*)
.from(User)
.where(User.gender in (1, 2))
.where(User.id between (1, 10))
.where(User.name.isNotNull)
.where(User.name like "%xxx%")
上文中object里未使用haveNull来标注的字段,使用isNull或isNotNull时,产生一个编译错误。
这些运算符不仅可以代入数值,字符串等常量,表达式类型的子类也可以代入其中,比如我们需要做一个需求,查询当前的时间在表的两个字段范围内的数据,我们可以这样写:
select(*).from(User).where(const(Date()).between(User.time1, User.time2))
这已经体现出运算符的抽象能力了,但是,我们还可以再简洁一些:
import dsl.dateToExpr
select (*) from User where Date().between(User.time1, User.time2)
库提供了+、-、*、/、%五个数学运算符,比如:
select(count(*) + 1).from(User)
使用caseWhen()方法和中缀函数thenIs与elseIs来生成一个case表达式:
val c = caseWhen(User.gender === 1 thenIs "男", User.gender === 2 thenIs "女") elseIs "其他"
select(c as "gender").from(User)
这会产生下面的查询:
SELECT CASE
WHEN user.gender = 1 THEN '男'
WHEN user.gender = 2 THEN '女'
ELSE '其他'
END AS gender
FROM user
case when表达式也可以传入聚合函数中:
val c = caseWhen(User.gender === 1 thenIs User.gender) elseIs null
val select = select(count(c) as "male_count").from(User)
这会产生下面的查询:
SELECT COUNT(CASE
WHEN user.gender = 1 THEN user.gender
ELSE NULL
END) AS male_count
FROM user
使用聚合函数或rank()、denseRank()、rowNumber()三个窗口专用函数,后面调用.over,来创建一个窗口函数,然后通过partitionBy和orderBy来构建一个窗口:
select(rank().over.partitionBy(User.id).orderBy(User.name.asc) as "over")
.from(User)
这会产生如下的查询:
SELECT RANK() OVER (PARTITION BY user.id ORDER BY user.user_name ASC) AS over FROM user
partitionBy()接收若干个表达式类型
orderBy()接收若干个排序列,在表达式类型之后调用.asc或.desc来生成排序规则。
窗口函数是一种高级查询方式,使用时需要注意数据库是否支持(比如mysql8.0以下版本不支持窗口函数功能)。
可以使用内置的NormalFunctionExpr来封装一个数据库函数。
函数不利于查询优化以及不同数据库转换,因此不推荐使用。
使用cast()方法生成一个cast表达式用于数据库类型转换。
第一个参数为表达式类型,为待转换的表达式;
第二个参数为String,为想转换的数据类型。
比如:
val select = select(cast(User.id, "CHAR")).from(User)
这会产生下面的查询:
SELECT CAST(user.id AS CHAR) FROM user
会影响查询效率,不推荐使用。
在介绍完表达式和运算符之后,我们便可以开始着重来讲sql的核心:select语句的构建。
库内置了一系列方法来支持SELECT语句的编写,比如:
val select = select (*) from User where User.name === "小黑"
然后我们就可以用sql方法,并传入数据库类型枚举DB,获取生成的sql:
val sql = select.sql(DB.MYSQL)
当然,生成sql语句的方法并非只有sql,后文会详细说明。
select()方法中支持传入若干个前文介绍的表达式类型:
select(User.id, User.name)
链式调用多个select,会在生成sql时依次拼接进sql语句。 由于需要对查询类型校验,每次调用select都会产生一个新的Select对象,如无必要(比如动态生成sql),请慎用多次select功能。
from()方法支持传入一个字符串表名,或者前文介绍的继承了TableSchema的对象名:
select(*).from(User)
select(*).from("table")
此功能使用Scala3的union type功能实现。
不支持from多张表,如果有此类需求,请使用join功能。
alias()方法给表起别名:
select(*).from(User).as("t1")
如果别名需要加入列名,alias()的表名参数后面继续添加列名即可(这种别名方式对后文介绍的values临时表非常有用):
select(*).from(User).as("table", "col1", "col2")
as调用之前,必须保证调用了from,否则运行时会抛出异常。
使用where()配合各种前面介绍的运算符和表达式,生成where条件:
select(*).from(User).where(User.id === 1).where(User.gender <> 1)
多个where()会使用AND来拼接条件,如果需要使用OR和XOR,请参考前文的运算符部分。
有些时候,我们需要根据一些条件动态拼接where条件,我们可以这样调用:
select(*).from(User).where(testCondition, User.name === "")
where()的第一个参数接收一个Boolean表达式,只有表达式返回的值是true的时候,条件才会被拼接到sql中。
如果判断条件比较复杂,第一个参数也可以传入一个返回Boolean类型的lambda表达式。
不止是select语句,后文的update和delete语句也支持这些where()的调用方式,以后便不再赘述。
使用orderBy()方法传入表达式类型的.asc或者.desc方法来生成的排序规则:
select (*) from User orderBy (User.id.asc, User.name.desc)
这可能还不够像sql风格,我们可以导入scala.language.postfixOps来去掉asc和desc之前的点:
import scala.language.postfixOps
select (*) from User orderBy (User.id asc, User.name desc)
使用groupBy()来聚合数据,having()来做聚合后的筛选:
select(User.gender, count(*)).from(User).groupBy(User.gender).having(count(*) > 1)
groupBy()接收若干个表达式类型,having()的使用方式与where()相似。
在调用链中添加distinct即可,会对查出的列进行去重。
select(User.name).from(User).distinct
使用limit(count, offset)来做数据条数筛选(注意此处与mysql的参数顺序不一样),如:
select(*).from(User).limit(1, 100)
limit中第二个参数也可以不填,即为默认值0:
select(*).from(User).limit(1)
我们也可以使用中缀函数limit和offset组合调用
select (*) from User limit 1 offset 10
也可以不调用offset函数,即为默认值0。
limit语句并不是sql标准用法,因此每个数据库厂商采用的语法都有差异,生成sql时会根据数据源的数据库类型进行方言适配。
oracle需要版本在12c以上,sqlserver需要版本在2012以上。低于此版本,需要使用者自行处理ROW NUMBER。
提供:join()、innerJoin()、leftJoin()、rightJoin()、crossJoin()、fullJoin()几种不同的join方法。
上述方法配合on()方法来做表连接:
select(*).from(User).leftJoin(Post).on(User.id === Post.uid)
对于表的as()方法,会给最近的一个表起别名。
如果需要使用子查询,我们另外声明一个Select对象传入调用链即可:
select(*).from(Select().from(User)).as("t")
join中的子查询:
select(*).from("t1")
.leftJoin(Select().from("t2").limit(10))
.as("t2")
.on(col("t1.id") === col("t2.id"))
操作符中的子查询:
select(*).from(User)
.where(User.id in Select().select(User.id).from(User).limit(10))
支持EXISTS、NOT EXISTS、ANY、ALL、SOME这五个子查询谓词,使用对应的全局函数把查询调用链代入即可:
select(*).from(User)
.where(exists(Select().select(max(User.id)).from(User)))
当然子查询谓词依然是表达式类型,所以可以使用操作符函数来计算:
select(*).from(User)
.where(User.id < any(Select().select(max(User.id)).from(User)))
如果需要使用LATERAL子查询,把from()改为fromLateral()即可(join的调用方式类似,需要注意使用的数据库版本是否支持LATERAL关键字):
select(*).fromLateral(Select().from(User)).as("t")
使用forUpdate方法将查询加锁:
select(*).from(User).forUpdate
不支持sqlite;在sqlserver中会在FROM子句后生成WITH (UPDLOCK);其他数据库会在sql语句末尾生成FOR UPDATE。
前面通过链式调用构建的查询,其实只是构建出sql语法树,还并未生成sql,我们还需要一个链式调用终止操作,来生成需要的sql语句:
select.sql(DB) // 直接生成sql
select.fetchCountSql(DB) // 生成查询count的sql(会复制查询副本并去掉limit和order by信息)
select.pageSql(10, 1)(DB) // 生成分页sql(会复制查询副本并根据参数替换掉limit信息)
除了普通的select语句之外,还支持union等一些特殊查询,这些查询只支持使用sql方法获取sql语句:
支持union、unionAll、except、interSect来将两个查询拼接在一起:
val s = (select (User.name) from User where User.id === 1) union (select (User.name) from User where User.id === 2)
如果两个查询select中的返回类型不一致,将无法通过编译。
生成一个with查询(mysql和pgsql使用递归查询在调用链添加.recursive)
import dsl.boolToExpr
val w = WithSelect()
.add("q1", List("name"), Select() select User.name from User where User.id === 1)
.add("q2", List("name"), Select() select User.name from User where User.id === 2)
.select { s =>
s from "q1" join "q2" on true
}
val v = ValuesSelect().addRow(1, "x").addRow(2, "y")
values临时表也可以被放入union中。
val i = insertInto(User)(User.id, User.name).values((1, "x"), (2, "y"))
val sql = i.sql(DB)
得益于Scala3强大的类型系统,values中如果类型与前面的字段定义不一致,将会产生编译错误。
我们也可以使用实体类来插入数据,实体类继承TableEntity, 并添加伴生对象:
case class User(id: Int = 123, name: Option[String] = Some("")) extends TableEntity[Int]
object User extends TableSchema {
override val tableName: String = "user"
val id: PrimaryKeyColumnExpr[Int] = intColumn("id").incr
val name: TableColumnExpr[String | Null] = varcharColumn("user_name").haveNull
}
TableEntity中的类型参数为主键类型,如果是联合主键,可以传入一个元组类型,比如:TableEntity[(Int, String)]
联合主键的表,请确保实体类、伴生对象、TableEntity的主键定义顺序一致。
然后使用insert方法:
val user = User(name = Some("x"))
val sql = insert(user).sql(DB)
使用incr标注的字段将会在语句生成时忽略。
val u = update (User) set (User.name to "x") where User.id === 1
to是对TableColumnExpr添加的扩展函数,其中添加了类型检查,所以如果更新成与字段不同的类型,将不会通过编译检查;
并且由于是对TableColumnExpr的扩展,所以如果更新主键,将会产生编译错误。
to的右边不止可以是值,也可以是其他表达式,所以我们可以实现这样的功能:
update (User) set (User.followers to User.followers + 1) where User.id === 1
我们也可以传入上文插入语句介绍中的实体类,用主键字段的值作为条件更新其他字段:
val user = User(1, Some("x"))
update(user)
我们这样创建一个删除语句:
val d = deleteFrom (User) where User.id === 1
我们也可以使用实体类的类型生成按主键删除的sql:
delete[User](1)
使用实体类生成按主键插入或更新的sql:
val user = User(1, Some("x"))
val s = save(user)
val sql = s.sql(DB)
每一种数据库生成的sql均不同。
上面我们了解了查询构造器,并知道了如何利用它来生成sql语句,但是这还不够。
实际项目中,我们需要连接到数据库,并修改数据库中的数据或是拿到查询结果。
目前,easysql添加了jdbc子项目,基于jdbc做了数据库交互的实现(以后会逐步添加其他的数据库驱动实现, 比如异步数据库连接)
下面我们以jdbc子项目为例,来介绍easysql与数据库交互的方式(以后其他的数据库驱动方式,由于跟jdbc共享同一个父接口,使用方式大同小异):
为了更好地使用jdbc,我们需要一个数据库连接池,此处以druid为例,使用者可以自行替换成实现了DataSource接口的连接池。
我们首先创建一个连接池,并交由JdbcConnection类管理:
// 此处省略连接池配置
val druid = new DruidDataSource()
// 此处第一个参数为连接池,第二个参数为数据库类型的枚举,用户可自行根据项目需要替换
val db = new JdbcConnection(druid, DB.MYSQL)
拿到了JdbcConnection的实例之后,我们可以在此之上操作数据库:
对于insert、update、delete等修改数据的sql,我们可以使用run方法执行,并返回Int类型的受影响行数:
val insert = insertInto(User)(User.id, User.name)((1, "x"), (2, "y"))
val result: Int = db.run(insert)
如果insert操作时,数据库中有自增主键,我们可以使用runAndReturnKey方法,返回一个List[Long]类型的结果集:
val insert = insertInto(User)(User.id, User.name)((1, "x"), (2, "y"))
val result: List[Long] = db.runAndReturnKey(insert)
对于select、values临时表等查询类sql,可以使用JdbcConnection类进行查询,并返回查询结果。
支持的单条结果映射类型有三种:
使用queryMap、queryTuple、queryEntity来查询结果集,返回结果是一个List,如果没有查询到结果,返回一个0元素的List:
val select = select (User.id, User.name) from User
val result1: List[Map[String, Any]] = db.queryMap(select)
val result2: List[(Int, String | Null)] = db.queryTuple(select)
val result3: List[User] = db.queryEntity[User](select)
使用findMap、findTuple、findEntity来查询单条结果,返回结果是一个Option,如果没有查询到结果,返回一个None:
val select = select (User.id, User.name) from User
val result1: Option[Map[String, Any]] = db.findMap(select)
val result2: Option[(Int, String | Null)] = db.findTuple(select)
val result3: Option[User] = db.findEntity[User](select)
使用pageMap、pageTuple、pageEntity来进行分页查询,返回结果是一个Page类型,其定义如下:
case class Page[T](totalPage: Int = 0, totalCount: Int = 0, data: List[T] = List())
分页查询参数中除了需要传入查询dsl之外,还需要依次传入一页的结果集条数,页数,和是否需要查询count;
其中最后一个参数,默认值为true,为true时会附带执行一个查询count的sql,如无必要,请传入false,以便提升效率:
val select = select (User.id, User.name) from User
val result1: Page[Map[String, Any]] = db.pageMap(select)(10, 1)
val result2: Page[(Int, String | Null)] = db.pageTuple(select)(10, 1, true)
val result3: Page[User] = db.pageEntity[User](select)(10, 1, false)
使用fetchCount方法来查询结果集大小,返回结果是Int类型。
此处会对生成的sql语法树进行复制,并去除对于查询count无用的order by和limit信息,并把select列表替换成COUNT(*),以提高查询效率:
val select = select (User.id, User.name) from User orderBy User.id.asc limit 10 offset 10
val count: Int = db.fetchCount(select)
此处实际生成的sql为:
SELECT COUNT(*) AS count FROM user
使用transaction函数来产生一个事务,该函数是一个高阶函数。
高阶函数中如果出现了异常,将会进行回滚,如无异常,将会提交事务,高阶函数结算后回收数据库连接:
db.transaction { t =>
t.run(...) // 高阶函数里可以执行一些查询
throw Exception() // 出现异常后会回滚事务
}
我们也可以手动传入java.sql.Connection中定义的隔离级别,比如:
db.transaction(TRANSACTION_READ_UNCOMMITTED, { t =>
t.run(...)
})
easysql的scala版本的诞生,需要感谢两个人:
jilen:https://github.com/jilen
scala最好的orm:quill的核心作者,在此感谢jilen指引我前进的方向。
氘発孜然:https://github.com/daofaziran1
在此感谢氘発孜然在scala3的macro等方面给予我的帮助。
此处可能存在不合适展示的内容,页面不予展示。您可通过相关编辑功能自查并修改。
如您确认内容无涉及 不当用语 / 纯广告导流 / 暴力 / 低俗色情 / 侵权 / 盗版 / 虚假 / 无价值内容或违法国家有关法律法规的内容,可点击提交进行申诉,我们将尽快为您处理。