Skip to content

Second Tutorial Part 6: Reading And Writing Data

Laurent Hasson edited this page Dec 3, 2019 · 3 revisions
Previous Main Next
<-- Part 5 Main Part 7 -->

In this section, we'll create a simple piece of code that generates a bunch of records for users and test questions. When thinking about the Tilda APIs, it's simpler to think of them in terms of regex-like patterns:

  • create set* write (set+ write)*
  • lookup set* read (set+ write)*
  • lookup set+ write

In plain English, you can

  • create an object in memory and set the not-null fields, optionally set additional fields through their respective setters, write the object, and then repeat the set/write cycle if needed.
  • lookup an object, optionally set additional fields that will complement the base-line where clause generated by the method of access, read the object, and then zero or more set/write cycles if needed.
  • lookup an object in memory, set at least one field, and then write to the database, making the assumption that the object already exists and saving a read from the database from occurring.

🎈 NOTE: Although there is no caching built into Tilda (i.e., a call to the data store is a call to the data store), there are often simple patterns of access that are optimized. For example

  • Calling write twice in a row without calling any set in between will not result in a duplicate update.
  • Calling set with a value matching what is already in the object's state won't mark the object as updated and calling write won't result in any additional update.
  • If you call lookup followed by set, followed by write, you'll get a single update in the store. Only if you call read will there be a select against the data store.

Creating Data

So let's start creating some sample user data.

// Sample data for user ids and emails
String[][] Users = { {"Tom" , "tom@blah.com" , "Blah, Tom"}
                  ,{"Mary", "mary@blah.com", "Bleh, Mary"}
                  ,{"Jean", "jean@blah.com", "Blih, Jean"}
                   };

for (String[] u: Users)
  {
    // Creating an object only creates is as a template in memory. No database
    // access occurs at this point. Note that the API doesn't require a
    // Connection object to be passed in,. which is a standard convention in
    // Tilda to denote operations that do not access the database layer.
    // The create method automatically lists all not-null columns (which don't
    // have default values defined).
    User_Data U = User_Factory.create(u[0], u[1]); // create(String id, String email)
    
    // Set the "name" field. It's nullable, so the setter is technically optional.
    U.setName(u[2]);
  
    // Writing now requires a connection, so this is a signal by convention that
    // the operation accesses the database backend. Because the object was just
    // created, we expect the write operation to do an insert.
    // The method will throw an exception is something drastic occurs, i.e., the 
    // database went down, or the generated SQL somehow causes a syntax error.
    // It's possible that someone may have dropped a column in the database but
    // not update the model for the application.
    // If the insert fails though because of a constraint violation (duplicate 
    // identity), Tilda manages the error handling and simply returns false to
    // the application caller. In databases such as Postgres where such an error  
    // invalidates the connection, proper handling of savepoints are necessary
    // for a clean behavior at the application level.
    if (U.write(C) == false)
      {
        // If the insert failed, we can try to lookup the object instead and update
        // it. This class has 2 natural identities as id and email must each be
        // unique. You can hover over the class to access the JavaDocs which are
        // generated by Tilda for the class.
        // Here, we give precedence to the email address as "stable" and update the
        // id. The reverse would have been another perfectly acceptable way to do
        // this. As for create(), the lookup() method doesn't take any connection
        // and therefore, doesn't cause a database access.
        U = User_Factory.lookupByEmail(u[1]);
  
        // We can set fields the easy way.
        U.setId(u[0]);
        U.setName(u[2]);
        
        // Here, because we looked up the object, it's set up to do an update
        // directly. Note that Tilda understands intent here and the object was never
        // read from the DB. This saves a SELECT as we can assume correctness here and
        // assume errors will be unlikely. In other applications, especially with
        // objects with multiple identifies error handling may be a bit more
        // complicated, i.e., in our case, it could be possible that someone changed
        // Tom's email to 'thomas@blah.com' and so the original create fails because
        // there is a duplicate on the id, but we assume the email is stable which
        // would no longer be true. Other cases are possible too.
        if (U.write(C) == false)
          throw new Exception("Error: Can't create/update user record '"
                             +u[0]+"/"+u[1]+"'."
                             );
      }
}

