aws-amplify / amplify-android

The fastest and easiest way to use AWS from your Android app.
https://docs.amplify.aws/lib/q/platform/android/
Apache License 2.0
250 stars 118 forks source link

[DataStore] @hasMany connection to another model returns null inside query and crashes app #1676

Open srimalaya opened 2 years ago

srimalaya commented 2 years ago

Before opening, please confirm:

Language and Async Model

Kotlin - Coroutines

Amplify Categories

DataStore

Gradle script dependencies

```groovy // Put output below this line // Amplify core dependency implementation 'com.amplifyframework:core:1.32.1' implementation 'com.amplifyframework:core-kotlin:0.16.0' implementation 'com.amplifyframework:aws-api:1.32.1' implementation 'com.amplifyframework:aws-datastore:1.32.1' ```

Environment information

``` # Put output below this line ------------------------------------------------------------ Gradle 7.2 ------------------------------------------------------------ Build time: 2021-08-17 09:59:03 UTC Revision: a773786b58bb28710e3dc96c4d1a7063628952ad Kotlin: 1.5.21 Groovy: 3.0.8 Ant: Apache Ant(TM) version 1.10.9 compiled on September 27 2020 JVM: 17.0.1 (Oracle Corporation 17.0.1+12-39) OS: Mac OS X 12.1 aarch64 ```

Please include any relevant guides or documentation you're referencing

https://docs.amplify.aws/lib/datastore/relational/q/platform/android/

Describe the bug

I am trying to access nested model (Blog & Post in schema) inside DataStore queries similar to how it is possible to access Nested CustomType (which works). In the code, when trying to access the (Mutable)List, I get a null pointer error and my app crashes. I have been facing this issue since amplifyframework version 1.31.3 and now I am on 1.32.x and still facing it.

Any ideas or suggestions?

This seems to be working on other platforms. On Flutter, https://github.com/aws-amplify/amplify-flutter/issues/260#issuecomment-909679333 mentions that [CustomType] is not supported but that works on my end. Only [AnotherModel] i.e. a @hasMany relationship between tables is not working.

Reproduction steps (if applicable)

  1. Set up env with provided GraphQL Schema
  2. Notice that list of CustomType is working fine and is accessible.
  3. But, list of AnotherModel is not accessible/returns null and crashes app.
  4. This seems to work on iOS, Flutter and JS, but only fails on Native Android (Kotlin)

Code Snippet

// Put your code below this line.
GlobalScope.launch {
            Amplify.DataStore.query(Blog::class, Where.id("1"))
                .catch { Log.e("DataStore", "Error", it)}
                .collect {
                    Log.d("DataStore", "${it.id}, ${it.name}, ${it.customs}")
                    Log.d("DataStore", it.customs[1].children[0].nestedName + ", customs size: ${it.customs.size}")
// the next line returns null and crashes app
                    Log.d("DataStore", "posts test id: ${it.post[0].id}")
                }
}

Log output

``` // Put your logs below this line E/AndroidRuntime: FATAL EXCEPTION: DefaultDispatcher-worker-1 Process: com.example.datastorecrud, PID: 22111 java.lang.NullPointerException: Attempt to invoke interface method 'java.lang.Object java.util.List.get(int)' on a null object reference at com.example.datastorecrud.MainActivity$onCreate$1$invokeSuspend$$inlined$collect$1.emit(Collect.kt:136) at kotlinx.coroutines.flow.FlowKt__ErrorsKt$catchImpl$$inlined$collect$1.emit(Collect.kt:134) at kotlinx.coroutines.flow.FlowKt__ChannelsKt.emitAllImpl$FlowKt__ChannelsKt(Channels.kt:61) at kotlinx.coroutines.flow.FlowKt__ChannelsKt$emitAllImpl$1.invokeSuspend(Unknown Source:11) at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33) at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:106) at kotlinx.coroutines.scheduling.CoroutineScheduler.runSafely(CoroutineScheduler.kt:571) at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.executeTask(CoroutineScheduler.kt:750) at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.runWorker(CoroutineScheduler.kt:678) at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.run(CoroutineScheduler.kt:665) ```

amplifyconfiguration.json

No response

GraphQL Schema

```graphql // Put your schema below this line type Blog @model { id: ID! name: String! customs: [MyCustomModel] notes: [String] post: [Post] @hasMany(indexName: "postByBlog", fields: ["id"]) } type MyCustomModel { id: ID! name: String! desc: String children: [MyNestedModel] } type MyNestedModel { id: ID! nestedName: String! notes: [String] } type Post @model { id:ID! name:String! blog_id: ID! @index(name: "postByBlog") blog: Blog @belongsTo(fields: ["blog_id"]) } ```

Additional information and screenshots

No response

eeatonaws commented 2 years ago

Thank you for reporting this issue, we are looking into a fix for the issue.

PS-MS commented 2 years ago

I have noticed this for any connection, not just hasMany.

In the above example Post.blog will return null as well, I assumed this was a limitation as the datastore sqlite tables only hold an ID.

srimalaya commented 2 years ago

@PS-MS I can confirm that Post.blog works on my end with the amplifyframework version I have mentioned above. I am able to get values inside a blog using "it.blog.id" or similar commands.

Only the @hasMany relationship does not work, specifically for [models].

One solution can be to increase the depth of codegen using "amplify configure codegen". However, the default depth is 2 which should be fine in this case.

Or, maybe try updating your CLI and Datastore to latest versions.

PS-MS commented 2 years ago

I've tried updating to the latest amplify releases and it.blog.id works because the id is stored in the Post table however it.blog.name will return null for me

alharris-at commented 2 years ago

