Reading and Updating an Ion Document

In this section, we’ll read our Person document back from QLDB, make some structural changes to the document using the Ion libraries, and we’ll write it back to QLDB. This approach is a great alternative to executing PartiQL UPDATE statements to update fields within a document, especially when making complex structural changes to the document or modifying elements in list fields.

We’ll start where we left off in the last section. The App.java file should be open in the Cloud9 IDE.

Create a new method for this exercise in the App.java file by adding the code below.

private static void readAndUpdateIon() throws Exception {
}

Modify the main() method to call our new method for this exercise instead of the method for the last exercise. The main() method should now look like this:

public static void main(String[] args) throws Exception {
    //buildAndWriteIon();
    readAndUpdateIon();
}

Initialize the QLDB driver as we did before by adding the following code to our new readAndUpdateIon() method. Again, change the ledger name in the driver initialization if you named your QLDB ledger something other than “ion-lab”.

QldbSessionClientBuilder sessionClientBuilder = QldbSessionClient.builder();
QldbDriver driver = QldbDriver
  .builder()
  .ledger("ion-lab")
  .sessionClientBuilder(sessionClientBuilder)
  .build();

Fetch the Person document that we created in the last exercise by searching QLDB for the document by its PersonId field. If you recall, we created an index on the PersonId field in the Person table in our ledger. Since we’re doing a search against an indexed field using an equality operator, this will be an efficient query in QLDB regardless of the size of our ledger.

Enter the code below into the readAndUpdateIon() method, below the initialization of the QLDB driver. We’re using the same Java lambda expression technique we used in the previous exercise. We execute a PartiQL SELECT statement, retrieving the entire Ion document.

driver.execute(txn -> {
    Result result = txn.execute("SELECT * FROM Person WHERE PersonId = '123456789'");
});

Retrieve our document from the Result object returned from our query executioner. Insert the code below into the readAndUpdateIon() method below the execution of the query, inside the driver.execute() call. This code is nearly identical to the code we used in the previous exercise to extract the document ID from the Result after inserting our document. However, this time, we run it inside the driver.execute() call because we have more code that we want to execute in the scope of the same QLDB transaction. We do so by putting it all into the same call to driver.execute().

Note that QLDB does not enforce uniqueness in its indexes. It is possible for multiple Person documents with the same PersonId to exist in the ledger unless our application prohibits it. In fact, if you ran the complete code from the last exercise multiple times, there may be multiple Person documents with the same PersonId. The code below simply grabs the first Ion document it encounters in the Result.

The null check in the last statement protects us from an error if we haven’t run the code from the previous exercise yet.

IonStruct personDocument = null;
Iterator<IonValue> iter = result.iterator();
while (iter.hasNext()) {
    IonValue obj = iter.next();
    if (obj instanceof IonStruct) {
        personDocument = (IonStruct) obj;
        break;
    }
}

if (personDocument == null)
    return;

Our readAndUpdateIon() method should look like this so far:

private static void readAndUpdateIon() throws Exception {
    QldbSessionClientBuilder sessionClientBuilder = QldbSessionClient.builder();
    QldbDriver driver = QldbDriver
      .builder()
      .ledger("ion-lab")
      .sessionClientBuilder(sessionClientBuilder)
      .build();

    driver.execute(txn -> {
        Result result = txn.execute("SELECT * FROM Person WHERE PersonId = '123456789'");

        IonStruct personDocument = null;
        Iterator<IonValue> iter = result.iterator();
        while (iter.hasNext()) {
            IonValue obj = iter.next();
            if (obj instanceof IonStruct) {
                personDocument = (IonStruct) obj;
                break;
            }
        }

        if (personDocument == null)
            return;
    });
}        

Let’s pretty-print our Ion document to make sure it’s the document we expected. Add the following line of code to the end of our lambda expression inside the call to driver.execute(), below the code that checks to see if the personDocument is null.

System.out.println(personDocument.toPrettyString());

The output should contain something like this:

{
  PersonId:"123456789",
  FirstName:"John",
  LastName:"Doe",
  MoneyInWallet:31.24,
  DateOfBirth:1970-07-04,
  NumberOfLegs:2,
  LikesGreenBeans:false,
  ThingsInPocket:[
    "keys",
    "pocketknife",
    "lint",
    "pack of gum"
  ],
  HomeAddress:{
    Street1:"123 Main Street",
    City:"Beverly Hills",
    State:"CA",
    Zip:"90210"
  },
  WorkAddress:{
    Street1:"12 Elm Street",
    City:"Los Angeles",
    State:"CA",
    Zip:"90001"
  }
}

Change our Person’s first name to demonstrate how to replace a basic value type. Add the code below to the the readAndUpdateIon() method before the pretty-printing of the document.

