Hibernate-reactive: pengambilan koleksi malas di Quarkus

Dibuat pada 12 Mar 2021  ·  30Komentar  ·  Sumber: hibernate/hibernate-reactive

Saat mencoba mengambil asosiasi dengan malas dengan Mutiny di Quarkus, itu tidak berfungsi jika pengambilan dilakukan di sesi yang berbeda dari sesi yang digunakan untuk menyimpan data. Jika sesi yang sama digunakan (jadi mengambil tepat setelah bertahan di sesi yang sama), itu berfungsi.

Perilaku yang diharapkan
Ia bekerja juga dalam sesi yang berbeda.

Perilaku sebenarnya
Pengecualian dilemparkan saat mencoba mengambil asosiasi:

2021-03-12 09:56:23,634 ERROR [org.jbo.res.rea.com.cor.AbstractResteasyReactiveContext] (vert.x-eventloop-thread-2) Request failed: org.hibernate.LazyInitializationException: Collection cannot be initialized: com.example.Author.books
    at org.hibernate.reactive.session.impl.ReactiveSessionImpl.initializeCollection(ReactiveSessionImpl.java:330)
    at org.hibernate.collection.internal.AbstractPersistentCollection$4.doWork(AbstractPersistentCollection.java:589)
    at org.hibernate.collection.internal.AbstractPersistentCollection.withTemporarySessionIfNeeded(AbstractPersistentCollection.java:264)
    at org.hibernate.collection.internal.AbstractPersistentCollection.initialize(AbstractPersistentCollection.java:585)
    at org.hibernate.collection.internal.AbstractPersistentCollection.read(AbstractPersistentCollection.java:149)
    at org.hibernate.collection.internal.AbstractPersistentCollection$1.doWork(AbstractPersistentCollection.java:178)
    at org.hibernate.collection.internal.AbstractPersistentCollection$1.doWork(AbstractPersistentCollection.java:163)
    at org.hibernate.collection.internal.AbstractPersistentCollection.withTemporarySessionIfNeeded(AbstractPersistentCollection.java:264)
    at org.hibernate.collection.internal.AbstractPersistentCollection.readSize(AbstractPersistentCollection.java:162)
    at org.hibernate.collection.internal.PersistentBag.size(PersistentBag.java:371)
    at org.hibernate.bytecode.enhance.spi.interceptor.LazyAttributeLoadingInterceptor.takeCollectionSizeSnapshot(LazyAttributeLoadingInterceptor.java:160)
    at org.hibernate.bytecode.enhance.spi.interceptor.LazyAttributeLoadingInterceptor.lambda$loadAttribute$0(LazyAttributeLoadingInterceptor.java:110)
    at org.hibernate.bytecode.enhance.spi.interceptor.EnhancementHelper.performWork(EnhancementHelper.java:130)
    at org.hibernate.bytecode.enhance.spi.interceptor.LazyAttributeLoadingInterceptor.loadAttribute(LazyAttributeLoadingInterceptor.java:76)
    at org.hibernate.bytecode.enhance.spi.interceptor.LazyAttributeLoadingInterceptor.fetchAttribute(LazyAttributeLoadingInterceptor.java:72)
    at org.hibernate.bytecode.enhance.spi.interceptor.LazyAttributeLoadingInterceptor.handleRead(LazyAttributeLoadingInterceptor.java:53)
    at org.hibernate.bytecode.enhance.spi.interceptor.AbstractInterceptor.readObject(AbstractInterceptor.java:153)
    at com.example.Author.$$_hibernate_read_books(Author.java)
    at com.example.Author.getBooks(Author.java:43)
    at com.example.TestResource.lambda$getBooks$2(TestResource.java:61)
    at io.smallrye.mutiny.operators.UniOnItemTransformToUni.invokeAndSubstitute(UniOnItemTransformToUni.java:31)
    at io.smallrye.mutiny.operators.UniOnItemTransformToUni$2.onItem(UniOnItemTransformToUni.java:74)
    at io.smallrye.mutiny.context.ContextPropagationUniInterceptor$1.lambda$onItem$1(ContextPropagationUniInterceptor.java:31)
    at io.smallrye.context.impl.wrappers.SlowContextualExecutor.execute(SlowContextualExecutor.java:19)
    at io.smallrye.mutiny.context.ContextPropagationUniInterceptor$1.onItem(ContextPropagationUniInterceptor.java:31)
    at io.smallrye.mutiny.operators.UniSerializedSubscriber.onItem(UniSerializedSubscriber.java:85)
    at io.smallrye.mutiny.context.ContextPropagationUniInterceptor$1.lambda$onItem$1(ContextPropagationUniInterceptor.java:31)
    at io.smallrye.context.impl.wrappers.SlowContextualExecutor.execute(SlowContextualExecutor.java:19)
    at io.smallrye.mutiny.context.ContextPropagationUniInterceptor$1.onItem(ContextPropagationUniInterceptor.java:31)
    at io.smallrye.mutiny.operators.UniDelegatingSubscriber.onItem(UniDelegatingSubscriber.java:24)
    at io.smallrye.mutiny.context.ContextPropagationUniInterceptor$1.lambda$onItem$1(ContextPropagationUniInterceptor.java:31)
    at io.smallrye.context.impl.wrappers.SlowContextualExecutor.execute(SlowContextualExecutor.java:19)
    at io.smallrye.mutiny.context.ContextPropagationUniInterceptor$1.onItem(ContextPropagationUniInterceptor.java:31)
    at io.smallrye.mutiny.operators.UniSerializedSubscriber.onItem(UniSerializedSubscriber.java:85)
    at io.smallrye.mutiny.context.ContextPropagationUniInterceptor$1.lambda$onItem$1(ContextPropagationUniInterceptor.java:31)
    at io.smallrye.context.impl.wrappers.SlowContextualExecutor.execute(SlowContextualExecutor.java:19)
    at io.smallrye.mutiny.context.ContextPropagationUniInterceptor$1.onItem(ContextPropagationUniInterceptor.java:31)
    at io.smallrye.mutiny.operators.uni.builders.UniCreateFromCompletionStage.lambda$forwardFromCompletionStage$1(UniCreateFromCompletionStage.java:30)
    at java.base/java.util.concurrent.CompletableFuture.uniWhenComplete(CompletableFuture.java:859)
    at java.base/java.util.concurrent.CompletableFuture$UniWhenComplete.tryFire(CompletableFuture.java:837)
    at java.base/java.util.concurrent.CompletableFuture.postComplete(CompletableFuture.java:506)
    at java.base/java.util.concurrent.CompletableFuture.complete(CompletableFuture.java:2073)
    at com.ibm.asyncutil.iteration.AsyncTrampoline$TrampolineInternal.unroll(AsyncTrampoline.java:127)
    at com.ibm.asyncutil.iteration.AsyncTrampoline$TrampolineInternal.lambda$unroll$0(AsyncTrampoline.java:123)
    at java.base/java.util.concurrent.CompletableFuture.uniWhenComplete(CompletableFuture.java:859)
    at java.base/java.util.concurrent.CompletableFuture$UniWhenComplete.tryFire(CompletableFuture.java:837)
    at java.base/java.util.concurrent.CompletableFuture.postComplete(CompletableFuture.java:506)
    at java.base/java.util.concurrent.CompletableFuture.complete(CompletableFuture.java:2073)
    at org.hibernate.reactive.pool.impl.Handlers.lambda$toCompletionStage$0(Handlers.java:26)
    at io.vertx.sqlclient.impl.SqlResultHandler.complete(SqlResultHandler.java:98)
    at io.vertx.sqlclient.impl.SqlResultHandler.handle(SqlResultHandler.java:87)
    at io.vertx.sqlclient.impl.SqlResultHandler.handle(SqlResultHandler.java:33)
    at io.vertx.sqlclient.impl.SocketConnectionBase.handleMessage(SocketConnectionBase.java:241)
    at io.vertx.sqlclient.impl.SocketConnectionBase.lambda$init$0(SocketConnectionBase.java:88)
    at io.vertx.core.net.impl.NetSocketImpl.lambda$new$2(NetSocketImpl.java:101)
    at io.vertx.core.streams.impl.InboundBuffer.handleEvent(InboundBuffer.java:237)
    at io.vertx.core.streams.impl.InboundBuffer.write(InboundBuffer.java:127)
    at io.vertx.core.net.impl.NetSocketImpl.handleMessage(NetSocketImpl.java:357)
    at io.vertx.core.impl.ContextImpl.executeTask(ContextImpl.java:366)
    at io.vertx.core.impl.EventLoopContext.execute(EventLoopContext.java:43)
    at io.vertx.core.impl.ContextImpl.executeFromIO(ContextImpl.java:229)
    at io.vertx.core.net.impl.VertxHandler.channelRead(VertxHandler.java:163)
    at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:379)
    at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:365)
    at io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:357)
    at io.netty.channel.CombinedChannelDuplexHandler$DelegatingChannelHandlerContext.fireChannelRead(CombinedChannelDuplexHandler.java:436)
    at io.vertx.pgclient.impl.codec.PgEncoder.lambda$write$0(PgEncoder.java:78)
    at io.vertx.pgclient.impl.codec.PgCommandCodec.handleReadyForQuery(PgCommandCodec.java:138)
    at io.vertx.pgclient.impl.codec.PgDecoder.decodeReadyForQuery(PgDecoder.java:226)
    at io.vertx.pgclient.impl.codec.PgDecoder.channelRead(PgDecoder.java:86)
    at io.netty.channel.CombinedChannelDuplexHandler.channelRead(CombinedChannelDuplexHandler.java:251)
    at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:379)
    at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:365)
    at io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:357)
    at io.netty.channel.DefaultChannelPipeline$HeadContext.channelRead(DefaultChannelPipeline.java:1410)
    at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:379)
    at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:365)
    at io.netty.channel.DefaultChannelPipeline.fireChannelRead(DefaultChannelPipeline.java:919)
    at io.netty.channel.nio.AbstractNioByteChannel$NioByteUnsafe.read(AbstractNioByteChannel.java:163)
    at io.netty.channel.nio.NioEventLoop.processSelectedKey(NioEventLoop.java:714)
    at io.netty.channel.nio.NioEventLoop.processSelectedKeysOptimized(NioEventLoop.java:650)
    at io.netty.channel.nio.NioEventLoop.processSelectedKeys(NioEventLoop.java:576)
    at io.netty.channel.nio.NioEventLoop.run(NioEventLoop.java:493)
    at io.netty.util.concurrent.SingleThreadEventExecutor$4.run(SingleThreadEventExecutor.java:989)
    at io.netty.util.internal.ThreadExecutorMap$2.run(ThreadExecutorMap.java:74)
    at io.netty.util.concurrent.FastThreadLocalRunnable.run(FastThreadLocalRunnable.java:30)