Now, this pattern of "if insert fails, update", or "if update fails, insert", is rather common: it's essentially Upsert. Some databases such as Postgres have great support for efficient upsert operations, and Postgres goes the extra mile by offering a syntax that is dead easy to use. Tilda offers a simple API entry point to do the same and control the order of insert/update or update/insert. What is best to use really depends on your use case. For this tutorial, we want to write code that could potentially be run over and over again, and does all the updating properly. As such, only the first time this code is run will an insert succeed. We can therefore optimize the whole process and try an update first (which will succeed in all cases except the first time), and then do an insert (only for the first time).

for (String[] u: Users)
  {
    // Creating an object only creates is as a template in memory. No database
    // access occurs at this point. Note that the API doesn't require a
    // Connection object to be passed in,. which is a standard convention in
    // Tilda to denote operations that do not access the database layer.
    // The create method automatically lists all not-null columns (which don't
    // have default values defined).
    User_Data U = User_Factory.create(u[0], u[1]); // create(String id, String email)
    
    // Set the "name" field. It's nullable, so the setter is technically optional.
    U.setName(u[2]);
  
    // We can upsert the object and specify we should try an update first, and then
    // an insert if the update failed.
    if (U.upsert(C, true) == false)
     throw new Exception("Error: Can't create/update user record '"
                        +u[0]+"/"+u[1]+"'."
                        );
  }

This is much simpler first of all, and second, it allows Tilda to figure out the best way to accomplish it.

Upsert

Upsert deserves some extra explanations. Currently, it is implemented as an insert/update or an update/insert, i.e., we do not yet take advantage of the wonderful native upsert syntax from Postgres (a future feature for sure). But the most important part is to understand how objects are identified. To do an update, you need a where clause, and because you are unlikely to have a primary key in advance, you are mostly likely going to go with a natural key. Upsert will look at all the fields set since the create and determine if any natural identity can be fulfilled. If one is found, it will be used as part of the update's where clause. If not, an exception will be thrown.

For upsert to function, it must fulfill the requirements of both insert and update, i.e., all its non-null fields must have been set (which create handles), and all the fields needed for a natural identity (in fact, any natural identify), must have been set (which would have been typically handled through lookup and which here is handled via additional set calls if needed). Clearly, if you know the object already exists, then an update you suffice, and if you knew the object didn't exist, then a create would suffice. The whole point of an upsert is to cover both cases simultaneously as to be transparent.

🎈 NOTE: Nowhere in this code was any data read from the database. The objects exist in memory and only contain the fields that were explicitly set by the application, along with fields that are set automatically (default values and the primary key refnum if auto-generated). If you try to get a field that wasn't set first, you'd get an exception. If you wanted to get the data from the database, you'd execute a simple read:

for (String[] u: Users)
  {
    // Creating an object only creates is as a template in memory. No database
    // access occurs at this point. Note that the API doesn't require a
    // Connection object to be passed in,. which is a standard convention in
    // Tilda to denote operations that do not access the database layer.
    // The create method automatically lists all not-null columns (which don't
    // have default values defined).
    User_Data U = User_Factory.create(u[0], u[1]); // create(String id, String email)
    
    // Set the "name" field. It's nullable, so the setter is technically optional.
    U.setName(u[2]);
  
    // We can upsert the object and specify we should try an update first, and then
    // an insert if the update failed.
    if (U.upsert(C, true) == false)
     throw new Exception("Error: Can't create/update user record '"
                        +u[0]+"/"+u[1]+"'."
                        );
    // Go back to the database to read all the fields from the object. In this
    // example, the table is trivial, but you can easily imagine a larger table with
    // lots of extra fields. Because many code patterns may not actually care about
    // the data in the database, only to update a few fields, the read call is left
    // to the application to manage.
    // As a convention, because we expect this to almost never fail in practice (we
    // just inserted or updated the object successfully), we throw an Error instead
    // of an Exception. This would depend of course on coding practices and standards
    // within your organization. For the sake of simplicity here, we are glossing over
    // some edge cases here where in fact, an error could happen if, let's say, the
    // database went down between the upsert and the following select.
    if (U.read(C) == false)
     throw new Error("Error: WHUT??? This should never happen!!! Cannot read user '"
                    +u[0]+"/"+u[1]+"' from the database."
                    );
    // Use any field from U. 
    ...
  }

