前面我们介绍了透过 DAO 取出资料的许多方式,包含了一对多关联,多对多关联,甚至包含到 Parent-Child reference 的做法。
今天我们来介绍使用 DAO 有时会遇到的 N+1 问题,以及在 Exposed 框架下的解决方式。
比方说我们有一个一对多关联如下
object Cities : IntIdTable() {
val name = varchar("name", 50)
}
class City(id: EntityID<Int>) : IntEntity(id) {
companion object : IntEntityClass<City>(Cities)
var name by Cities.name
val users by User referrersOn Users.city
}
object Users : IntIdTable() {
val city = reference("city", Cities)
val name = varchar("name", 50)
}
class User(id: EntityID<Int>) : IntEntity(id) {
companion object : IntEntityClass<User>(Users)
var city by City referencedOn Users.city
var name by Users.name
}
然後我们写入资料如下
SchemaUtils.create(Users)
SchemaUtils.create(Cities)
val paris = City.new {
name = "Paris"
}
val moscow = City.new {
name = "Moscow"
}
val helsinki = City.new {
name = "Helsinki"
}
val taipei = City.new {
name = "Taipei"
}
val alice = User.new {
name = "Alice"
city = paris
}
val bob = User.new {
name = "Bob"
city = paris
}
val carol = User.new {
name = "Carol"
city = moscow
}
这时候,我们想要拿出所有 city
对应的所有 user
时,
我们可以这样写
City
.all()
.forEach {
it
.users
.forEach{
println(it.name)
}
}
这样写逻辑没有问题,是可以印出我们想要的资料的
Alice
Bob
Carol
不过,如果我们透过 StdOutSqlLogger
看看底层的 query 长怎样
addLogger(StdOutSqlLogger)
City
.all()
.forEach {
it
.users
.forEach{
println(it.name)
}
}
我们会看到
SQL: SELECT CITIES.ID, CITIES."NAME" FROM CITIES
SQL: SELECT USERS.ID, USERS.CITY, USERS."NAME" FROM USERS WHERE USERS.CITY = 1
SQL: SELECT USERS.ID, USERS.CITY, USERS."NAME" FROM USERS WHERE USERS.CITY = 2
SQL: SELECT USERS.ID, USERS.CITY, USERS."NAME" FROM USERS WHERE USERS.CITY = 3
SQL: SELECT USERS.ID, USERS.CITY, USERS."NAME" FROM USERS WHERE USERS.CITY = 4
由於存取 city.users
的逻辑撰写在 forEach()
内,所以 Exposed 在第一次透过 SELECT 取出所有的 city
之後,必须对个别 city
都执行一次 SELECT 语法,来取出对应的 user
。
如果今天 city
的个数越来越多,那麽可以想到这段程序的 query 数量就会越来越多,运行时间也就会越来越长。
这就是我们所说的 N+1 问题,框架先透过一次 query ,取出了 N 个物件,然後针对每个物件都个别执行 query,导致再执行了 N 次 query,才能取出关联的物件。所以总计需要 N+1 个 query 才能达成我们需要的结果。
那麽,要怎麽改善这段程序呢?
with()
要改善的逻辑其实很单纯,就是我们要让 Exposed 在 city.all()
之後,就知道我们之後的程序会需要 city.users
的内容,并且事先取出所有 city 对应的 city.users
。
我们透过 with()
函数,来改写前面这段逻辑
addLogger(StdOutSqlLogger)
City
.all()
.with(City::users)
.forEach {
it
.users
.forEach{
println(it.name)
}
}
这样撰写之後,我们取出的内容是一样的。不过我们的 query 会变成
SQL: SELECT CITIES.ID, CITIES."NAME" FROM CITIES
SQL: SELECT USERS.ID, USERS.CITY, USERS."NAME" FROM USERS WHERE USERS.CITY IN (1, 2, 3, 4)
由於事先就知道我们後面的逻辑会用到 city.users
的内容,所以 Exposed 就先用一个 query,取出所有的 city.users
了。
这样不管我们的 city
有几笔资料,这段程序之後执行时都会是两次 query 完成,不会有随着资料成长导致 query 数目增加的问题,N+1 问题也就解决了。
https://bootstrap5.hexschool.com/docs/5.1/getting-...
昨天是使用Android平台来作开发,当然不可少iOS平台罗! 有人给你了apk(Android),...
Day 7 卡片在商品介绍 或登入介面时常用到 通常格式为一张图片 与他的title 配上说明 也有...
阿嬷都看得懂的 JavaScript 怎麽写 昨天我们提及程序语言的 4 个重要特徵: 变数 型别 ...
你学到了甚麽? 我们可以将学到的图表分为3类 Trends - 可以定义一种变换的模式 sns.li...