Java ACDP Tutorial

Querying

There is neither a query language nor a data definition language in ACDP. Simply put: ACDP's SQL is Java. This chapter shows you how to formulate and execute queries and how to change data in the database, inside or outside a database transaction.

Structural changes to the database are not an issue in this chapter. Structural changes include removing a table from the database or inserting a new column into an existing table. How to structurally change the database is shown in Section Refactoring of Chapter Maintenance.

The remaining sections of this page are

Populating the Database

To populate the sample database just invoke the run method of the Populator class.

Which of EduPL, EduDB, and Database to use?

How data stored in the database is accessed, depends on how the database is opened. If the database is opened by creating an instance of the EduPL class, then this instance is used for reading data from and writing data to the database. If, on the other hand, the database is opened directly, the central class to communicate with the database is either the domain-specific EduDB class or the non-domain-specific Database interface, depending on whether the database is opened as a strongly typed or weakly typed database.
   Path path = Paths.get("dbDir/layout");
   
   // Open database by creating EduPL instance.
   try (EduPL pl = new EduPL(path, -1, false, 0)) {
      // do something with pl
   }
   
   // Open database as strongly typed database.
   try (EduDB db = new EduDB(path, -1, false, 0)) {
      // do something with db
   }
   
   // Open database as weakly typed database.
   try (Database db = Database.open(path, -1, false, null)) {
      // do something with db
   }
Whenever possible, use the persistence layer, as it is the easiest and safest to use. This applies in particular if data is to be inserted, updated or deleted. However, the persistence layer abstracts from the underlying database tables and table rows. So, whenever there is a need for a direct access to the tables and rows, the database should be openend as a strongly typed database.

The database should only be opened as a weakly typed database if the use case can neither be implemented with the persistence layer nor by opening the database as a strongly typed database or if the use case is not domain-specific. (For example, a non-domain-specific use case is get the names of the tables of the database.)

To show the difference, let us write some code to display the name and email address of the student with number 74. (The name and email address of the student with number 74 is equal to Nigel Lee and nigel@example.com, respectively). We do not repeat the process of opening the database.

EduPL

   // pl instance of EduPL
   Student student = pl.getStudent(74);
   System.out.println(student.getName() + "  " +
                      student.getContact().getEmail());
+ Compact
+ Foolproof
+ Built-in index can be used

EduDB

   // EduDB is open
   Row row = STUDENT_TABLE.get(
                         STUDENT_TABLE.indexGet(74),
                                     NAME, CONTACT);
   String email = CONTACT_TABLE.getValue(
                           row.get(CONTACT), EMAIL);
   System.out.println(row.get(NAME) + "  " + email);
(We use static imports to avoid qualifying constants, for example STUDENT_TABLE instead of EudDB.STUDENT_TABLE or CONTACT instead of StudentTable.CONTACT.)

- Less convenient
- No enforcement of domain-specific constraints implemented in the persistence layer (not a problem in this example)
+ Built-in index can be used
+ Easy and strongly typed access to tables and columns

Database

   // db instance of Database
   Table studentTable = db.getTable("Student");
   Column<?> studentNumberColumn = studentTable.
                                   getColumn("Student Number");
   Column<?> nameColumn = studentTable.getColumn("Name");
   Column<?> contactColumn = studentTable.getColumn("Contact");
			
   Table contactTable = db.getTable("Contact");
   Column<?> emailColumn = contactTable.getColumn("Email");
			
   Row studentRow = studentTable.rows(studentNumberColumn,
                                     nameColumn, contactColumn).
        filter(row -> (int) row.get(studentNumberColumn) == 74).
        findAny().get();
			
   Row contactRow = contactTable.get((Ref) studentRow.get(
                                  contactColumn), emailColumn);
   System.out.println(studentRow.get(nameColumn) + "  " +
                                  contactRow.get(emailColumn));
- Verbose
- Cumbersome and error-prone access to tables and columns
- Must iterate Student table

Ad-hoc Queries

When it comes to formulating a query on the fly, ACDP cannot compete with SQL. This is not surprising, because SQL is an interpreted, declarative language developed with a focus on the spontaneous formulation of database queries, while Java is a compilable, general-purpose programming language. However, even if ad-hoc queries in ACDP are not quite as ad-hoc as in SQL: For a Java programmer who is practiced in querying data structures of a certain complexity, they do not pose a major challenge. And the nice thing is that no knowledge beyond Java is required to program database queries in ACDP and to process their results.

In the following, we define a query on our sample database to be an ad-hoc query if it cannot be done with the persistence layer. Therefore, we open the sample database as a strongly typed EduDB database as was shown in the previous section.

Visiting the Rows of a Table

It is possible to iterate over the rows of a table. Good to know that the order in which the rows of an unchanged table are returned by any of the iterators is always the same. In addition, ACDP supports the streams API of Java 8.

For example, displaying the names of all courses can be done in the following three ways. (The process of opening the database and the declaration of some necessary static imports are omitted.)

1. Applying a for-each loop
   for (Row row : COURSE_TABLE) {
      System.out.println(row.get(NAME));
   }
2. Getting an instance of Iterator<Row>
   for (Iterator<Row> it = COURSE_TABLE.iterator(NAME);
                                        it.hasNext();) {
      System.out.println(it.next().get(NAME));
   }
3. Using Stream<Row>
   COURSE_TABLE.rows(NAME).forEach(row ->
                     System.out.println(row.get(NAME)));