Reproduksi

https://github.com/markusdlugi/hibernate-reactive-lazy-fetching

Langkah-langkah untuk mereproduksi perilaku:

  1. Mulai basis data menggunakan infrastructure/docker-compose.yml .
  2. Mulai aplikasi Quarkus menggunakan mvn quarkus:dev
  3. Kirim permintaan: POST http://localhost:8080/test/working .
    3.1. Ini berfungsi dan mengembalikan daftar buku.
  4. Kirim permintaan: GET http://localhost:8080/test/failing/1 (atau authorId lain yang dibuat melalui POST http://localhost:8080/test/failing ).
    4.1 Ini gagal dengan pengecualian di atas.
  • Versi Quarkus
  • Versi Reaktif Hibernasi: 1.0.0.Beta4 (juga diuji dengan 1.0.0.CR1 dengan menimpa versi di Maven, perilaku yang sama)
  • Hibernate Core Version: 5.4.28.Final (juga diuji dengan 5.4.29.Final dengan menimpa versi di Maven, perilaku yang sama)
bug

Komentar yang paling membantu

Baru saja mengujinya dan dapat mengonfirmasi bahwa pengambilan koleksi malas sekarang berfungsi di Quarkus. makasih sekali lagi :smile:

Semua 30 komentar

/cc @DavideD @gavinking

