In the original Hello World example, we partitioned the HELLOWORLD table on dialect to demonstrate partitioning, which is a key concept for VoltDB. However, there are only so many languages in the world, and the words for "hello" and "world" are not likely to change frequently. In other words, the HELLOWORLD table is both small and primarily read-only.
Not all tables need to be partitioned. If a table is small and updated infrequently, it can be replicated. Copies of a replicated table are stored on every server. This means that the tables can only be updated with a multi-partition procedure (which is why you shouldn't replicate write-intensive tables). However, replicated tables can be read from any single-partitioned procedure since there is a copy available to every partition.
HELLOWORLD is an ideal candidate for replication, so we will replicate it in this iteration of the Hello World application.
USERACCOUNT, on the other hand, is write-intensive. The table is updated every time a user signs in and the record count increases as new users register with the system. Therefore, it is important that we partition this table.
The partitioning column needs to support the key access methods for the table. In the case of registered users, the table is accessed via the user's unique ID, their email address, when the user signs in. So we will define the EMAIL column as the partitioning column for the table.
The choice of partitioning column is defined in the database schema. If a table is not listed as being partitioned, it becomes a replicated table by default. So for the updated Hello World example, you can remove the PARTITION TABLE statement for the HELLOWORLD table and add one for USERACCOUNT. The updated schema contains the following PARTITION TABLE statement:
PARTITION TABLE USERACCOUNT ON COLUMN EMAIL;
For the sake of demonstration, we only need three stored procedures for our rewrite of Hello World:
Insert Language — Loads the HELLOWORLD table, just as in the original Hello World tutorial.
Register User — Creates a new USERACCOUNT record.
Sign In — Performs the bulk of the work, looking up the user, recording their sign in, and looking up the correct word for saying hello.
To load the HELLOWORLD table, we can reuse the Insert stored procedure from our original Hello World example. The only change we need to make is, because HELLOWORLD is now a replicated table, remove the PARTITION ON clause from the CREATE PROCEDURE statement that defines the Insert procedure in the schema DDL.
To add a new user to the system, the RegisterUser stored procedure needs to add the user's name, language, and their email address as the unique identifier for the USERACCOUNT table.
Creating a new record can be done with a single INSERT statement. In this way, the RegisterUser procedure is very similar to the Insert procedure for the HELLOWORLD table. The RegisterUser procedure looks like this:
CREATE PROCEDURE RegisterUser AS INSERT INTO USERACCOUNT (Email, Firstname, Lastname, Dialect) VALUES (?,?,?,?);
The difference is that RegisterUser can and should be single-partitioned so it does not unnecessarily tie up multiple partitions. Since the table is partitioned on the EMAIL column, the CREATE PROCEDURE statement should include a PARTITION ON clause like so:
CREATE PROCEDURE RegisterUser
PARTITION ON TABLE Useraccount COLUMN Email
AS INSERT INTO USERACCOUNT
(Email, Firstname, Lastname, Dialect)
VALUES (?,?,?,?);
Finally, we need a procedure to sign in the user and retrieve the word for "hello" in their native language. The key goal for this procedure, since it will be invoked more frequently than any other, is to be performant. To ensure the highest throughput, the procedure needs to be single-partitioned.
The user provides their email address as the unique ID when they log in, so we can make the procedure single-partitioned, specifying the email address as the partitioning value. Within the procedure itself we perform two actions:
Join the USERACCOUNT and HELLOWORLD tables based on the Dialect column to retrieve both the user's name and the appropriate word for "hello"
Update the user's record with the latest login timestamp.
Because this stored procedure uses two queries, we can write the stored procedure logic as a Java class. (See Using VoltDB for details on writing Java stored procedures.) We could write custom code to check the return values from the join of the two tables to ensure that an appropriate user record was found. However, VoltDB provides predefined expectations for many common query conditions. We can take advantage of one of these expectations, EXPECTS_ONE_ROW, to verify that we get the results we want. If the first query, getuser, does not return one row (for example, if no user record is found), VoltDB aborts the procedure and notifies the calling program that a rollback has occurred.
Expectations provide a way to simplify and standardize error handling in your stored procedures. See the chapter on simplifying application coding in the Using VoltDB manual for more information.
The resulting SignIn procedure is as follows:
import org.voltdb.*; public class SignIn extends VoltProcedure { public final SQLStmt getuser = new SQLStmt( "SELECT H.HELLO, U.FIRSTNAME " + "FROM USERACCOUNT AS U, HELLOWORLD AS H " + "WHERE U.EMAIL = ? AND U.DIALECT = H.DIALECT;" ); public final SQLStmt updatesignin = new SQLStmt( "UPDATE USERACCOUNT SET lastlogin=? " + "WHERE EMAIL = ?;" ); public VoltTable[] run( String id, long signintime) throws VoltAbortException { voltQueueSQL( getuser, EXPECT_ONE_ROW, id ); voltQueueSQL( updatesignin, signintime, id ); return voltExecuteSQL(); } }
We also want to declare the procedure and define how it is partitioned in the schema DDL. To do that, we add the following statement to our schema file:
CREATE PROCEDURE PARTITION ON TABLE Useraccount COLUMN Email FROM CLASS SignIn;