[Day 05] Exposed 和资料库进行互动的方式之一:DSL

昨天我们成功和资料库进行了串接,但是我们只建立了一张资料表,还没有实际的处理资料内容。

今天我们尝试对资料库进行基本的 CRUD 操作,也就是常说的建立、读取、修改、删除四个操作。

在 Exposed 框架内,有两种操作的方式:DSL 和 DAO。

今天我们先来学习使用 DSL 的方式,和资料库进行互动。

什麽是 DSL

DSL(Domain Specific Language),简单的说,就是 Exposed 框架所设计的一系列函数,协助我们不需要直接撰写 SQL Query 就可以进行对资料库的存取。

这个做法类似於其他框架或语言的 Query Builder,如果读者已经有熟悉的资料库存取框架,可以拿 Exposed 框架的 DSL 和自己熟悉的做法比较看看有什麽异同之处。

新增和读取

调整资料前,我们先将 addLogger(StdOutSqlLogger) 移除,这样可以简化我们後面印出的资料内容。

要新增一笔资料,我们在建立资料表後面的程序,加上以下程序码

Cities.insert {  
    it[name] = "Taipei"  
}

就可以建立资料。

不过单纯建立资料,不读取看看的话,我们也不知道资料是否成功建立。

我们可以用另一种写法,来取得我们所建立资料的 ID

val id = Cities.insertAndGetId {  
    it[name] = "Tainan"  
}

这样我们就可以用取得的 id,从资料库取出我们之前写入的资料。

透过 id 取出资料的方式如下

Cities  
    .select { Cities.id eq id }  
    .forEach {  
        println("City #$id: ${it[Cities.name]}")  
    }

整个 transaction 看起来像是这样

transaction {  
    SchemaUtils.create(Cities)  
    Cities.insert {  
        it[name] = "Taipei"  
 	}  
 	val id = Cities.insertAndGetId {  
 		it[name] = "Tainan"  
 	}  

 	Cities  
        .select { Cities.id eq id }  
 		.forEach {  
 			println("City #$id: ${it[Cities.name]}")  
        }
}

运作程序後,我们应该可以看到

City #2: Tainan

如果要印出所有的 City,我们可以用 selectAll() 这个函数来调整一下程序

transaction {  
 	SchemaUtils.create(Cities)  
    Cities.insert {  
 		it[name] = "Taipei"  
	}  
 	val id = Cities.insertAndGetId {  
 		it[name] = "Tainan"  
 	}  
  
 	Cities
        .selectAll()  
        .forEach {  
 			println("City #${it[Cities.id]}: ${it[Cities.name]}")  
		}  
}

到这边,我们就可以做到新增和读取资料了!

修改

要修改资料的话,我们可以这样做

Cities.update({ Cities.id eq id }) {  
    it[name] = "Kaohsiung"  
}

如果你不希望用 id 找出要修改的资料,也可以使用其他条件

Cities.update({ Cities.name eq "Tainan" }) {  
    it[name] = "Kaohsiung"  
}

加上这段逻辑後,如果我们尝试印出原本的资料,就会得到

City #2: Kaohsiung

到这边,我们就可以做到修改资料了!

删除

要删除资料,我们可以透过 deleteWhere() 函数来移除资料

Cities.deleteWhere { Cities.id eq id }

资料移除之後,无法读取内容,那我们怎麽知道我们成功删除了呢?

我们可以透过 count() 函数来取得资料笔数,如果资料笔数为零,我们就知道原先的资料已经不存在,资料删除成功了

println(  
    Cities  
        .select { Cities.id eq id }  
 		.count()  
)

成功的话就会印出 0,证明我们的资料删除成功。

到这边,资料的 CRUD 就全部介绍完毕了。

能够做到资料的 CRUD 之後,下面我们来说明一下前面的程序码。

什麽是 it

除了前面介绍过的 Passing trailing lambdas,在一开始,我们最先会注意到的,应该就是写入资料时

Cities.insert {
    it[name] = "Taipei"  
}

的这个 it 了。

根据 kotlin 的官方文件Context object: this or it,里面提到 it 是 Lambda 函数内参数(Argument)的预设值。

也就是说,我们在 insert() 函数里面指定要操作的资料时,我们没有特别写一个变数名称,来指定我们要写入的资料内容。

取而代之,我们的做法是透过 Kotlin 提供的预设名称来撰写我们需要的内容。直接说 it[name] = "Taipei" 就知道要写入的资料是什麽样子了。

类似的逻辑,也出现在要印出所有资料时,我们透过 forEach() 的操作上

Cities  
    .selectAll()  
    .forEach {  
 		println("All Cities: ${it[Cities.name]}")  
    }

只要在 forEach() 内写 it,kotlin 就知道指的是我们所取出的单笔资料,并且可以针对单笔资料进行操作。

这样的设计方式,是不是很直观呢?

什麽是 eq

我们会特别注意到的部分,还有取得资料时

Cities  
	.select { Cities.id eq id }  
	.forEach {  
		println("City #$id: ${it[Cities.name]}")  
	}

所使用的 eq 了,这是什麽做法呢?

在 kotlin 内,除了定义前缀(prefix)与後缀(suffix)之外,还可以特别定义所谓的中缀(infix)函数。这个函数可以从前後接受参数并进行运算,让我们的语法看起来更加直观。

在框架的程序码里面,是这麽定义 eq 这个函数的

  
/** Checks if this expression is equals to some [t] value. */
infix fun <T> ExpressionWithColumnType<T>.eq(t: T): Op<Boolean> = if (t == null) isNull() else EqOp(this, wrap(t))

利用 infix 的特性,我们就可以不用类似 eq(Cities.id, id) 的做法,直接写 Cities.id eq id,让语法更加简洁并且直观。

字串样板 "${}"

再来,我们注意到的,应该就是印出资料时

println("City #$id: ${it[Cities.name]}")

所使用的 $id${it[Cities.name]} 了。

在 kotlin 内,如果我们需要印出某个变数,我们可以透过在印出的字串内加上 $ 符号,来加入该变数内容。kotlin 会自动将

这个方式,在 kotlin 内,称为 string templates

如果我们要印出的内容比较复杂,需要透过表达式(expression)来进行呈现,我们可以用 ${} 包装整个表达式,就像是范例里面的 ${it[Cities.name]} 这样。

到这边,希望所有的范例都能让读者们操作成功并理解了,我们明天见!


<<:  从 IT 技术面细说 Search Console 的 27 组数字 KPI (10) :连结 - 内部连结

>>:  参与"在MCU 上全面建构AI能力" 9/10 心得

Day 07:我今天想不到标题之整合 tmux 和 zsh

我把从第一天到现在每天的 Home 目录都放上 GitHub 了,README.md 里面有说明 ...

拿 ml5 来练习 如何载入别人的情绪给自己的电脑 (四)

介绍 首先介绍什麽是载入别人的情绪给自己的电脑, 有个人已经将自己读文章时,所产生的情绪,让机器来学...

区块链与物联网的两人三脚

我抠 我抠 我抠抠抠,"钱歹赚"的2022,我的每一分钱都打上9个结,为了生存时...

Day_01 Openwrt intro

你曾经被有家里的网路设备介面与功能雷到吗?你讨厌路由器原生不亲民的页面吗?你想要更多功能可是预算却有...

python 中 pickle 读档问题的解决方法

前言 要读取 .pkl 档,结果遇到各种状况,在网路上查了许多资料後终於解决了! 在这边简单做个过程...