Ya, saya pikir kami memutuskan ini adalah hal yang benar untuk dilakukan: di JPA tidak ada gagasan untuk mengaitkan kembali instance entitas dengan sebuah sesi. Itu sedikit berbeda dengan pola penggunaan Hibernate yang sangat lama di mana Anda dapat menggunakan saveOrUpdate() untuk menghubungkan kembali objek yang terpisah secara langsung. Dalam JPA model untuk menangani objek yang terpisah adalah operasi merge() .

(Dan ada alasan bagus untuk tidak memiliki ini. Pengalaman adalah bahwa dalam praktiknya, pengguna membuat diri mereka berantakan dengan reasosiasi.)

Karena memuat koleksi terpisah benar-benar berarti memasang kembali induknya, kami tidak mendukung ini.

Namun, IIRC, aku membiarkan Anda melakukan ini dalam StatelessSession , karena itu jauh lebih kompatibel dengan model pemrograman. Coba gunakan sesi stateless dan beri tahu saya jika itu berhasil untuk Anda.

Hai @gavinking , terima kasih atas balasan cepatnya.

Saya tidak yakin apakah saya mengerti apa yang Anda katakan. Jadi apa yang saya terapkan persis seperti yang dijelaskan dalam dokumentasi :

session.find(Author.class, authorId)
    .chain(author -> Mutiny.fetch(author.getBooks()));

