[Day 12] N+1 问题的解决方式:eager loading

前面我们介绍了透过 DAO 取出资料的许多方式,包含了一对多关联,多对多关联,甚至包含到 Parent-Child reference 的做法。

今天我们来介绍使用 DAO 有时会遇到的 N+1 问题,以及在 Exposed 框架下的解决方式。

什麽是 N+1 问题

比方说我们有一个一对多关联如下

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 问题也就解决了。


<<:  [Day5]-串列的相关用法

>>:  PHP 基础复习

Vue3 使用 Bs5 、 Jq 、 gsap

https://bootstrap5.hexschool.com/docs/5.1/getting-...

[Day6] Face Detection - 使用Google ML Kit (iOS)

昨天是使用Android平台来作开发,当然不可少iOS平台罗! 有人给你了apk(Android),...

[Day 8] 常用的卡片 Card

Day 7 卡片在商品介绍 或登入介面时常用到 通常格式为一张图片 与他的title 配上说明 也有...

[Day 27] 阿嬷都看得懂的 JavaScript 怎麽写

阿嬷都看得懂的 JavaScript 怎麽写 昨天我们提及程序语言的 4 个重要特徵: 变数 型别 ...

Day 24 [Python ML、资料视觉化] 如何选择图表型态

你学到了甚麽? 我们可以将学到的图表分为3类 Trends - 可以定义一种变换的模式 sns.li...