Note that the Row instances returned in the for-each loop of the first variant contain all column values, including the values for the ID and Teacher columns of the Course table. In the second and third variant the row instances only contain the values of the Name column.

More Queries

• Create a list of names of all students enrolled in the C# programming course:
   List<String> result = STUDENT_TABLE.rows(NAME, COURSES).
          filter(r -> Arrays.stream(r.get(COURSES)).
               filter(ref -> COURSE_TABLE.getValue(ref,
                     CourseTable.ID).equals("CSHARP")).
                     findAny().isPresent()).
          map(r -> r.get(NAME)).collect(toList());
• Create a map with keys being the names of the teachers and values being the names of the students attending at least one of the courses instructed by the teacher:
   Map<String, List<String>> result = new HashMap<>();
   for (Row tRow : TEACHER_TABLE) {
      List<Ref> l1 = Arrays.asList(tRow.get(
                              TeacherTable.COURSES));
      List<String> sList = new ArrayList<>();
      for (Row sRow : STUDENT_TABLE) {
         List<Ref> l2 = Arrays.asList(sRow.get(
                              StudentTable.COURSES));
         if (l1.stream().filter(l2::contains).
                             findAny().isPresent()) {
            sList.add(sRow.get(StudentTable.NAME));
         }
      }
      result.put(tRow.get(TeacherTable.NAME), sList);
   }
Of course, many more queries are conceivable.

Data Changes

To ensure that no domain-specific constraints are violated, data changes should only be made using the persistence layer. For example, deleting teacher "Jane Smith" by calling
   // pl instance of EduPL
   pl.deleteTeacher("Jane Smith");
not only causes the data of Jane Smith to be removed from the Teacher table, but also removes her contact details from the Contact table. Furthermore, the values in the Teacher column of the Course table are overwritten with the null value for the courses that "Jane Smith" has instructed.

Here is how to change the email address of student 74 to n.lee@example.com and add the course LSSGBP to his list of courses:

   Student student = pl.getStudent(74);
   Contact contact = student.getContact();
   List<String> courses = student.getCourses();
   
   contact.setEmail("n.lee@example.com");
   courses.add("LSSGBP");
         
   pl.persistStudent(74, student.getName(), contact, courses);
Sometimes, however, changes to the data need to be made ad-hoc. Let us assume, for example, that the system for assigning postal codes has been changed: All postal codes starting with 8 are to start with 80. So the postal code 84672 should now be 804672. The persistence layer doesn't provide direct access to the contact details. Hence, we open the database as a strongly typed EduDB database and call
   EduDB.CONTACT_TABLE.updateAllChangeValues(
           ContactTable.POSTAL_CODE,
           pc -> pc / 10000 == 8 ? 800000 + pc % 80000 : pc);

Transactions, Units, Read Zones

So far, all database operations were executed outside a transaction. We tacitly assumed that
  1. A possible destruction of the database's integrity is accepted. (This may be due to a write operation that catastrophically fails.)
  2. A possible violation of domain-specific constraints is accepted. (This may be because a sequence of write operations could not be completed due to an unexpected exceptional situation.)
  3. No other clients use the database at the same time.
To execute a database operation within a transaction it must be executed either within a unit or a read zone. Database operations in a read zone must be read-only.

For example, using a unit, the code snippet

   // pl instance of EduPL
   try (Unit u = pl.openUnit()) {
      pl.deleteCourse("CFR");
      u.commit();
   }
removes the CFR course from the database. (The CFR course can be deleted without provoking a DeleteConstraintException because it is neither referenced by a student nor a teacher.) If u.commit() had been missing, the deletion process would have been undone when the unit was closed. For example, the following code does not remove the other non-referenced courses from the database, because u.commit() is missing:
   // Don't forget u.commit() after the last write operation.
   try (Unit u = pl.openUnit()) {
      pl.deleteCourse("CDPC");
      pl.deleteCourse("HDJ");
      pl.deleteCourse("MBS");
   }
And
   // Don't forget u.commit() after the last write operation.
   try (Unit u = pl.openUnit()) {
      pl.deleteCourse("CDPC");
      pl.deleteCourse("HDJ");
      u.commit();
      pl.deleteCourse("MBS");
   }
just removes the CDPC and HDJ courses but not the MBS course from the database.

Of course, the integrity of the database is maintained even if a call to u.commit() is missing. However, one must be aware that if database changes are rolled back, other things can get into disarray: In the example above, the index of the Course table will miss the entry for the MBS course, although that course still resides in the database. This is because the removal of the MBS course from the index has not been undone.

It is a good idea to treat any rollback situation as a bug.

Two database write operations that are started in two different threads are never executed concurrently, regardless of whether they run inside or outside a unit. Write operations are therefore synchronized. On the other hand, read operations are not synchronized. They can fail if a write operation is currently running in a different thread. To synchronize read operations, use a read zone. Read operations within a read zone are never executed concurrently with a write operation, but they are executed concurrently with read operations inside open read zones in different threads.

In Section Visiting the Rows of a Table from above we displayed the name of all courses in the Course table. Let's do the same thing but this time inside a read zone:

   try (ReadZone rz = db.openReadZone()) {
      COURSE_TABLE.rows(NAME).forEach(row ->
                     System.out.println(row.get(NAME)));
   }
We could execute this operation within a unit, which would, however, have the consequence that all program instructions within read zones in other threads would be blocked until this operation was completed.

Learn more about how transactions are implemented in ACDP by reading the sections Units, Read Zones, and Durability in the Javadoc description of the Database interface.
Back To Top