Ini berfungsi jika penulis dan bukunya telah bertahan dengan sesi yang sama (yaitu, dalam permintaan HTTP yang sama), tetapi tidak berfungsi jika saya menggunakan sesi lain (yaitu, permintaan lain).

Jadi Anda menyiratkan bahwa ini adalah perilaku yang dimaksudkan, setidaknya untuk sesi "normal" (status?)? Bagi saya ini seperti kasus penggunaan yang sangat standar, karena saya hampir selalu mencoba untuk mendapatkan beberapa data dari database beberapa saat setelah mempertahankannya, jadi dalam sesi yang berbeda. Ini berarti bahwa menggunakan sesi normal, asosiasi malas tidak didukung. Dalam hal ini, saya berpendapat bahwa harus disebutkan secara eksplisit dalam dokumentasi bahwa StatelessSession perlu digunakan jika asosiasi malas akan diambil.

Saya mengatakan bahwa di sesi baru Anda harus menggunakan getReference() untuk mendapatkan referensi terikat sesi ke entitas yang memiliki koleksi. Kemudian Anda dapat mengambil koleksi sesuka Anda.

Ini adalah model JPA standar, bahkan sedikit lebih mudah digunakan di HR karena saya kelebihan beban getReference() .

@gavinking maksudmu seperti ini?

Author reference = session.getReference( Author.class, authorId );
return Mutiny.fetch( reference.getBooks() );

Tentu, tapi saya membuatnya lebih mudah. Anda dapat langsung meneruskan penulis terpisah ke getReference() .

Oke, saya mencoba saran itu, tapi sayangnya tidak berhasil juga. Mungkin Anda bisa berbaik hati untuk menunjukkan jika saya melakukan sesuatu yang salah di sini:

https://github.com/markusdlugi/hibernate-reactive-lazy-fetching/blob/f3458eee969d7c3597560aaf8a646b7aad2ee9b0/src/main/Java/com/example/TestResource.java#L55 -L79

Pendekatan menggunakan getReference() gagal dengan pengecualian berikut:

2021-03-12 14:17:51,381 ERROR [org.jbo.res.rea.ser.cor.ExceptionMapping] (vert.x-eventloop-thread-10) Request failed : java.lang.NullPointerException
    at org.hibernate.engine.internal.AbstractEntityEntry.overwriteLoadedStateCollectionValue(AbstractEntityEntry.java:336)
    at org.hibernate.persister.entity.AbstractEntityPersister.initializeLazyProperty(AbstractEntityPersister.java:1144)
    at org.hibernate.persister.entity.AbstractEntityPersister.initializeEnhancedEntityUsedAsProxy(AbstractEntityPersister.java:4497)
    at org.hibernate.bytecode.enhance.spi.interceptor.EnhancementAsProxyLazinessInterceptor.forceInitialize(EnhancementAsProxyLazinessInterceptor.java:221)
    at org.hibernate.bytecode.enhance.spi.interceptor.EnhancementAsProxyLazinessInterceptor.lambda$handleRead$0(EnhancementAsProxyLazinessInterceptor.java:133)
    at org.hibernate.bytecode.enhance.spi.interceptor.EnhancementHelper.performWork(EnhancementHelper.java:130)
    at org.hibernate.bytecode.enhance.spi.interceptor.EnhancementAsProxyLazinessInterceptor.handleRead(EnhancementAsProxyLazinessInterceptor.java:98)
    at org.hibernate.bytecode.enhance.spi.interceptor.AbstractInterceptor.readObject(AbstractInterceptor.java:153)
    at com.example.Author.$$_hibernate_read_books(Author.java)
    at com.example.Author.getBooks(Author.java:43)
    at com.example.TestResource.getBooksUsingReference(TestResource.java:69)
    ...