Batch Inserts

When doing multiple inserts, you can gain significant performance improvements by batching the operations. Tilda currently provides support for this as part of a batch insert. You'd collect all the in-memory objects in a list, and pass that to the API. Similarly as before, for very large data sets, you'd add a loop.

private static boolean Test1c(Connection C)
throws Exception
  {
    List<User_Data> L = new ArrayList<User_Data>();
    for (String[] u : Users)
      {
        // Creating an object only creates is as a template in memory. No database
        // access occurs at this point. Note that the API doesn't require a
        // Connection object to be passed in,. which is a standard convention in
        // Tilda to denote operations that do not access the database layer.
        // The create method automatically lists all not-null columns (which don't
        // have default values defined).
        User_Data U = User_Factory.create(u[0], u[1]); // create(String id, String email)

        // Set the "name" field. It's nullable, so the setter is technically optional.
        U.setName(u[2]);

        // Add in-memory object to list
        L.add(U);
      }
    
    int failedRec = User_Factory.writeBatch(C, L, 1000, 5000);
    if (failedRec != -1)
      throw new Exception("Error inserting multiple User records at record # "
                         +failedRec+": "+L.get(failedRec).toString());
      
    return true;
  }

The writeBatch API allows you to set the batch size and the commit size. if the commit size is set to -1, then no commit will be performed internally. For large data sets, and depending on the size of the objects managed, you should spend some time tuning those 2 parameters as they can have significant performance implications. Based on your hardware configuration, and handling smaller objects (only a few columns), then setting the batch size in the thousands and the commit size as a multiple of that, may yield better performance than smaller numbers.

🎈 NOTE: It is important to understand that batch inserts imply some constraints: all objects must have a similar "change" profile. So for example, in our tutorial, ALL user objects are initialized exactly the same way: id, mail and name. If we had a conditional here, for example making name optional, the batch insert would fail because some records would insert 3 columns, and others would insert 2. Under the covers, the SQL generated is equivalent to the following and is the reason why batch inserts have this constraint.

insert into USER (id, email, name)
values (?,?,?)
      ,(?,?,?)
      ,(?,?,?)
etc...

Error Handling

Postgres can be annoyingly strict sometimes. In particular, the database will literally invalidate your session if ANY error occurs: you won't be able to issue any further query and need to immediately commit or rollback. This is annoying because errors come in all sorts of flavors of importance:

  • system level error, like the database went down, or ran out of disk space etc...
  • syntax error such as an invalidly written SQL statement, or referring to a column that doesn't exist or some other type related issue (comparing a date to an integer for example) etc...
  • a logical error because you issued an insert that failed due to a constraint violation for example, a missing not-null field, or tried to update a record that didn't exist in the database.

Frankly, the last class of errors should be handled in the application layer, and Tilda manages the details internally to not throw an exception or invalidate the current transaction so that application logic can perhaps handle the issue and continue. This is critical for a simple pattern like the upsert discussed above. To avoid an error, one would otherwise have to select first, and if a record was returned, do an update or an insert. In many practical cases, we can avoid the select altogether.

Under the covers, Tilda will test the error code returned by the JDBC layer and will either throw an exception (the first two classes of errors), or return a boolean false (the last class of "errors"). For Postgres, Tilda makes use of save-points to manage the error handling and is a better alternative than setting ON_ERROR_ROLLBACK which applies to all queries (not just inserts and updates which is what matters at the application level and where the third class of errors can occur). Nice descriptions of that Postgres behavior can we found here and here.

