Automatic implementation of a Repository interface for a given domain object.
- i41d - https://classroom.github.com/a/rgXxQMNw
- i42d - https://classroom.github.com/a/S81A4PRI
- i43d - https://classroom.github.com/a/T4Dpgvhr
- i44d - https://classroom.github.com/a/6Dl1CfG4
- i41n - https://classroom.github.com/a/oALcsFpm
Deadlines:
This library provides an automatic implementation of a Repository
interface for a given domain object, inspired by the repository
pattern used in Spring Data JPA.
The repository pattern definition by Martin Fowler
states:
Mediates between the domain and data mapping layers using a collection-like interface for accessing domain objects
interface Repository<K, T> {
fun getById(id: K): T? // Find an entity by its ID
fun getAll(): List<T> // Retrieve all entities
fun update(entity: T) // Update an existing entity
fun deleteById(id: K) // Delete an entity by its ID
}The jdbcRepo has some limitations and only supports a domain model design with the following constraints:
- Support for SQL insert operations will be implemented in the second part of the assignment through the dynamic generation of bytecode.
- Only supports associations with single multiplicity.
This means that properties cannot use data structures like
List,Collection,Iterable, etc., to hold multiple items. - The object graph should not contain cyclic references. For example, a domain class cannot have a property of the same type as its owning class.
Example of a supported domain model:
---
config:
theme: mc
look: classic
layout: dagre
---
classDiagram
direction LR
ChannelType <-- Channel : type
Channel <-- Member : channel
Channel <-- Message : channel
Permission <-- Member : permission
Member --> User : user
Message --> User : user
Provide an implementation of the RepositoryReflect class in the jdbcRepoLib
module using the Kotlin Reflection API.
The unit tests in the jdbcRepoLib module use a PostgreSQL database running in
a Docker container. The Docker setup and database initialization scripts are
located in the /src/test/docker and /src/test/sql directories, respectively.
The test task in build.gradle is configured to automatically start the
Docker container before running the unit tests and stop it afterward.
To run the unit tests, you must install Docker
Desktop.
Your implementation should be developed incrementally, addressing the requirements of each of the following unit tests:
UserRepositoryTest– Supports entities without associations and no enum type handling.ChannelRepositoryTest– Adds support for enum types.MessageRepositoryTest– Introduces support for associations.
You may use the ad-hoc implementation of ChannelRepositoryJdbc in the
chatDomainModel module as a reference for implementing RepositoryReflect.
Run the micro-benchmarking from jdbcRepoMicrobench and register the slowdown
of the RepositoryReflect implementation compared to the ad-hoc ChannelRepositoryJdbc.
Below is an example of the expected slowdown, approximately 3×, in the
RepositoryReflect implementation when retrieving all channels using an
in-memory mock JDBC implementation with five Channel instances.
Benchmark Mode Cnt Score Error Units
benchRepositoryJdbcGetAllChannels avgt 4 938.291 ± 29.267 ns/op
benchRepositoryReflectGetAllChannels avgt 4 3143.358 ± 904.489 ns/op
NOTE: Before proceeding, ensure that the unit tests of jdbcRepoMicrobench pass without errors.
To run the benchmark on you local machine just run:
./gradlew jmhJar
And then:
java -jar jdbcRepoMicrobench/build/libs/jdbcRepoMicrobench-jmh.jar -i 4 -wi 4 -f 1 -r 2 -w 2
-i4 iterations-wi4 warmup iterations-f1 fork-r2 run each iteration for 2 seconds-w2 run each warmup iteration for 2 seconds.
Create your own example of a domain model and a corresponding PostgreSQL
database in src/test/sql within the jdbcRepoLib module. Then, write unit
tests to verify the correctness of your RepositoryReflect implementation using
this domain.
Your domain should include at least two associations and two enum types. You may base it on the domain model specified by one of your group members in the first assignment of Jsonoy.
Following up on Assignment 1 developed in the jdbcRepo library, it is
intended to complete the implementation of the buildRepositoryByteArray() in
RepositoryDynamic.kt file which makes a dynamic implementation of a class
implementing the interface Repository, but that DOES NOT use reflection to
instantiate the domain class (e.g. User, Channel, etc).
NOTE that reflection will continue to be used to read metadata, only ceasing
to be used in operations such as ctor.callBy(...). The instantiation of a
domain class will now be done directly based on code generated at runtime
through the
Class-File API.
NOTE you will need JDK 22 to use the Class-File API.
The following Figure presents an example of the use of RepositoryReflect from
Assignment 1, where each instance of RepositoryReflect is associated with an
instance of KClass to manage a specific domain class.
Now, in Assignment 2, you will have a different Repository class (e.g.,
RepositoryDynUser, RepositoryDynChannel, etc.) for each domain class, rather
than using the same type of Repository to manage all classes (i.e.
RepositoryReflect).
These repositories are generated at runtime (i.e., dynamically) with the support
of the Class-File API.
For each domain entity class the loadDynamicRepository() creates a specific
implementation of a new class named RepositoryDyn<Entity Name>, e.g.
RepositoryDynChannel.
Each of these classes inherits from RepositoryReflect and simply overrides
the method responsible for mapping a ResultSet into an entity object—such
as Channel, Message, or others—specifically the
open fun mapRowToEntity(rs: ResultSet): T method. To that end, ENSURE the following:
- The
RepositoryReflectclass is marked asopen. - The method
mapRowToEntity, or its equivalent, is declared asopen. - The
connectionproperty is declared asprotected.
Update your jdbcRepo project to match the configurations introduced in
commit 3cb3b53,
ensuring support for Java 22 and the new Class-File API.
This includes updating the following files:
gradle-wrapper.propertiesbuild.gradlein each module (IGNORE the modification of docker path)
After making these changes, verify that the project still builds successfully
by running ./gradlew build.
Your implementation should be developed incrementally, fulfilling the requirements of each step without skipping any.
Create a separate commit for each of the following changes, and include the requirement order number in the commit message.
-
Update the
UserRepositoryTestto support different implementations of theRepositoryinterface, following the changes introduced in commit 6726b03.
Add the following dependency to enable parameterized tests:
testImplementation("org.junit.jupiter:junit-jupiter-params:5.10.2").
After making these changes, verify that the project still builds and tests are passing successfully -
Copy the
RepositoryDynamicfile into your project. Review its structure and carefully analyze each of its functions. Add Javadoc comments to each member of theRepositoryDynamicfile, clearly explaining its behavior and purpose. -
Implement the
buildRepositoryByteArray()function:- This function generates a byte array representing a dynamically created
class that extends
RepositoryReflect, and then saves it to the corresponding class file. - Ensure that the
RepositoryReflectclass is marked asopen. - The dynamically created class must have a constructor that matches the base class constructor and calls the base constructor with the required arguments.
- Do not override any methods in this step.
- In
UserRepositoryTestadd an instance ofRepository<Long, User>, created usingloadDynamicRepo(connection, User::class), to the repositories returned by therepositories()function. - Verify that all tests pass and confirm that the
RepositoryDynUser.classfile has been created. Open the resulting class file in IntelliJ and check the decompiled code.
- This function generates a byte array representing a dynamically created
class that extends
-
Override the
mapRowToEntitymethod in the dynamically generated class, or any other method responsible for mapping aResultSetinto an instance of the entity classT.
Note: Ensure the base method inRepositoryReflectis declared asopen.
In this step, handle only properties whose types are primitives,String, orjava.sql.Date. After overriding, verify thatUserRepositoryTeststill passes. -
Update
ChannelRepositoryTestto use a parameterized test, as in step 1, covering both reflective and dynamic repository implementations.
Ensure that the overriddenmapRowToEntitymethod correctly handles properties of enum types.
Confirm that theRepositoryDynChannel.classfile has been created.
Open the resulting class file in IntelliJ and check the decompiled code. -
Update
MessageRepositoryTestto use a parameterized test, as in step 1, covering both reflective and dynamic repository implementations.
Ensure that the overriddenmapRowToEntitymethod correctly supports associations between domain classes. Remember, we need access to theconnectionproperty to be passed to the auxiliary instance of the repository. Confirm that theRepositoryDynMessage.classfile has been created.
Open the resulting class file in IntelliJ and check the decompiled code. -
Update your tests from part 1.3 of Assignment 1 to use parameterized tests, following the pattern from step 1.
Make sure to cover both reflective and dynamic repository implementations, and verify that all tests pass.
Update the jdbcRepoMicrobench module to include performance measurements for a
dynamically generated repository.
Verify if its performance is similar to the baseline.
Implement a performance test for an entity from your domain in part 1.3 of Assignment 1 that maintains associations with other entities.
Modify FakePreparedStatement and FakeResultSet to support this test.
Compare the performance across the three approaches: baseline, reflective, and dynamic.
OPTIONAL: Improve the performance of the dynamic repository by adding instance fields that hold instances of auxiliary repositories for related entities.
Add support for the SQL INSERT statement. To achieve this, create an interface
extending Repository and define an insert method annotated with @Insert.
This method should be implemented dynamically.
The updated loadDynamicRepo should return an instance of a dynamically created
class that extends RepositoryReflect and implements the given interface, e.g.:
UserRepository:
val repo: UserRepository = loadDynamicRepo(connection, User::class, UserRepository::class)
interface UserRepository : Repository<Long, User> {
@Insert
fun insert(
name: String,
email: String,
birthdate: Date,
): User
}Create a new implementation of buildRepositoryByteArray() that generates a new
class from scratch. This class should not inherit from RepositoryReflect,
but instead provide dynamic implementations of methods, replacing reflection.
Note: You do not need to implement all methods dynamically. You may use an
auxiliary abstract base class that provides the skeleton for the main methods
(e.g., update, delete, etc.). Delegate the implementation of the specific
methods to hook methods (abstract methods) that will be implemented dynamically
by the derived class.
In this assignment, you should update the repository implementation to support
building and executing lazy SQL queries through the new findAll() method
of the Repository interface:
interface Repository<K, T> {
...
fun findAll(): Queryable<T>
}To achieve this, allow query clauses to be added incrementally, producing new query objects based on existing ones. The actual SQL execution should only happen when the result is iterated.
In the following example, note two important aspects:
- You can add additional clauses to an existing query, which produces a new query.
- The SQL statement is only executed when you start iterating over the result
(e.g., using
forEach). At that moment, the query will reflect any changes made to the database in the meantime, such as newly inserted entries.
val repository: Repository<String, Channel> = ...
val channelsPublicAndReadOnly =
repository
.findAll()
.whereEquals(Channel::type, ChannelType.PUBLIC)
.whereEquals(Channel::isReadOnly, true)
.iterator()
// Insert a new public and read-only channel before iterating over the result
ChannelRepositoryJdbc(connection).insert(
Channel("Surf", ChannelType.PUBLIC, System.currentTimeMillis(), false, 400, 50, true, 0L),
)
// The newly inserted channel will appear during iteration, even though the query was defined earlier
assertEquals("Support", channelsPublicAndReadOnly.next().name)
assertEquals("Surf", channelsPublicAndReadOnly.next().name)
assertFalse { channelsPublicAndReadOnly.hasNext() }This new behavior is defined by the Queryable interface shown in the following
listing.
Note that whereEquals and orderBy can be chained in any order to
build a new query.
Also note that Queryable implements the Sequence interface.
"Unlike collections, sequences don't contain elements, they produce them while
iterating."
interface Queryable<T> : Sequence<T> {
fun <V> whereEquals(prop: KProperty1<T, V>, value: V): Queryable<T>
fun <V> orderBy(prop: KProperty1<T, V>): Queryable<T>
}The findAll() implementation should be provided in RepositoryReflect, making
it available to any subclass and ensuring consistent behavior in dynamically
generated repositories as well.
The method should be implemented following the approach below, where all
query-building logic is encapsulated in an auxiliary class named
QueryableBuilder, which you must implement as part of this assignment.
The QueryableBuilder can implement the iterator protocol either by
explicitly implementing the Iterator interface or by using the sequence
generator function.
You can optionally implement both approaches.
Note that RepositoryReflect should provide to the QueryableBuilder any
information necessary to support this functionality, such as metadata or helper
functions.
In the following example, properties contains all the necessary information
about the properties of the given entity T.
open class RepositoryReflect<K : Any, T : Any>(
protected val connection: Connection,
private val domainKlass: KClass<T>,
) : Repository<K, T> {
...
override fun findAll(): Queryable<T> {
val sql = "SELECT ... FROM ..."
return QueryableBuilder(connection, sql, properties, ::mapRowToEntity)
}
}You must also implement unit tests to verify the correct behavior of
whereEquals and orderBy, including their lazy evaluation semantics.
You should ensure that closeable resources, such as PreparedStatement and
ResultSet, are automatically closed when iteration over the returned result
reaches the end.
