1 Star 0 Fork 0

氘発孜然 / easysql-scala

加入 Gitee
与超过 1200万 开发者一起发现、参与优秀开源项目,私有仓库也完全免费 :)
免费加入
克隆/下载
README.md 24.34 KB
一键复制 编辑 原始数据 按行查看 历史
wz 提交于 2022-04-06 06:08 . update README.md.

项目介绍

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,有下面的好处:

  1. 类型安全:将表达式与错误类型的值或者表达式比较时,将会编译失败;
  2. 防注入:dsl的背后是sql语法树,并非是单纯字符串拼接,静态的语法树类型可以防止绝大多数sql注入;
  3. 查询的任何部分都可以被封装到方法或变量中,我们可以用来动态构建sql;
  4. 获得ide提示。

支持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)

case表达式

使用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()方法生成一个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()方法中支持传入若干个前文介绍的表达式类型:

select(User.id, User.name)

链式调用多个select,会在生成sql时依次拼接进sql语句。 由于需要对查询类型校验,每次调用select都会产生一个新的Select对象,如无必要(比如动态生成sql),请慎用多次select功能。

from子句

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()配合各种前面介绍的运算符和表达式,生成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()的调用方式,以后便不再赘述。

order by子句

使用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)

group by和having子句

使用groupBy()来聚合数据,having()来做聚合后的筛选:

select(User.gender, count(*)).from(User).groupBy(User.gender).having(count(*) > 1)

groupBy()接收若干个表达式类型,having()的使用方式与where()相似。

distinct子句

在调用链中添加distinct即可,会对查出的列进行去重。

select(User.name).from(User).distinct

limit子句

使用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子句

提供: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")

for update

使用forUpdate方法将查询加锁:

select(*).from(User).forUpdate

不支持sqlite;在sqlserver中会在FROM子句后生成WITH (UPDLOCK);其他数据库会在sql语句末尾生成FOR UPDATE。

获取sql

前面通过链式调用构建的查询,其实只是构建出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查询

支持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查询

生成一个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
    }

values临时表

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的实例之后,我们可以在此之上操作数据库:

执行sql

对于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类进行查询,并返回查询结果。

支持的单条结果映射类型有三种:

  1. 映射到继承了TableEntity的实体类(可空字段将会被映射到Option类型);
  2. 映射到一个Tuple,Tuple的实际类型取决于select方法的参数(此时不能使用select *或者无类型参数的col,否则会出现运行时异常);
  3. 映射到一个Map[String, Any],map的key为字段名(或查询中的别名),value为查询结果的值。

查询结果集

使用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)

查询count

使用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等方面给予我的帮助。

Scala
1
https://gitee.com/daofaziran01/easysql-scala.git
git@gitee.com:daofaziran01/easysql-scala.git
daofaziran01
easysql-scala
easysql-scala
master

搜索帮助