Finally, for Postgres, there is also the ability to use the upsert syntax ON CONFLICT DO NOTHING to cause those classes of errors to not invalidate the transaction, and most importantly, avoid doing a save-point which would further improve performance. The Tilda code would check that an insert succeeded but returned a count of 0 inserts and return accordingly true/false. This is a future feature.

Logging

The "T" in Tilda is for transparency... You might start getting sick of hearing this all the time :) Anyways, this applies also to the runtime. Extensive logging is provided to show all the underlying details of queries being generated and executed.

This is what the logging looks like for the first version of the code (insert/update) the first time it's run:

[TILDATUTORIAL.User]: insert into TILDATUTORIAL.User("refnum","id","email","name","created","lastUpdated") values (?,?,?,?, statement_timestamp(), statement_timestamp())
   refnum: 502; id: [3] Tom; email: [12] tom@blah.com; name: [9] Blah, Tom; created: 0999-12-31T23:59:00Z[Etc/UTC]; lastUpdated: 0999-12-31T23:59:00Z[Etc/UTC]; deleted: null;
   Inserted 1 records in 19.00ms
[TILDATUTORIAL.User]: insert into TILDATUTORIAL.User("refnum","id","email","name","created","lastUpdated") values (?,?,?,?, statement_timestamp(), statement_timestamp())
   refnum: 503; id: [4] Mary; email: [13] mary@blah.com; name: [10] Bleh, Mary; created: 0999-12-31T23:59:00Z[Etc/UTC]; lastUpdated: 0999-12-31T23:59:00Z[Etc/UTC]; deleted: null;
   Inserted 1 records in 2.00ms
[TILDATUTORIAL.User]: insert into TILDATUTORIAL.User("refnum","id","email","name","created","lastUpdated") values (?,?,?,?, statement_timestamp(), statement_timestamp())
   refnum: 504; id: [4] Jean; email: [13] jean@blah.com; name: [10] Blih, Jean; created: 0999-12-31T23:59:00Z[Etc/UTC]; lastUpdated: 0999-12-31T23:59:00Z[Etc/UTC]; deleted: null;
   Inserted 1 records in 2.00ms

The second time the code is run, we'd have the following logs:

    ~~ [TILDATUTORIAL.User]: insert into TILDATUTORIAL.User("refnum","id","email","name","created","lastUpdated") values (?,?,?,?, statement_timestamp(), statement_timestamp())
    ~~    refnum: 1002; id: [3] Tom; email: [12] tom@blah.com; name: [9] Blah, Tom; created: 0999-12-31T23:59:00Z[Etc/UTC]; lastUpdated: 0999-12-31T23:59:00Z[Etc/UTC]; deleted: null;
JDBC Error: No row updated or inserted: SQLState=23505, ErrorCode=0
JDBC Message: ERROR: duplicate key value violates unique constraint "user_id".  Detail: Key (id)=(Tom) already exists.
    ~~    No record Inserted  in 23.00ms
    ~~ [TILDATUTORIAL.User]: update TILDATUTORIAL.User set "id"=?,"email"=?,"name"=?,"lastUpdated"=statement_timestamp() where (TILDATUTORIAL.User."email"=?)
    ~~    refnum: 0; id: [3] Tom; email: [12] tom@blah.com; name: [9] Blah, Tom; created: null; lastUpdated: 0999-12-31T23:59:00Z[Etc/UTC]; deleted: null;
    ~~    Updated 1 records in 5.00ms
    ~~ [TILDATUTORIAL.User]: insert into TILDATUTORIAL.User("refnum","id","email","name","created","lastUpdated") values (?,?,?,?, statement_timestamp(), statement_timestamp())
    ~~    refnum: 1003; id: [4] Mary; email: [13] mary@blah.com; name: [10] Bleh, Mary; created: 0999-12-31T23:59:00Z[Etc/UTC]; lastUpdated: 0999-12-31T23:59:00Z[Etc/UTC]; deleted: null;
