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
247 stars 117 forks source link

ConflictUnhandled error when using API and Datastore in the same project #591

Closed KayIlory closed 4 years ago

KayIlory commented 4 years ago

On 17th June, I was able to do update via Amplify with no issues. From Sunday, the 21st June 2020, I am no longer able to do this. The values do not get updated and I get errors.

Steps to reproduce.. Set api conflict resolution to auto merge

type UserTest @model
{
    id: ID!
    gender: Gender!
}

Android class

 public void createUser() {
        UserTest userTest = UserTest.builder().gender(Gender.Male).build();
        Amplify.API.mutate(ModelMutation.create(userTest),
                response -> {
                    if(response.getData() != null) {
                        updateUser(response.getData());
                    }
                }, error -> {
                    Log.e("MyAmplifyApp", "Create failed", error);
                });
    }

    public void updateUser(UserTest userTest)
    {
        UserTest _userTest = userTest.copyOfBuilder().gender(Gender.Female).build();
        Amplify.API.mutate(ModelMutation.update(_userTest),
                response -> {
                    if(response.getData() != null) {
                        Log.i("Output", response.getData().toString());
                    }
                }, error -> {
                    Log.e("MyAmplifyApp", "Create failed", error);
                });

    }

It doesn't update the sex from Male to Female.

image

KayIlory commented 4 years ago
waitForSignIn: userState:SIGNED_IN

I/Output: UserTest {id=a236299d-efd1-43ac-aaac-508c2e24be9c, gender=Male}

After an update to try and change Gender to Female and doesnt work..

KayIlory commented 4 years ago
E/Delete Error:: GraphQLResponse.Error{message='Conflict resolver rejects mutation.', locations='[GraphQLLocation{line='1', column='56'}]', path='[GraphQLPathSegment{value='deleteUserTest'}]', extensions='{errorInfo=null, data={gender=Male, id=76253e92-b18d-4f1e-b364-a766e8e32ab0}, errorType=ConflictUnhandled}'}

When trying to deleted a newly created user..

image

KayIlory commented 4 years ago
    public void createUser() {
        UserTest userTest = UserTest.builder().gender(Gender.Male).build();
        Amplify.API.mutate(ModelMutation.create(userTest),
                response -> {
                    if(response.getData() != null) {
                        //updateUser(response.getData());
                        deleteUser(response.getData());
                    }
                    if(response.getErrors() != null)
                    {
                        for(GraphQLResponse.Error error : response.getErrors())
                        {
                            Log.e("Create Error:", error.toString());
                        }
                    }
                }, error -> {
                    Log.e("MyAmplifyApp", "Create failed", error);
                });
    }

    public void updateUser(UserTest userTest)
    {
        UserTest _userTest = userTest.copyOfBuilder().gender(Gender.Female).build();
        Amplify.API.mutate(ModelMutation.update(_userTest),
                response -> {
                    if(response.getData() != null) {
                        deleteUser(response.getData());
                        Log.i("Output", response.getData().toString());
                    }
                    if(response.getErrors() != null)
                    {
                        for(GraphQLResponse.Error error : response.getErrors())
                        {
                            Log.e("Update Error:", error.toString());
                        }
                    }
                }, error -> {
                    Log.e("MyAmplifyApp", "Create failed", error);
                });

    }

    public void deleteUser(UserTest userTest)
    {
        Amplify.API.mutate(ModelMutation.delete(userTest),
                response -> {
                    if(response.getData() != null) {
                        Log.i("Output", response.getData().toString());
                    }
                    if(response.getErrors() != null)
                    {
                        for(GraphQLResponse.Error error : response.getErrors())
                        {
                            Log.e("Delete Error:", error.toString());
                        }
                    }
                }, error -> {
                    Log.e("MyAmplifyApp", "Create failed", error);
                });

    }
KayIlory commented 4 years ago

I disabled DataStore for entire API to get this working. ? Please select from one of the below mentioned services: GraphQL ? Select from the options below Disable DataStore for entire API

richardmcclellan commented 4 years ago