Solusi lain yang disarankan menggunakan StatelessSession gagal dengan:

2021-03-12 14:21:11,840 ERROR [org.jbo.res.rea.ser.cor.ExceptionMapping] (vert.x-eventloop-thread-5) Request failed : org.hibernate.LazyInitializationException: Unable to perform requested lazy initialization [com.example.Author.books] - no session and settings disallow loading outside the Session
    at org.hibernate.bytecode.enhance.spi.interceptor.EnhancementHelper.throwLazyInitializationException(EnhancementHelper.java:199)
    at org.hibernate.bytecode.enhance.spi.interceptor.EnhancementHelper.performWork(EnhancementHelper.java:89)
    at org.hibernate.bytecode.enhance.spi.interceptor.LazyAttributeLoadingInterceptor.loadAttribute(LazyAttributeLoadingInterceptor.java:76)
    at org.hibernate.bytecode.enhance.spi.interceptor.LazyAttributeLoadingInterceptor.fetchAttribute(LazyAttributeLoadingInterceptor.java:72)
    at org.hibernate.bytecode.enhance.spi.interceptor.LazyAttributeLoadingInterceptor.handleRead(LazyAttributeLoadingInterceptor.java:53)
    at org.hibernate.bytecode.enhance.spi.interceptor.AbstractInterceptor.readObject(AbstractInterceptor.java:153)
    at com.example.Author.$$_hibernate_read_books(Author.java)
    at com.example.Author.getBooks(Author.java:43)
    at com.example.TestResource.lambda$getBooksUsingStatelessSession$3(TestResource.java:78)
    ...

Saya akan melihatnya, sepertinya terkait dengan peningkatan bytecode

@DavideD , ada pembaruan?

Saya mencoba mereproduksi masalah tanpa Quarkus hanya dengan menggunakan session-example dalam repo ini, tetapi saya tidak dapat melakukannya. Hanya dengan Hibernate Reactive dan Pemberontakan biasa, ini berfungsi seperti yang diharapkan. Ini juga berfungsi menggunakan find() , tidak diperlukan getReference() atau StatelessSession .

Jadi masalah ini sepertinya hanya terjadi di Quarkus.

Aku masih melihat ke dalamnya.

Masalah terjadi karena peningkatan bytecode yang diaktifkan secara default di Quarkus.
Saya masih belum mengetahui set properti mana yang membuat ulang masalah saat hanya menggunakan Hibernate Reactive. Tapi saya akan terus mengerjakannya hari ini

Saya sekarang dapat membuat ulang kesalahan dalam reaktif. Ini membutuhkan dua hal:

  1. atur hibernate.bytecode.allow_enhancement_as_proxy menjadi true
  2. setel SessionFactoryOptions#enableCollectionInDefaultFetchGroup ke false. Itu harus disetel ke true: https://github.com/hibernate/hibernate-reactive/blob/a3f4c61eef71baab8ca321da5af0bfb9052bde4b/hibernate-reactive-core/src/main/Java/org/hibernate/reactive/provider/service/ReactiveSessionService.jactoryva #L27