JDBC Error: No row updated or inserted: SQLState=23505, ErrorCode=0
JDBC Message: ERROR: duplicate key value violates unique constraint "user_id".  Detail: Key (id)=(Mary) already exists.
    ~~    No record Inserted  in 2.00ms
    ~~ [TILDATUTORIAL.User]: update TILDATUTORIAL.User set "id"=?,"email"=?,"name"=?,"lastUpdated"=statement_timestamp() where (TILDATUTORIAL.User."email"=?)
    ~~    refnum: 0; id: [4] Mary; email: [13] mary@blah.com; name: [10] Bleh, Mary; created: null; lastUpdated: 0999-12-31T23:59:00Z[Etc/UTC]; deleted: null;
    ~~    Updated 1 records in 2.00ms
    ~~ [TILDATUTORIAL.User]: insert into TILDATUTORIAL.User("refnum","id","email","name","created","lastUpdated") values (?,?,?,?, statement_timestamp(), statement_timestamp())
    ~~    refnum: 1004; id: [4] Jean; email: [13] jean@blah.com; name: [10] Blih, Jean; created: 0999-12-31T23:59:00Z[Etc/UTC]; lastUpdated: 0999-12-31T23:59:00Z[Etc/UTC]; deleted: null;
JDBC Error: No row updated or inserted: SQLState=23505, ErrorCode=0
JDBC Message: ERROR: duplicate key value violates unique constraint "user_id".  Detail: Key (id)=(Jean) already exists.
    ~~    No record Inserted  in 2.00ms
    ~~ [TILDATUTORIAL.User]: update TILDATUTORIAL.User set "id"=?,"email"=?,"name"=?,"lastUpdated"=statement_timestamp() where (TILDATUTORIAL.User."email"=?)
    ~~    refnum: 0; id: [4] Jean; email: [13] jean@blah.com; name: [10] Blih, Jean; created: null; lastUpdated: 0999-12-31T23:59:00Z[Etc/UTC]; deleted: null;
    ~~    Updated 1 records in 2.00ms

As you can see, we try to insert first, and then, since that fails, we then try to update. If we run the second code sample, where we (1) use upsert which is simpler, and (2) privilege the use case of an update first, you will then see a reversed scenario logged. We'll truncate the table and rerun a "first-time" usecase.

[TILDATUTORIAL.User]: update TILDATUTORIAL.User set "refnum"=?,"id"=?,"email"=?,"name"=?,"created"=statement_timestamp(),"lastUpdated"=statement_timestamp() where (TILDATUTORIAL.User."id"=?)
   refnum: 1502; id: [3] Tom; email: [12] tom@blah.com; name: [9] Blah, Tom; created: 0999-12-31T23:59:00Z[Etc/UTC]; lastUpdated: 0999-12-31T23:59:00Z[Etc/UTC]; deleted: null;
   No record Updated  in 6.00ms
[TILDATUTORIAL.User]: insert into TILDATUTORIAL.User("refnum","id","email","name","created","lastUpdated") values (?,?,?,?, statement_timestamp(), statement_timestamp())
   refnum: 1503; id: [3] Tom; email: [12] tom@blah.com; name: [9] Blah, Tom; created: 0999-12-31T23:59:00Z[Etc/UTC]; lastUpdated: 0999-12-31T23:59:00Z[Etc/UTC]; deleted: null;
   Inserted 1 records in 5.00ms
[TILDATUTORIAL.User]: select  TILDATUTORIAL.User."refnum", TILDATUTORIAL.User."id", TILDATUTORIAL.User."email", TILDATUTORIAL.User."name", TILDATUTORIAL.User."created", TILDATUTORIAL.User."lastUpdated", TILDATUTORIAL.User."deleted" from TILDATUTORIAL.User where (TILDATUTORIAL.User."refnum"=?)
   refnum: 1503; id: [3] Tom; email: [12] tom@blah.com; name: [9] Blah, Tom; created: 0999-12-31T23:59:00Z[Etc/UTC]; lastUpdated: 0999-12-31T23:59:00Z[Etc/UTC]; deleted: null;
   Selected 1 records in 3.00ms