Hi @Kayllory, there are some known issues with using Datastore and API in the same app. Essentially, there's not an easy way to specific special fields like _version via Amplify.API, which are needed for conflict resolution, and that is likely why you are getting errors. This is something we want to fix, but for now, as a workaround, I'd suggest using API or Datastore, but not both.

jamesonwilliams commented 4 years ago

As @richardmcclellan notes, we do not currently support using GraphQL API and DataStore at the same time. It is recommended to use one or the other.

We can keep this as a feature request.

If you would like to see this functionality added, please add a 👍 or similar reaction.

Scarlett13 commented 4 years ago

hey @jamesonwilliams! just to clarify, as per today, can we use GraphQL API and DataStore at the same time using 1.3.0 library?

jamesonwilliams commented 4 years ago

@Scarlett13 Nope, still not possible in 1.3.0.

My original design and implementation of the DataStore did actually account for this functionality. I was asked to remove it, to align the behavior to iOS and JavaScript.

But, I have heard from many customers by now that this is an important piece of missing functionality. So, I've re-implemented a prototype of the functionality. With https://github.com/aws-amplify/amplify-android/pull/786, you would be able to add a "datastore" block to your amplifyconfiguration.json, as below. It would allow you to specify a particular API to use for DataStore sync:

{
    ...
    "api": {
        "plugins": {
            "awsAPIPlugin": {
                "[API NAME 1]": {
                    ...
                },
                "[API NAME 2]": {
                    ...
                }
            }
        }
    },
    "datastore": {
        "plugins": {
            "awsDataStorePlugin": {
                "apiName": "[API NAME 1]"
            }
        }
    }
}

^^ Note: need to align this with my team & with the other platforms. The config structure is subject to change.

KayIlory commented 4 years ago

@jamesonwilliams really cant wait for that enhancement. It will improve app performance by 4x

richardmcclellan commented 4 years ago

Hey @Kayllory, could you elaborate a little more on your use case? How would it improve your app performance?

Amplify.DataStore only works when conflict detection is enabled on your AppSync API. This is needed for DataStore to resolve conflicts when merging remote updates with the local store, so this probably won't be changing.

Amplify.API currently only works when conflict detection is disabled. When enabled, queries return errors about conflict handling because mutations do not include the _version of the model.

It may be possible to update Amplify.API such that it can work with conflict detection enabled. But before we go down that route, could you describe what you want to use Amplify.API for, that you can't do with Amplify.DataStore?

KayIlory commented 4 years ago

@richardmcclellan - my understanding is that datastore store the data locally and sync with the backend dynamo while API is direct async call to the dynamo for CRUD operations. We see performance issues on the London and Frankfurt regions when do API Crud operations. With datastore we hoping user experience doesnt have to wait for that call, and simply persist locally and sync with the backend without user needed to constantly hit the backed. We found that if we can do a combination of datastore with sync for most of our calls and APi for the ones we need to always get latest from server, the application experience is much better. The time it takes to read 10rows about 4secs from backend with API, we tried with datastore and it was instant. Having API work with conflict detection enabled means we can use both datastore and api in the same app without issues is my understanding.

Happy to be corrected if my assumptions above are incorrect.

richardmcclellan commented 4 years ago

That's right, DataStore persists data locally, so that when you do a DataStore.query, you get an almost instant response, since it's reading from the SQLite table on disc, rather than making a network call. When DataStore is initialized, it fetches all of your models from the AppSync API and saves them locally. This sync actually just uses Amplify.API under the hood, so there's no different in "performance" between API and DataStore, except that DataStore is caching everything on disc when your app starts, so when you query for it, it's available right away.

We found that if we can do a combination of datastore with sync for most of our calls and API for the ones we need to always get latest from server, the application experience is much better.

DataStore also subscribes to all changes on all models, so even after the sync queries are completed, it should always be up to date, so you shouldn't need to use Amplify.API at all.

Scarlett13 commented 4 years ago

Hi @jamesonwilliams @richardmcclellan I have tried what you suggested to me, and I got this error

