在上一篇文章从服务端到客户端,一次Ktor的跨端实践中我们已经知道了如何去使用Ktor创建一个简单的服务端项目,开发接口,并在自己的demo中去调用接口去展示数据,但是美中不足的是所使用的数据是我们自己造的假数据,真正的研发过程中,服务端往往是会通过数据库与数据打交道,所以今天我们就来一步步学习下如何在一个ktor的服务端项目中植入一个数据库,并实现基本的增删改查操作,以及优化下上一篇文章中做的客户端demo,让它这次可以跟服务端有个数据的交互
第一步:引入依赖
首先在gradle.properties
文件中加入两个版本号,分别是Exposed和H2两个库
然后在build.gradle.kts
文件中添加Exposed与H2两个库的依赖
第二步:创建数据模型与数据库表
新建一个PhotoTable.kt
文件,在里面加入数据类Photo
,另外创建一个object
类Photos
作为我们的数据表,Photos
继承自Exposed
库里面的Table
类
在表中id
作为主键自增长,title
保存着图片的名字,imgUrl
保存着图片的链接
第三步:连接数据库
连接数据库需要用到Exposed
库里面的Database
类,它会调用connect
函数来连接数据库,connect
函数里面需要传入两个参数,一个是jdbc链接,另一个是驱动名字,所以首先创建一个object类DatabaseFactory
,建立一个init
函数,在里面去连接数据库
注意这里的driverClassName
与jdbcURL
是写死的两个值,除非是自己去自定义数据库,建立好连接以后,在connect
函数底下调用transaction
函数,在函数体内部我们就可以去创建表,创建的方式是调用SchemaUtils.create
函数,里面传入我们之前声明好的Photos
类,这样表就创建好了,考虑到每次访问数据库都需要开启一个事务,我们在DatabaseFactory
中新增一个挂起函数dbQuery
,这个函数接受一个挂起的函数类型参数,在这个函数类型参数里面我们去执行每一次对数据库的操作
最后在程序启动的时候,在Application.module
函数中调用一下Databasefactory.init
函数,我们数据库就完成了所有初始化的工作
第四步:访问数据
完成了对数据库的初始化工作以后,就该对里面的数据进行操作了,对数据库最基本的操作无非就是增删改查,所以接下来我们也添加上增删改查的api,第一步先创建个interface,命名为PhotoDao.kt
,在里面定义我们需要的操作,有这么几个
既然都是数据库操作,所以它们都属于比较耗时的,所以将它们都定义为挂起函数,然后就是去实现这些接口了,这里有个快捷方式可以一键生成impl类,那就是点击左上角的那个灯泡,在下来菜单中点击implement interface
接下去就会询问你是否创建PhotoDaoImpl.kt
类,一直点ok就可以了,最终得到了我们的实现类
现在就可以在实现类里面写数据库操作的代码了,不过在这之前我们需要增加一个函数,这个函数用来将从数据库里获得的对象也就是ResultRow
,转换成我们需要用的数据类也就是Photo
查询所有数据
函数allPhotos
的功能就是将表中的所有数据都读出来,使用selectAll
函数来实现,然后使用map
操作符将数据通过resultRowToPhoto
函数转换成List<Photo>
查询单条数据
查询单个数据使用select
函数进行,与selectAll
不同的是,select
函数接受一个SqlExpressionBuilder
为接收者的lambda
表达式,我们在表达式里面添加查询的条件,比如我们这里的条件就是与传入的id
相同的数据,那么就可以这样写
eq
就表示相等的意思,除此之外还可以使用其他操作符比如less
, greater
, lessEq
, greaterEq
等来表示不同的条件,singleOrNull
表示如果返回结果数组大小为1,那么直接返回,否就返回空
新增数据
需要插入数据的时候我们使用insert
函数,insert
也接受一个lambda
表达式,表达式里面有个InsertStatement
对象,我们调用这个对象的set
操作符将对应字段插入到对应Column
里面去
更新数据
更新一条数据使用update函数,该函数接收三个参数,第一个where是一个SqlExpressionBuilder
为接收者的lambda
表达式,用来填写条件来查找需要更新的数据,第二个参数是limit
,表示最大可更新的数据量,第三个参数是body
,是调用UpdateStatement
的set
操作符来执行更新数据的操作,update
函数会返回一个int
结果,当结果大于0的时候表示更新成功
删除数据
删除数据使用的是deleteWhere
函数,这个函数接收三个参数,第一个参数为limit
,表示删除的最大数量,第二个参数为offset
,表示删除的偏移量,第三个参数删除的条件语句,也是一个SqlExpressionBuilder
为接收者的lambda
表达式,最终是否删除成功也是根据deleteWhere
的返回值是否大于0来判断
就这样增删改查的所有操作的写完了,最后在PhotoDaoImpl.kt
文件里面添加一个PhotoDao
的顶层属性,用来提供给外界来访问对数据库进行操作
第五步:实战
展示数据
数据库部分写完了,我们现在可以来实际演练一下,先更改一下上一篇文章中写好的queryPhotos
接口,原本这个接口返回的是造出来的假数据,现在我们可以让这个接口直接去数据库里面查询,将所有数据都读出来,代码更改如下
现在如果重新运行一下之前写好的图片列表demo,访问的接口就是从我们的数据库里面拿数据了,不过在运行之前我们先给数据库里面默认添加一条,因为现在整个表没数据,就算运行了客户端代码,页面也是空的
当创建PhotoDao
的时候,就判断如果表中没有数据,就默认添加一条进去,现在我们运行一下服务端项目,然后打开我们的客户端,看看效果
我们看到界面上展示的数据与ktor打印出来的日志相吻合,说明我们已经将数据库中的数据展示出来了。
添加数据
现在让我们扩展一下,增加一个添加图片的功能,首先服务端那边需要添加一个addPhoto
的接口,当客户端调用addPhoto
接口的时候,服务端获取到请求,判断传过来的数据是否不为空,如果不是空的话就往数据库中插入一条数据,所以首先我们在服务端项目中打开Routing.kt
文件,在原有的queryPhotos
接口下面添加上下面这段代码
一般这种添加数据的操作都是post
的请求,所以这里先调用了post
函数,里面传入我们接口的路径addPhoto
,在函数体里面我们调用call.receive
函数用来接受客户端传过来的参数body,AddPhotoRequest
是接收过来的参数的数据模型
当接口从参数中获取title
字符串后,判断是否为空,如果不为空就插入到数据库,图片链接我们就随机获取的一个链接,最终再将数据库中的所有数据再返还给客户端,在客户端项目中,我们也添加上addPhoto
接口的post请求,代码如下
AddImageRequest
为客户端调用addPhoto
接口的请求体,接口加好之后,我们在界面上做一下修改,由于是要添加图片,所以要有一个编辑框去编辑图片的title
,然后还需要一个按钮去触发addPhoto
接口,更改后的界面代码如下所示
然后在Button
的onClick
事件中调用添加图片的接口
当接口成功返回数据之后,我们将编辑框的内容清空,然后把返回的新的图片数据赋值给feedList
,界面就刷新了,分别运行下服务端与客户端项目,看看效果
我们看到当点击添加按钮之后,控制台那边返回了两条数据,我们界面上也对应着展示出了两条数据,说明我们这个添加图片的功能完成了
删除数据
最后我们再来一个删除数据的操作,删除数据就是把id传给服务端,服务端接收到id之后,再用这个id去数据库中执行删除操作,删除成功后再返回一个状态给客户端,客户端刷新列表,首先我们在服务端这里新增一个接收删除数据传参的数据类DeletePhotoRequest.kt
然后在Routing.kt
文件里面新增一个接口叫deletePhoto
,代码如下
当删除数据成功之后,服务端返给客户端一个status:ok
的json数据,如果删除失败,那么返给客户端一个status:fail
的json数据,然后运行一遍服务端代码重启下服务,我们回到客户端的代码中,首先在LazyColumn
的每个item
底下新增一个删除按钮
clickable
函数里面就是调用删除图片的接口,现在我们添加上调用接口的代码,在之前添加图片的接口底下新增deletePhoto
接口
DeleteImageResponse
和DeleteImageRequest
分别是调用deletePhoto
接口的返回数据和请求参数数据类
然后在删除按钮的clickable
函数中调用删除图片的接口
我们看到在接口请求成功的回调那里,我们将被删除的id赋值给了一个变量deleteId
,deleteId
是remember
出来的一个Int
变量,目的是当deleteId
值发生改变之后可以刷新列表数据,所以我们还需要将请求列表数据的接口包在LaunchedEffect
函数体内,deleteId
作为参数传给LaunchedEffect
,这样当deleteId
发生改变的时候就又会请求一遍列表数据
现在来跑一遍客户端代码看看效果
总结
头一次写demo有种既当爹又当妈的感觉,自己要啥接口自己写,写完接口自己调,虽说目前这些服务端代码写的还比较不规范,不能跟真正的大项目代码做比较,但是也是托Ktor的福,让我也接触到了一点服务端开发的皮毛,后面也会继续调研这方面的技术,有收获了就会拿出来分享给大家~