[TILDATUTORIAL.User]: update TILDATUTORIAL.User set "refnum"=?,"id"=?,"email"=?,"name"=?,"created"=statement_timestamp(),"lastUpdated"=statement_timestamp() where (TILDATUTORIAL.User."id"=?)
   refnum: 1504; id: [4] Mary; email: [13] mary@blah.com; name: [10] Bleh, Mary; created: 0999-12-31T23:59:00Z[Etc/UTC]; lastUpdated: 0999-12-31T23:59:00Z[Etc/UTC]; deleted: null;
   No record Updated  in 2.00ms
[TILDATUTORIAL.User]: insert into TILDATUTORIAL.User("refnum","id","email","name","created","lastUpdated") values (?,?,?,?, statement_timestamp(), statement_timestamp())
   refnum: 1505; id: [4] Mary; email: [13] mary@blah.com; name: [10] Bleh, Mary; created: 0999-12-31T23:59:00Z[Etc/UTC]; lastUpdated: 0999-12-31T23:59:00Z[Etc/UTC]; deleted: null;
   Inserted 1 records in 2.00ms
[TILDATUTORIAL.User]: select  TILDATUTORIAL.User."refnum", TILDATUTORIAL.User."id", TILDATUTORIAL.User."email", TILDATUTORIAL.User."name", TILDATUTORIAL.User."created", TILDATUTORIAL.User."lastUpdated", TILDATUTORIAL.User."deleted" from TILDATUTORIAL.User where (TILDATUTORIAL.User."refnum"=?)
   refnum: 1505; id: [4] Mary; email: [13] mary@blah.com; name: [10] Bleh, Mary; created: 0999-12-31T23:59:00Z[Etc/UTC]; lastUpdated: 0999-12-31T23:59:00Z[Etc/UTC]; deleted: null;
   Selected 1 records in 1.00ms
[TILDATUTORIAL.User]: update TILDATUTORIAL.User set "refnum"=?,"id"=?,"email"=?,"name"=?,"created"=statement_timestamp(),"lastUpdated"=statement_timestamp() where (TILDATUTORIAL.User."id"=?)
   refnum: 1506; id: [4] Jean; email: [13] jean@blah.com; name: [10] Blih, Jean; created: 0999-12-31T23:59:00Z[Etc/UTC]; lastUpdated: 0999-12-31T23:59:00Z[Etc/UTC]; deleted: null;
   No record Updated  in 2.00ms
[TILDATUTORIAL.User]: insert into TILDATUTORIAL.User("refnum","id","email","name","created","lastUpdated") values (?,?,?,?, statement_timestamp(), statement_timestamp())
   refnum: 1507; id: [4] Jean; email: [13] jean@blah.com; name: [10] Blih, Jean; created: 0999-12-31T23:59:00Z[Etc/UTC]; lastUpdated: 0999-12-31T23:59:00Z[Etc/UTC]; deleted: null;
   Inserted 1 records in 3.00ms
[TILDATUTORIAL.User]: select  TILDATUTORIAL.User."refnum", TILDATUTORIAL.User."id", TILDATUTORIAL.User."email", TILDATUTORIAL.User."name", TILDATUTORIAL.User."created", TILDATUTORIAL.User."lastUpdated", TILDATUTORIAL.User."deleted" from TILDATUTORIAL.User where (TILDATUTORIAL.User."refnum"=?)
   refnum: 1507; id: [4] Jean; email: [13] jean@blah.com; name: [10] Blih, Jean; created: 0999-12-31T23:59:00Z[Etc/UTC]; lastUpdated: 0999-12-31T23:59:00Z[Etc/UTC]; deleted: null;
   Selected 1 records in 1.00ms

the code is attempting an update first, and as that fails since this is the first time, it will then do an insert. Then it will refresh the object with a select. If we run this again, as a second time, the initial update will succeed, so no insert.

