Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
1 task done
srimalaya opened this issue Mar 9, 2022 · 11 comments
Open
1 task done
Labels
datastore DataStore category/plugins feature-request Request a new feature

Comments

@srimalaya
Copy link

srimalaya commented Mar 9, 2022

Before opening, please confirm:

Language and Async Model

Kotlin - Coroutines

Amplify Categories

DataStore

Gradle script dependencies

// 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, aws-amplify/amplify-flutter#260 (comment) 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

// 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

@srimalaya srimalaya changed the title [DataStore] @hasMany connection to another model returns null inside query and crashes application [DataStore] @hasMany connection to another model returns null inside query and crashes app Mar 9, 2022
@poojamat poojamat assigned poojamat and unassigned poojamat Mar 9, 2022
@sktimalsina sktimalsina added the datastore DataStore category/plugins label Mar 11, 2022
@eeatonaws eeatonaws self-assigned this Mar 11, 2022
@eeatonaws
Copy link
Contributor

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

@PS-MS
Copy link

PS-MS commented Mar 29, 2022

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
Copy link
Author

srimalaya commented Mar 29, 2022

@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
Copy link

PS-MS commented Mar 30, 2022

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

@div5yesh div5yesh added the bug Something isn't working label Apr 19, 2022
@alharris-at
Copy link
Contributor

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.

@chrisbonifacio chrisbonifacio added the pending-triage Issue is pending triage label Apr 20, 2022
@eeatonaws eeatonaws removed their assignment Apr 20, 2022
@srimalaya
Copy link
Author

srimalaya commented Apr 20, 2022

@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
Copy link
Contributor

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

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 mikepschneider added feature-request Request a new feature and removed bug Something isn't working labels May 3, 2022
@mikepschneider
Copy link
Contributor

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.

@poojamat poojamat removed the pending-triage Issue is pending triage label May 10, 2022
@JoergSchultz-TWT
Copy link

JoergSchultz-TWT commented Jun 28, 2023

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
Copy link

osamwelian3 commented Dec 30, 2023

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
Copy link

@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?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
datastore DataStore category/plugins feature-request Request a new feature
Projects
None yet
Development

No branches or pull requests