Elaboration
In ChapterOpening, we opened and closed a database without having done anything useful with it. We now could populate the database and carry out some queries right away. In this chapter, however, we program some key aspects of a robust persistence layer for a multilayered application. This requires the programming of a few additions to the database classes.
The persistence layer to be created has the following characteristics:
- It enforces compliance with domain-specific constraints.
- The return values as well as the parameters of the methods being part of the user interface are strongly typed.
- The user interface includes domain-specific DAOs (Data Access Objects).
The remaining sections of this page are
Readers who want to see ACDP in action now, can skip this chapter and
move on to Chapter Querying
.
Sourceslists the source code of all classes used in this tutorial.
The EduPL
Class
The persistence layer should cover at least the basic functionality of a
persistent storage: Insert, Update, Delete and Get.
For this purpose, we assign a DAO to each of the entities Contact, Course,
Student, and Teacher and declare the following methods:
void persistCourse(String id, String name)
void persistStudent(int studentNumber, String name,
Contact contact, List<String> courses)
void persistTeacher(String name, BigInteger salary,
Contact contact, List<String> courses)
void deleteCourse(String id)
void deleteStudent(int studentNumber)
void deleteTeacher(String name)
Course getCourse(String id)
Student getStudent(int studentNumber)
Teacher getTeacher(String name)
No methods are defined for dealing directyl with contacts, because according
to the UML diagram, the Contact entity is
entirely managed by the Student and Teacher entities.
We could add these methods to the EduDB
database class which we last
saw in Section
Preparing the Database Class
of
Chapter Creation
.
However, we prefer a clear separation between the functionality of the
database and the functionality of the persistence layer.
That is why we declare these methods in a new class, the
EduPL
class, where
PL
is an abbreviation for Persistence Layer
.
- It is assumed that the course
id
, thestudentNumber
and the teachername
act as the primary keys for the Course, Student and Teacher entities, respectively. - A
persist
method works as an insert method if the database does not yet contain an entry with the specified value for the primary key. Otherwise, it works as an update method. - The
persistCourse
method has noteacher
parameter: The link from a course to a teacher is controlled by thecourses
parameter of thepersistTeacher
method. -
The
Contact
DAO class just collects the details of a contact. We included thecheckEmail
method that checks if an arbitrary text is a valid email address. It would be a good idea to define such validation methods for the other contact-details as well and call them from within the constructor and setter methods. -
Both DAO classes
Student
andTeacher
are derived from thePerson
class because students and teachers have common attributes: Besides a name, both have a contact and a list of courses. Note that aPerson
object does not store the contact or courses directly, but only links to the contact and courses. Technically, a link in ACDP is a row reference, that is, a reference that directly references a specific row in a table. ThePerson
class stores the row reference of the contact in thecontactRef
variable and the row references of the courses in thecourseRefs
variable. Only when thegetContact
and thegetCourses
methods are called is the data in the referenced rows loaded from the Contact and Course table, respectively. The same mechanism is applied for the link to the teacher in theCourse
DAO class. Note that all contact details are loaded but only the values for the primary keys of the Course and Teacher entities which are the courseid
and the teachername
, respectively. -
ACDP does not allow dangling row references.
A dangling row reference would arise if it were possible to delete a row
in a table that is referenced by another row.
ACDP therefore prevents the deletion of a referenced row and throws a
DeleteConstraintException
instead. Since courses can be referenced by both, students and teachers, it is not possible to delete a course from the database as long as at least one student or teacher references the course. Fine, but what about removing a teacher from the database? The UML diagram shows that the entities Course and Teacher can reference each other. Therefore, without further action no teacher could be removed from the database that references a course that in turn references the teacher. The solution is to delete the references to this teacher in all courses of the Course table before this teacher itself is deleted in the Teacher table. For details, see the part of the source code in thedeleteTeacher
method which is commented withRelease link from course to teacher
. (Releasing a link is done by setting the row reference tonull
.) -
The source code of the methods dealing with students or teachers includes
the persistence management of contacts.
For example, inserting a student into the Student table includes inserting
that student's contact details into the Contact table.
When a contact is updated, it is checked which contact details have
actually changed compared to the stored contact details and only those
that have actually changed are updated, see the source code of the
updateContact
method. - Contacts do not have an independent identity. It is therefore not necessary to define a primary key for contacts. In a relational database, an artificial primary key for contacts would have to be defined.
-
As we will see in the next section, an index is maintained for
each of the Course, Student and Teacher tables to quickly determine
whether a certain course, a certain student or a certain teacher is stored
in the database and, if so, in which row it is located within the
respective table.
The index therefore maps the value for a primary key to a row reference.
The corresponding
indexGet
method defined in theIdTable
class is invoked in almost all public methods of theEduPL
class. More about indexes and theIdTable
class in the following sections.
EduPL
class has two
additional methods openUnit()
and openReadZone()
.
They are related to database transactions and are explained in Chapter Querying.
The rest of this section delves deeper into the source code by taking a
closer look at the getContact
method of the
Person
class as an example.
public final Contact getContact() {
final Row row = CONTACT_TABLE.get(contactRef);
return new Contact(row.get(PHONE_NUMBER), row.get(EMAIL), row.get(STREET),
row.get(CITY), row.get(STATE),
row.get(POSTAL_CODE), row.get(COUNTRY));
}
The first line of the method's body reads the row referenced by the
contactRef
variable from the Contact table.
The following lines extract the individual column values from the row and
create and return a new
Contact
DAO.
The parameters of the constructor of the Contact
class are
strongly typed.
For example, the postalCode
parameter is not simply of type
Object
, but of the primitive type int
.
The interesting thing is that the column value for the postal code does not
have to be cast to int
since row.get(POSTAL_CODE)
returns an instance of the Integer
class which is internally
converted by the JVM to an int
value.
In fact, the get(Column)
method of the Row
interface is declared as
<T> T get(Column<T> column)
Since the POSTAL_CODE
column is of type
Column<Integer>
, the type parameter T
is replaced by the compiler with the type argument Integer
.
If the concrete type of the POSTAL_CODE
column were to be
changed later, for example from Integer
to
String
, the compiler would immediately detect an error
wherever this column value was assigned to an int
variable.
Strong typing reduces the error-proneness of program code and makes it
more robust against future changes.
Elaborating the Table Classes
We last saw the table classesContactTable
,
StudentTable
,
CourseTable
, and
TeacherTable
in Section
Preparing the Table Classesof Chapter
Creation. They all were derived from the
CustomTable
abstract class.
We do not change these classes anymore with the exception that we place the
new abstract IdTable
class
between the CustomTable
class and the StudentTable
,
CourseTable
, and TeacherTable
classes:
CustomTable
class are not declared with the final
modifier.
Concrete table classes can overwrite these methods for various purposes,
such as to restrict their use or, as shown below, to update an index.
For our sample database, we assume that the domain-specific validity checks
of values, for example if a certain integer value is actually a valid postal
code, are already programmed in the DAO classes, so that we don't have to
worry about this in the table classes.
An ID table, that is technically a subclass of the IdTable
class, maintains an index of type Map<Object, Ref>
that
maps an object to a row reference.
The index is used to map a value for the primary key to a row reference, so
that for a particular value of the primary key the remaining column values
can be quickly loaded from the table without having to perform a costly
search within the table.
The IdTable
class assumes that the first column of the ID table
contains the values for the primary key.
IdTable.indexGet
method.
However, there is no direct way for changing the index.
The index is changed on the fly every time a student, course or teacher is
inserted into or deleted from the respective ID table, see the
IdTable.insert
and the IdTable.delete(Object)
methods.
Note that the IdTable.insert
method overrides the
CustomTable.insert
method.
Furthermore, the IdTable.delete(Ref)
method overrides the
CustomTable.delete(Ref)
method by throwing an
UnsupportedOperationException
.
It is therefore not possible to insert a row into or to delete a row from an
ID table without the index being updated.
More about Indexes
As seen in the source code of the constructor of the EduDB
class, the index for each
ID table is completely loaded into main memory.
Creating the indexes as part of opening the database works well because ACDP
iterates very efficiently over the rows of a table.
However, keeping the indexes in main memory may not be feasible.
In such cases, other strategies may be appropriate, for example using
MVStore of the
H2 Database Engine.