The code calls put() on the IonStruct object. put() will replace a field with the same name if it already exists. IonStruct also offers an add() method that will simply add another field with the same name to the document. We want to change the existing first name, not add another, so we’ll use put().

personDocument.put("FirstName").newString("Johnathan");

Now let’s change how we model our addresses. Instead of having work and home address elements at the top level of our document, let’s instead have a list of addresses where each address has a field that identifies its type (work, home, etc.).

First, remove the HomeAddress element from our Person document. Note that the call to remove() on our IonStruct personDocument object returns the element we’ve just removed. It’s returned as an IonValue, so we cast it to IonStruct. Add our new Type identifier to the address to indicate that it’s a “home” address. Add the code below to the readAndUpdateIon() method.

IonStruct homeAddress = (IonStruct) personDocument.remove("HomeAddress");
homeAddress.put("Type").newString("home");

Do the same thing for the work address. Add the code below to the readAndUpdateIon() method.

IonStruct workAddress = (IonStruct) personDocument.remove("WorkAddress");
workAddress.put("Type").newString("work");

Now we have two IonStruct objects that represent each of our addresses. These documents are not attached to our Person, so let’s attach them. See the code below. First, we create a new empty IonList field on our Person document called “Addresses”. Then we call add() on the addresses list to append each address to the list. Add the code below to the readAndUpdateIon() method.

IonList addresses = personDocument.put("Addresses").newEmptyList();
addresses.add(homeAddress);
addresses.add(workAddress);

Run the program to see how our Person document is structured now.

{
  PersonId:"123456789",
  LastName:"Doe",
  MoneyInWallet:31.24,
  DateOfBirth:1970-07-04,
  NumberOfLegs:2,
  LikesGreenBeans:false,
  ThingsInPocket:[
    "keys",
    "pocketknife",
    "lint",
    "pack of gum"
  ],
  FirstName:"Johnathan",
  Addresses:[
    {
      Street1:"123 Main Street",
      City:"Beverly Hills",
      State:"CA",
      Zip:"90210",
      Type:"home"
    },
    {
      Street1:"12 Elm Street",
      City:"Los Angeles",
      State:"CA",
      Zip:"90001",
      Type:"work"
    }
  ]
}

Now that we’ve made our modifications, let’s update the record in QLDB. Add the code below to the end of the driver.execute() call in the readAndUpdateIon() method. The code should look remarkably familiar to our other examples of querying from QLDB. Here we’re executing a PartiQL UPDATE statement, passing in the entire Ion document as a parameter. The WHERE clause in our PartiQL statement is very important. Without it, our statement would update all documents in the Person table. Since this UPDATE statement is executed inside the same call to driver.execute() as our SELECT statement, both statements will be executed together as an atomic operation in the same QLDB transaction.

txn.execute("UPDATE Person AS p SET p = ? WHERE PersonId = '123456789'", personDocument);

Notice that we have not explicitly declared any transaction starts, commits, or rollbacks in our code. The QLDB driver handles the transaction semantics for us.

Verify that our document was successfully updated in QLDB by running the following query in the Query Editor in the QLDB console:

select * from Person where PersonId = '123456789'

Verify Update

There should be one result returned. Click the checkbox in the Output section of the query editor and click the “View as Ion” button to view the entire document.

View as Ion

The complete readAndUpdateIon() method should now look like this:

private static void readAndUpdateIon() throws Exception {
    QldbSessionClientBuilder sessionClientBuilder = QldbSessionClient.builder();
    QldbDriver driver = QldbDriver
      .builder()
      .ledger("ion-lab")
      .sessionClientBuilder(sessionClientBuilder)
      .build();

    driver.execute(txn -> {
        Result result = txn.execute("SELECT * FROM Person WHERE PersonId = '123456789'");

        IonStruct personDocument = null;
        Iterator<IonValue> iter = result.iterator();
        while (iter.hasNext()) {
            IonValue obj = iter.next();
            if (obj instanceof IonStruct) {
                personDocument = (IonStruct) obj;
                break;
            }
        }

        if (personDocument == null)
            return;

        System.out.println(personDocument.toPrettyString());

        personDocument.put("FirstName").newString("Johnathan");

        IonStruct homeAddress = (IonStruct) personDocument.remove("HomeAddress");
        homeAddress.put("Type").newString("home");

        IonStruct workAddress = (IonStruct) personDocument.remove("WorkAddress");
        workAddress.put("Type").newString("work");

        IonList addresses = personDocument.put("Addresses").newEmptyList();
        addresses.add(homeAddress);
        addresses.add(workAddress);

        txn.execute("UPDATE Person AS p SET p = ? WHERE PersonId = '123456789'", personDocument);
    });
}

If you are having trouble getting your program to run, click here to download the complete App.java file for this lab.