[TILDATUTORIAL.User]: update TILDATUTORIAL.User set "refnum"=?,"id"=?,"email"=?,"name"=?,"created"=statement_timestamp(),"lastUpdated"=statement_timestamp() where (TILDATUTORIAL.User."id"=?)
   refnum: 2002; id: [3] Tom; email: [12] tom@blah.com; name: [9] Blah, Tom; created: 0999-12-31T23:59:00Z[Etc/UTC]; lastUpdated: 0999-12-31T23:59:00Z[Etc/UTC]; deleted: null;
   Updated 1 records in 7.00ms
[TILDATUTORIAL.User]: select  TILDATUTORIAL.User."refnum", TILDATUTORIAL.User."id", TILDATUTORIAL.User."email", TILDATUTORIAL.User."name", TILDATUTORIAL.User."created", TILDATUTORIAL.User."lastUpdated", TILDATUTORIAL.User."deleted" from TILDATUTORIAL.User where (TILDATUTORIAL.User."id"=?)
   refnum: 2002; id: [3] Tom; email: [12] tom@blah.com; name: [9] Blah, Tom; created: 0999-12-31T23:59:00Z[Etc/UTC]; lastUpdated: 0999-12-31T23:59:00Z[Etc/UTC]; deleted: null;
   Selected 1 records in 1.00ms
[TILDATUTORIAL.User]: update TILDATUTORIAL.User set "refnum"=?,"id"=?,"email"=?,"name"=?,"created"=statement_timestamp(),"lastUpdated"=statement_timestamp() where (TILDATUTORIAL.User."id"=?)
   refnum: 2003; id: [4] Mary; email: [13] mary@blah.com; name: [10] Bleh, Mary; created: 0999-12-31T23:59:00Z[Etc/UTC]; lastUpdated: 0999-12-31T23:59:00Z[Etc/UTC]; deleted: null;
   Updated 1 records in 3.00ms
[TILDATUTORIAL.User]: select  TILDATUTORIAL.User."refnum", TILDATUTORIAL.User."id", TILDATUTORIAL.User."email", TILDATUTORIAL.User."name", TILDATUTORIAL.User."created", TILDATUTORIAL.User."lastUpdated", TILDATUTORIAL.User."deleted" from TILDATUTORIAL.User where (TILDATUTORIAL.User."id"=?)
   refnum: 2003; id: [4] Mary; email: [13] mary@blah.com; name: [10] Bleh, Mary; created: 0999-12-31T23:59:00Z[Etc/UTC]; lastUpdated: 0999-12-31T23:59:00Z[Etc/UTC]; deleted: null;
   Selected 1 records in 1.00ms
[TILDATUTORIAL.User]: update TILDATUTORIAL.User set "refnum"=?,"id"=?,"email"=?,"name"=?,"created"=statement_timestamp(),"lastUpdated"=statement_timestamp() where (TILDATUTORIAL.User."id"=?)
   refnum: 2004; id: [4] Jean; email: [13] jean@blah.com; name: [10] Blih, Jean; created: 0999-12-31T23:59:00Z[Etc/UTC]; lastUpdated: 0999-12-31T23:59:00Z[Etc/UTC]; deleted: null;
   Updated 1 records in 3.00ms
[TILDATUTORIAL.User]: select  TILDATUTORIAL.User."refnum", TILDATUTORIAL.User."id", TILDATUTORIAL.User."email", TILDATUTORIAL.User."name", TILDATUTORIAL.User."created", TILDATUTORIAL.User."lastUpdated", TILDATUTORIAL.User."deleted" from TILDATUTORIAL.User where (TILDATUTORIAL.User."id"=?)
   refnum: 2004; id: [4] Jean; email: [13] jean@blah.com; name: [10] Blih, Jean; created: 0999-12-31T23:59:00Z[Etc/UTC]; lastUpdated: 0999-12-31T23:59:00Z[Etc/UTC]; deleted: null;
   Selected 1 records in 3.00ms
Previous Main Next
<-- Part 5 Main Part 7 -->
Clone this wiki locally