Hi @shri-onecup, in the generated models, is that field post marked as a non-null type? Referring to the docs for relational models https://docs.amplify.aws/lib/datastore/relational/q/platform/android/ there should be no expectation today of models being linked internally. If the post field is marked as optional in Kotlin, i.e. List<Post>, then you may need to null-check in your code before accessing. If not then we should fix the types being emitted here.

srimalaya commented 2 years ago

@alharris-at This is the only line that I could find related to post in the model for blog.

private final @ModelField(targetType="Post") @HasMany(associatedWith = "blog", type = Post.class) List<Post> post = null;

As you can see, it is being set to null by default which seems to be the issue here.

I am also attaching the entire generated models folder for your reference. model.zip

I am on the following versions of Amplify: CLI: 8.0.2 AmplifyFramework: 1.35.0 Kotlin Facade: 0.19.0

alharris-at commented 2 years ago

Hi @shri-onecup, taking a look at that code, I don't think the issue is necessarily that it's being set to null, since that seems correct given the statement from our docs that The @hasOne and @hasMany directives do not support referencing a model which then references the initial model via @hasOne or @hasMany if DataStore is enabled.

I believe you should be able to achieve the same goal by executing the following code.

Amplify.DataStore.query(Post::class)
    .catch { Log.e("MyAmplifyApp", "Error", it) }
    .collect { post ->
        val comments = Amplify.DataStore
            .query(Comment::class, Where.matches(Comment.POST_ID.eq(post.id)))
            .toList()
        Log.d("MyAmplifyApp", "Post: $post, Comments: $comments")
        Log.d("MyAmplifyApp", "posts test id: ${comments[0].id}")
    }

And here's the full MainActivity.kt file I used to test this

MainActivity.kt

```kotlin package com.example.bugrepro1676 import androidx.appcompat.app.AppCompatActivity import android.os.Bundle import android.util.Log import androidx.lifecycle.lifecycleScope import com.amplifyframework.AmplifyException import com.amplifyframework.core.model.query.Where import com.amplifyframework.datastore.AWSDataStorePlugin import com.amplifyframework.datastore.generated.model.Comment import com.amplifyframework.datastore.generated.model.Post import com.amplifyframework.kotlin.core.Amplify import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.InternalCoroutinesApi import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.toList import kotlinx.coroutines.launch class MainActivity : AppCompatActivity() { @ExperimentalCoroutinesApi @InternalCoroutinesApi override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) try { Amplify.addPlugin(AWSDataStorePlugin()) Amplify.configure(applicationContext) Log.i("Amplify", "Initialized Amplify") } catch (e: AmplifyException) { Log.e("Amplify", "Could not initialize Amplify", e) } val post = Post.builder() .title("My First Post") .content("Post Content") .build() val comment1 = Comment.builder() .postId(post.id) .content("My First Comment") .build() val comment2 = Comment.builder() .postId(post.id) .content("My Second Comment") .build() lifecycleScope.launch { // Setup Test Data Amplify.DataStore.clear() Amplify.DataStore.save(post) Amplify.DataStore.save(comment1) Amplify.DataStore.save(comment2) // Verify Test Data Amplify.DataStore .query(Post::class) .catch { Log.e("MyAmplifyApp", "Query failed", it) } .collect { Log.i("MyAmplifyApp", "Post: $it") } Amplify.DataStore .query(Comment::class) .catch { Log.e("MyAmplifyApp", "Query failed", it) } .collect { Log.i("MyAmplifyApp", "Comment: $it") } // Repro NPE // Amplify.DataStore.query(Post::class, Where.id(post.id)) // .catch { Log.e("MyAmplifyApp", "Error", it) } // .collect { Log.d("MyAmplifyApp", "posts test id: ${it.comments[0].id}") } // Updated Customer Code Amplify.DataStore.query(Post::class) .catch { Log.e("MyAmplifyApp", "Error", it) } .collect { post -> val comments = Amplify.DataStore .query(Comment::class, Where.matches(Comment.POST_ID.eq(post.id))) .toList() Log.d("MyAmplifyApp", "Post: $post, Comments: $comments") Log.d("MyAmplifyApp", "posts test id: ${comments[0].id}") } } } } ```

mikepschneider commented 2 years ago

Agreed that this behavior is inconsistent across platforms, however we consider that it is currently working as intended on Android so I have changed this from a bug to a feature request. It's possible to work around this issue by querying the child objects as Al showed above. We are keeping this on the roadmap for a future release.

JoergSchultz-TWT commented 1 year ago

Any news on this? Actually, this behaviour is not only inconsistent across platforms, it is also inconsistent with the documentation. To make it worse, it means that if I ask the parent whether it has children, I get 'null', independent from whether it actually has children or not. I.e. inconsistent data. I can only get this information by asking the children about their parents.

The posted workaround might be a reasonable solution when printing out data. In a real case scenario it adds another level of flows and observes which unnecessarily complicates the code.

osamwelian3 commented 11 months ago

I faced the same problem in Android. I had to create a copy of the generated model class. Added the comments list field to the buildsteps. Created two extension functions in kotlin, one to convert the generated Post class to my modified PostCopy class and takes a list of comments as a parameter and passes it to the comments buildstep. The other extension function simply converts the PostCopy instance back to the original Post.

With that in place. I first query the comments, then in the on success I query the posts and convert the retrieved posts using my extension function and pass the list of comments. I now can use my PostCopy class to get the expected functionality for bidirectional relationship.

Typed this from my phone, would willing write a step by step if anyone is interested in the workaround once I get to my workstation.

ShreyasDifferenz commented 10 months ago

@osamwelian3 Have you managed to resolve this issue? We will attempt to address it as well, but it is not functioning correctly. Could you please provide a more detailed explanation, possibly with examples or screenshots, to aid our understanding?