@gavinking Apakah saya benar dalam berpikir bahwa kita harus memastikan bahwa di Quarkus enableCollectionInDefaultFetchGroup disetel ke false? Saya akui bahwa saat ini saya tidak sepenuhnya memahami seluruh masalah (saya sudah mencoba mengikuti percakapan tentang masalah #374)

Um. Saya pikir masuk akal untuk memiliki koleksi di grup pengambilan default. Saya tidak berpikir itu pernah masuk akal untuk mengecualikan itu.

n Quarkus enableCollectionInDefaultFetchGroup disetel ke false

Maaf, ya, saya bermaksud menulis is set to true . Waktunya istirahat :-)

Ah oke bagus :-) Istirahat dulu :-)

@DavideD Itu beberapa temuan bagus, terima kasih :)

Hanya ingin menunjukkan, bahwa menurut diskusi di #374 dan terkait hibernate/hibernate-orm#3558, sepertinya enableCollectionInDefaultFetchGroup sengaja dibiarkan menjadi false di Quarkus? Setidaknya itulah yang saya kumpulkan dari komentar ini:

https://github.com/hibernate/hibernate-orm/pull/3558#issuecomment -695003875

Bagaimanapun, sepertinya Quarkus saat ini tidak menyetel properti itu ke true , karena tidak dikonfigurasi di FastBootReactiveEntityManagerFactoryBuilder :

https://github.com/quarkusio/quarkus/blob/55cf15173ff7b2985a8422de050d1c9c708be57c/extensions/hibernate-reactive/runtime/src/main/Java/io/quarkus/hibernate/reactive/runtime/boot/FastBootReactiveEntityManalder.

Dan jika saya memeriksanya dengan benar, default di Hibernate ORM tampaknya masih false , yang akan menjelaskan mengapa kami hanya melihat perilaku ini di Quarkus.

@Sanne perlu melihat yang ini.

Jika kita hanya perlu menyetelnya ke true, tampaknya cukup mudah untuk diselesaikan. Saya akan membuat perbaikan dan kemudian memeriksa dengan @Sanne

Nah yang saya ingin tahu adalah apakah itu disetel ke false karena itulah yang dibutuhkan ORM, dan apakah kita perlu berhati-hati untuk tidak menginjak Hibernate biasa?

penemuan yang bagus, terima kasih. Ya itu sedikit rumit karena dua ekstensi ("reguler" dan "reaktif") sangat digabungkan ATM.

Perlu memisahkan mereka dengan benar, yang dikembangkan terlalu terburu-buru.

(OTOH, saya tidak yakin mengapa ORM membutuhkannya; seperti yang saya katakan sebelumnya, saya pikir itu harus diaktifkan secara default.)

baiklah. Mari kita lakukan ini untuk kedua ORM (di Quarkus).

@gavinking , saya pikir judul itu salah, bukan? Saya kira Anda bermaksud meletakkan enableCollectionInDefaultFetchGroup sana. Peningkatan bytecode diperlukan untuk Quarkus karena asli, saya berasumsi :)

Ah ya, tentu, saya kira Anda benar.

Anda mengatakan bahwa Anda melihat bug ini dengan pengaturan default di Quarkus?

Ya, tidak diperlukan konfigurasi khusus di Quarkus untuk mendapatkan bug ini. @DavideD hanya perlu melakukan beberapa konfigurasi khusus untuk mereproduksi dalam HR biasa (tanpa Quarkus).

Tentu.

Masalahnya sebenarnya bukan bug di HR. Saya sengaja menambahkan bahwa "pengumpulan dalam grup pengambilan default" menjadi inti secara khusus karena kami membutuhkannya setiap saat di HR.

Jadi bug ada di ekstensi Quarkus yang mematikannya.

(Atau, bisa dibilang, pada intinya karena melakukan hal yang salah secara default.)

Saya telah mengirim perbaikan untuk Quarkus: https://github.com/quarkusio/quarkus/pull/15818
getReference tampaknya tidak bekerja pada quarkus saat ini. Saya akan membuat masalah terpisah untuk itu.

Kedengarannya bagus, terima kasih! 👍

Baru saja mengujinya dan dapat mengonfirmasi bahwa pengambilan koleksi malas sekarang berfungsi di Quarkus. makasih sekali lagi :smile:

Terima kasih @markusdlugi

Terima kasih telah melaporkannya @markusdlugi

Apakah halaman ini membantu?
0 / 5 - 0 peringkat