Artikel ini akan membahas cara mempercepat penulisan informasi dalam jumlah besar ke database relasional untuk aplikasi yang ditulis menggunakan Spring Boot. Saat menulis banyak baris sekaligus, Hibernate menyisipkannya satu per satu, yang menyebabkan penantian signifikan jika ada banyak baris. Mari kita pertimbangkan kasus bagaimana menyiasati ini.
Kami menggunakan aplikasi Spring Boot. Sebagai DBMS -> MS SQL Server, sebagai bahasa pemrograman - Kotlin. Tentu saja, tidak akan ada perbedaan untuk Java.
Entitas untuk data yang perlu kita tulis:
@Entity
@Table(schema = BaseEntity.schemaName, name = GoodsPrice.tableName)
data class GoodsPrice(
@Id
@Column(name = "GoodsPriceId")
@GeneratedValue(strategy = GenerationType.IDENTITY)
override val id: Long,
@Column(name = "GoodsId")
val goodsId: Long,
@Column(name = "Price")
val price: BigDecimal,
@Column(name = "PriceDate")
val priceDate: LocalDate
): BaseEntity(id) {
companion object {
const val tableName: String = "GoodsPrice"
}
}
SQL:
CREATE TABLE [dbo].[GoodsPrice](
[GoodsPriceId] [int] IDENTITY(1,1) NOT NULL,
[GoodsId] [int] NOT NULL,
[Price] [numeric](18, 2) NOT NULL,
[PriceDate] nvarchar(10) NOT NULL,
CONSTRAINT [PK_GoodsPrice] PRIMARY KEY(GoodsPriceId))
Sebagai contoh demo, kami akan mengasumsikan bahwa kami perlu merekam masing-masing 20.000 dan 50.000 rekaman.
Mari buat pengontrol yang akan menghasilkan data dan mentransfernya untuk merekam dan mencatat waktu:
@RestController
@RequestMapping("/api")
class SaveDataController(private val goodsPriceService: GoodsPriceService) {
@PostMapping("/saveViaJPA")
fun saveDataViaJPA(@RequestParam count: Int) {
val timeStart = System.currentTimeMillis()
goodsPriceService.saveAll(prepareData(count))
val secSpent = (System.currentTimeMillis() - timeStart) / 60
logger.info("Seconds spent : $secSpent")
}
private fun prepareData(count: Int) : List<GoodsPrice> {
val prices = mutableListOf<GoodsPrice>()
for (i in 1..count) {
prices.add(GoodsPrice(
id = 0L,
priceDate = LocalDate.now().minusDays(i.toLong()),
goodsId = 1L,
price = BigDecimal.TEN
))
}
return prices
}
companion object {
private val logger = LoggerFactory.getLogger(SaveDataController::class.java)
}
}
Kami juga akan membuat layanan untuk menulis data dan repositori GoodsPriceRepository
@Service
class GoodsPriceService(
private val goodsPriceRepository: GoodsPriceRepository
) {
private val xmlMapper: XmlMapper = XmlMapper()
fun saveAll(prices: List<GoodsPrice>) {
goodsPriceRepository.saveAll(prices)
}
}
Setelah itu, kami akan secara berurutan memanggil metode saveDataViaJPA kami untuk 20.000 catatan dan 50.000 catatan.
Menghibur:
Hibernate: insert into dbo.GoodsPrice (GoodsId, Price, PriceDate) values (?, ?, ?)
Hibernate: insert into dbo.GoodsPrice (GoodsId, Price, PriceDate) values (?, ?, ?)
Hibernate: insert into dbo.GoodsPrice (GoodsId, Price, PriceDate) values (?, ?, ?)
Hibernate: insert into dbo.GoodsPrice (GoodsId, Price, PriceDate) values (?, ?, ?)
Hibernate: insert into dbo.GoodsPrice (GoodsId, Price, PriceDate) values (?, ?, ?)
Hibernate: insert into dbo.GoodsPrice (GoodsId, Price, PriceDate) values (?, ?, ?)
Hibernate: insert into dbo.GoodsPrice (GoodsId, Price, PriceDate) values (?, ?, ?)
Hibernate: insert into dbo.GoodsPrice (GoodsId, Price, PriceDate) values (?, ?, ?)
Hibernate: insert into dbo.GoodsPrice (GoodsId, Price, PriceDate) values (?, ?, ?)
Hibernate: insert into dbo.GoodsPrice (GoodsId, Price, PriceDate) values (?, ?, ?)
Hibernate: insert into dbo.GoodsPrice (GoodsId, Price, PriceDate) values (?, ?, ?)
Hibernate: insert into dbo.GoodsPrice (GoodsId, Price, PriceDate) values (?, ?, ?)
Hibernate: insert into dbo.GoodsPrice (GoodsId, Price, PriceDate) values (?, ?, ?)
Hibernate: insert into dbo.GoodsPrice (GoodsId, Price, PriceDate) values (?, ?, ?)
2020-11-10 19:11:58.886 INFO 10364 --- [ restartedMain] xmlsave.controller.SaveDataController : Seconds spent : 63
Masalahnya adalah Hibernate mencoba memasukkan setiap baris dalam kueri terpisah, yaitu 20.000 kali. Dan di mesin saya butuh 63 detik.
Untuk 50.000 entri 166 detik.
Solusi
Apa yang bisa dilakukan? Ide utamanya adalah kita akan menulis melalui tabel buffer:
@Entity
@Table(schema = BaseEntity.schemaName, name = SaveBuffer.tableName)
data class SaveBuffer(
@Id
@Column(name = "BufferId")
@GeneratedValue(strategy = GenerationType.IDENTITY)
override val id: Long,
@Column(name = "UUID")
val uuid: String,
@Column(name = "xmlData")
val xmlData: String
): BaseEntity(id) {
companion object {
const val tableName: String = "SaveBuffer"
}
}
Skrip SQL untuk tabel dalam database
CREATE TABLE [dbo].[SaveBuffer](
[BufferId] [int] IDENTITY NOT NULL,
[UUID] [varchar](64) NOT NULL,
[xmlData] [xml] NULL,
CONSTRAINT [PK_SaveBuffer] PRIMARY KEY (BufferId))
Tambahkan metode ke SaveDataController:
@PostMapping("/saveViaBuffer")
fun saveViaBuffer(@RequestParam count: Int) {
val timeStart = System.currentTimeMillis()
goodsPriceService.saveViaBuffer(prepareData(count))
val secSpent = (System.currentTimeMillis() - timeStart) / 60
logger.info("Seconds spent : $secSpent")
}
Mari juga tambahkan metode ke GoodsPriceService:
@Transactional
fun saveViaBuffer(prices: List<GoodsPrice>) {
val uuid = UUID.randomUUID().toString()
val values = prices.map {
BufferDTO(
goodsId = it.goodsId,
priceDate = it.priceDate.format(DateTimeFormatter.ISO_DATE),
price = it.price.stripTrailingZeros().toPlainString()
)
}
bufferRepository.save(
SaveBuffer(
id = 0L,
uuid = uuid,
xmlData = xmlMapper.writeValueAsString(values)
)
)
goodsPriceRepository.saveViaBuffer(uuid)
bufferRepository.deleteAllByUuid(uuid)
}
Untuk menulis, pertama-tama mari buat uuid unik untuk membedakan data saat ini yang sedang kita tulis. Selanjutnya, kami menulis data kami ke dalam buffer yang dibuat dengan teks dalam bentuk xml. Artinya, tidak akan ada 20.000 penyisipan, tetapi hanya 1.
Dan setelah itu kami mentransfer data dari buffer ke tabel GoodsPrice dengan satu kueri seperti Insert into⦠select.
GoodsPriceRepository dengan metode saveViaBuffer:
@Repository
interface GoodsPriceRepository: JpaRepository<GoodsPrice, Long> {
@Modifying
@Query("""
insert into dbo.GoodsPrice(
GoodsId,
Price,
PriceDate
)
select res.*
from dbo.SaveBuffer buffer
cross apply(select temp.n.value('goodsId[1]', 'int') as GoodsId
, temp.n.value('price[1]', 'numeric(18, 2)') as Price
, temp.n.value('priceDate[1]', 'nvarchar(10)') as PriceDate
from buffer.xmlData.nodes('/ArrayList/item') temp(n)) res
where buffer.UUID = :uuid
""", nativeQuery = true)
fun saveViaBuffer(uuid: String)
}
Dan pada akhirnya, agar tidak menyimpan informasi duplikat di database, kami menghapus data dari buffer oleh uuid.
Mari panggil metode saveViaBuffer kami untuk 20.000 baris dan 50.000 baris:
Hibernate: insert into dbo.SaveBuffer (UUID, xmlData) values (?, ?)
Hibernate: insert into dbo.SaveBuffer (UUID, xmlData) values (?, ?)
Hibernate: insert into dbo.SaveBuffer (UUID, xmlData) values (?, ?)
Hibernate: insert into dbo.SaveBuffer (UUID, xmlData) values (?, ?)
Hibernate:
insert into dbo.GoodsPrice(
GoodsId,
Price,
PriceDate
)
select res.*
from dbo.SaveBuffer buffer
cross apply(select temp.n.value('goodsId[1]', 'int') as GoodsId
, temp.n.value('price[1]', 'numeric(18, 2)') as Price
, temp.n.value('priceDate[1]', 'nvarchar(10)') as PriceDate
from buffer.xmlData.nodes('/ArrayList/item') temp(n)) res
where buffer.UUID = ?
Hibernate: select savebuffer0_.BufferId as bufferid1_1_, savebuffer0_.UUID as uuid2_1_, savebuffer0_.xmlData as xmldata3_1_ from dbo.SaveBuffer savebuffer0_ where savebuffer0_.UUID=?
Hibernate: delete from dbo.SaveBuffer where BufferId=?
Hibernate: delete from dbo.SaveBuffer where BufferId=?
Hibernate: delete from dbo.SaveBuffer where BufferId=?
Hibernate: delete from dbo.SaveBuffer where BufferId=?
2020-11-10 20:01:58.788 INFO 7224 --- [ restartedMain] xmlsave.controller.SaveDataController : Seconds spent : 13
Seperti yang Anda lihat dari hasil, kami mendapat percepatan pencatatan data yang signifikan.
Untuk 20.000 rekaman, 13 detik adalah 63.
Untuk 50.000 rekaman, 27 detik adalah 166.
Tautan ke proyek pengujian