{message=Failure performing sync query to AppSync: [GraphQLResponse.Error{message='Cannot return null for non-nullable type: 'AWSTimestamp' within parent 'SelfEducation' (/syncSelfEducations/items[0]/_lastChangedAt)', locations='null', path='[GraphQLPathSegment{value='syncSelfEducations'}, GraphQLPathSegment{value='items'}, GraphQLPathSegment{value='0'}, GraphQLPathSegment{value='_lastChangedAt'}]', extensions='null'}, GraphQLResponse.Error{message='Cannot return null for non-nullable type: 'Int' within parent 'SelfEducation' (/syncSelfEducations/items[0]/_version)', locations='null', path='[GraphQLPathSegment{value='syncSelfEducations'}, GraphQLPathSegment{value='items'}, GraphQLPathSegment{value='0'}, GraphQLPathSegment{value='_version'}]', extensions='null'}, GraphQLResponse.Error{message='Cannot return null for non-nullable type: 'AWSTimestamp' within parent 'SelfEducation' (/syncSelfEducations/items[1]/_lastChangedAt)', locations='null', path='[GraphQLPathSegment{value='syncSelfEducations'}, GraphQLPathSegment{value='items'}, GraphQLPathSegment{value='1'}, GraphQLPathSegment{value='_lastChangedAt'}]', extensions='null'}, GraphQLResponse.Error{message='Cannot return null for non-nullable type: 'Int' within parent 'SelfEducation' (/syncSelfEducations/items[1]/_version)', locations='null', path='[GraphQLPathSegment{value='syncSelfEducations'}, GraphQLPathSegment{value='items'}, GraphQLPathSegment{value='1'}, GraphQLPathSegment{value='_version'}]', extensions='null'}, GraphQLResponse.Error{message='Cannot return null for non-nullable type: 'AWSTimestamp' within parent 'SelfEducation' (/syncSelfEducations/items[2]/_lastChangedAt)', locations='null', path='[GraphQLPathSegment{value='syncSelfEducations'}, GraphQLPathSegment{value='items'}, GraphQLPathSegment{value='2'}, GraphQLPathSegment{value='_lastChangedAt'}]', extensions='null'}, GraphQLResponse.Error{message='Cannot return null for non-nullable type: 'Int' within parent 'SelfEducation' (/syncSelfEducations/items[2]/_version)', locations='null', path='[GraphQLPathSegment{value='syncSelfEducations'}, GraphQLPathSegment{value='items'}, GraphQLPathSegment{value='2'}, GraphQLPathSegment{value='_version'}]', extensions='null'}], cause=null, recoverySuggestion=Sorry, we don't have a suggested fix for this error yet.}, recoverySuggestion=Check your internet connection.}

My schema look like:

type Self
@model(timestamps:{createdAt: "createdOn", updatedAt: "updatedOn"})
{
        id:ID!
        selfname: [SelfName] @connection(keyName: "bySelf", fields: ["id"])
        selfeducationhistory: [SelfEducation] @connection(keyName: "bySelf", fields: ["id"])
        ...
        updatedOn: AWSDateTime!
        createdOn: AWSDateTime!
}

type SelfName 
@model(timestamps:{createdAt: "createdOn", updatedAt: "updatedOn"})
@key(name: "bySelf", fields: ["selfid", "fullname"])
{
    id:ID!
    selfid:ID!
    ...
    createdOn: AWSDateTime! #
    updatedOn: AWSDateTime! #
}

type SelfEducation
@model(timestamps:{createdAt: "createdOn", updatedAt: "updatedOn"})
@key(name: "bySelf", fields: ["selfid", "institutionname"])
{
    id:ID!
    selfid:ID
    ...
    createdOn: AWSDateTime! #
    updatedOn: AWSDateTime! #
}

The error caused when I tried to add new SelfName to the existing Self type in dynamo db. Could you help me enlightened where did I do wrong? I didn't use Amplify.API in this case. And I am using 1.3.2 for every plugins. Thanks.