Configurable SQL queries

    While the verticle will turn the previously hard-coded values to configuration parameters, we will also go a step further by loading the SQL queries from a properties file.

    The queries will be loaded from a file passed as a configuration parameter or from a default resource if none is being provided. The advantage of this approach is that the verticle can adapt both to different JDBC drivers and SQL dialects.

    The verticle class preamble consists mainly of configuration key definitions:

    SQL queries are being stored in a properties file, with the default ones for HSQLDB being located in :

    1. create-pages-table=create table if not exists Pages (Id integer identity primary key, Name varchar(255) unique, Content clob)
    2. get-page=select Id, Content from Pages where Name = ?
    3. create-page=insert into Pages values (NULL, ?, ?)
    4. save-page=update Pages set Content = ? where Id = ?
    5. all-pages=select Name from Pages
    6. delete-page=delete from Pages where Id = ?

    The following code from the WikiDatabaseVerticle class loads the SQL queries from a file, and make them available from a map:

    1. private JDBCClient dbClient;
    2. @Override
    3. public void start(Promise<Void> promise) throws Exception {
    4. /*
    5. * Note: this uses blocking APIs, but data is small...
    6. */
    7. loadSqlQueries(); (1)
    8. dbClient = JDBCClient.createShared(vertx, new JsonObject()
    9. .put("url", config().getString(CONFIG_WIKIDB_JDBC_URL, "jdbc:hsqldb:file:db/wiki"))
    10. .put("driver_class", config().getString(CONFIG_WIKIDB_JDBC_DRIVER_CLASS, "org.hsqldb.jdbcDriver"))
    11. .put("max_pool_size", config().getInteger(CONFIG_WIKIDB_JDBC_MAX_POOL_SIZE, 30)));
    12. dbClient.getConnection(ar -> {
    13. if (ar.failed()) {
    14. LOGGER.error("Could not open a database connection", ar.cause());
    15. promise.fail(ar.cause());
    16. } else {
    17. SQLConnection connection = ar.result();
    18. connection.execute(sqlQueries.get(SqlQuery.CREATE_PAGES_TABLE), create -> { (2)
    19. connection.close();
    20. if (create.failed()) {
    21. LOGGER.error("Database preparation error", create.cause());
    22. promise.fail(create.cause());
    23. } else {
    24. vertx.eventBus().consumer(config().getString(CONFIG_WIKIDB_QUEUE, "wikidb.queue"), this::onMessage); (3)
    25. promise.complete();
    26. }
    27. });
    28. }
    29. });
    30. }
    1. Interestingly we break an important principle in Vert.x which is to avoid blocking APIs, but since there are no asynchronous APIs for accessing resources on the classpath our options are limited. We could use the Vert.x executeBlocking method to offload the blocking I/O operations from the event loop to a worker thread, but since the data is very small there is no obvious benefit in doing so.

    2. Here is an example of using SQL queries.

    3. The consumer method registers an event bus destination handler.

    Dispatching requests

    The event bus message handler is the onMessage method:

    We defined a ErrorCodes enumeration for errors, which we use to report back to the message sender. To do so, the fail method of the Message class provides a convenient shortcut to reply with an error, and the original message sender gets a failed AsyncResult.

    Reducing the JDBC client boilerplate

    1. retrieve a connection,

    2. perform requests,

    3. release the connection.

    This leads to code where lots of error processing needs to happen for each asynchronous operation, as in:

    1. dbClient.getConnection(car -> {
    2. if (car.succeeded()) {
    3. connection.query(sqlQueries.get(SqlQuery.ALL_PAGES), res -> {
    4. connection.close();
    5. List<String> pages = res.result()
    6. .getResults()
    7. .stream()
    8. .map(json -> json.getString(0))
    9. .sorted()
    10. .collect(Collectors.toList());
    11. message.reply(new JsonObject().put("pages", new JsonArray(pages)));
    12. } else {
    13. reportQueryError(message, res.cause());
    14. }
    15. });
    16. } else {
    17. reportQueryError(message, car.cause());
    18. }
    19. });

    Starting from Vert.x 3.5.0, the JDBC client now supports one-shot operations where a connection is being acquired to do a SQL operation, then released internally. The same code as above now reduces to:

    The rest of the class consists of private methods called when onMessage dispatches incoming messages:

    1. private void fetchAllPages(Message<JsonObject> message) {
    2. dbClient.query(sqlQueries.get(SqlQuery.ALL_PAGES), res -> {
    3. if (res.succeeded()) {
    4. List<String> pages = res.result()
    5. .getResults()
    6. .stream()
    7. .map(json -> json.getString(0))
    8. .sorted()
    9. .collect(Collectors.toList());
    10. message.reply(new JsonObject().put("pages", new JsonArray(pages)));
    11. } else {
    12. reportQueryError(message, res.cause());
    13. }
    14. });
    15. }
    16. private void fetchPage(Message<JsonObject> message) {
    17. String requestedPage = message.body().getString("page");
    18. JsonArray params = new JsonArray().add(requestedPage);
    19. dbClient.queryWithParams(sqlQueries.get(SqlQuery.GET_PAGE), params, fetch -> {
    20. if (fetch.succeeded()) {
    21. JsonObject response = new JsonObject();
    22. ResultSet resultSet = fetch.result();
    23. if (resultSet.getNumRows() == 0) {
    24. response.put("found", false);
    25. } else {
    26. response.put("found", true);
    27. JsonArray row = resultSet.getResults().get(0);
    28. response.put("id", row.getInteger(0));
    29. response.put("rawContent", row.getString(1));
    30. }
    31. message.reply(response);
    32. reportQueryError(message, fetch.cause());
    33. }
    34. });
    35. }
    36. private void createPage(Message<JsonObject> message) {
    37. JsonObject request = message.body();
    38. JsonArray data = new JsonArray()
    39. .add(request.getString("title"))
    40. .add(request.getString("markdown"));
    41. dbClient.updateWithParams(sqlQueries.get(SqlQuery.CREATE_PAGE), data, res -> {
    42. if (res.succeeded()) {
    43. message.reply("ok");
    44. } else {
    45. reportQueryError(message, res.cause());
    46. }
    47. });
    48. }
    49. private void savePage(Message<JsonObject> message) {
    50. JsonObject request = message.body();
    51. JsonArray data = new JsonArray()
    52. .add(request.getString("markdown"))
    53. .add(request.getString("id"));
    54. dbClient.updateWithParams(sqlQueries.get(SqlQuery.SAVE_PAGE), data, res -> {
    55. if (res.succeeded()) {
    56. message.reply("ok");
    57. } else {
    58. reportQueryError(message, res.cause());
    59. }
    60. });
    61. }
    62. private void deletePage(Message<JsonObject> message) {
    63. JsonArray data = new JsonArray().add(message.body().getString("id"));
    64. dbClient.updateWithParams(sqlQueries.get(SqlQuery.DELETE_PAGE), data, res -> {
    65. if (res.succeeded()) {
    66. message.reply("ok");
    67. } else {
    68. reportQueryError(message, res.cause());
    69. }
    70. });
    71. }
    72. private void reportQueryError(Message<JsonObject> message, Throwable cause) {
    73. LOGGER.error("Database query error", cause);
    74. }