-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
6 changed files
with
400 additions
and
312 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,54 +1,39 @@ | ||
# Booking Service Demo | ||
|
||
This project uses Kotlin and Quarkus, the Supersonic Subatomic Framework. | ||
This project is intended for demonstration purposes. It proposes a [Solution](./solution) to an example [Problem](./problem). | ||
|
||
The application is a GraphQL API writen in Kotlin and Quarkus, the Supersonic Subatomic Framework. It uses Postgres for persistence. | ||
|
||
If you want to learn more about Quarkus, please visit its website: <https://quarkus.io/>. | ||
|
||
To learn more about this project and try it out, continue reading. | ||
|
||
Thank you for your interest. Feel free to add questions, comments and suggestions [here](https://github.com/bkuberek/kotlin-quarkus-booking-service-demo/issues). | ||
|
||
## Table of Contents | ||
|
||
1. [Requirements](./requirements.md) | ||
2. [Running the Application](./running.md) | ||
3. [Using the API](./api.md) | ||
4. [Notes](./notes.md) | ||
## Documentation | ||
|
||
1. [Problem](./problem) | ||
2. [Solution](./solution) | ||
3. [Running the Application](./running.md) | ||
4. [Using the API](./api.md) | ||
5. [Notes](./notes.md) | ||
|
||
|
||
## Project Scaffolding | ||
|
||
This project was initialized using the command: | ||
|
||
```shell | ||
quarkus create app com.nelo:reservations-service --extension='kotlin,rest-jackson,quarkus-smallrye-graphql,quarkus-jdbc-postgresql,quarkus-hibernate-orm-panache,quarkus-liquibase' | ||
quarkus create app com.bkuberek:booking-service --extension='kotlin,rest-jackson,quarkus-smallrye-graphql,quarkus-jdbc-postgresql,quarkus-jdbi,quarkus-liquibase' | ||
``` | ||
|
||
## Related Guides | ||
|
||
- Kotlin ([guide](https://quarkus.io/guides/kotlin)): Write your services in Kotlin | ||
- REST Jackson ([guide](https://quarkus.io/guides/rest#json-serialisation)): Jackson serialization support for Quarkus REST. This extension is not compatible with the quarkus-resteasy extension, or any of the extensions that depend on it | ||
- SmallRye GraphQL ([guide](https://quarkus.io/guides/smallrye-graphql)): Create GraphQL Endpoints using the code-first approach from MicroProfile GraphQL | ||
- JDBC Driver - PostgreSQL ([guide](https://quarkus.io/guides/datasource)): Connect to the PostgreSQL database via JDBC | ||
- Hibernate ORM with Panache ([guide](https://quarkus.io/guides/hibernate-orm-panache)): Simplify your persistence code for Hibernate ORM via the active record or the repository pattern | ||
- Liquibase ([guide](https://quarkus.io/guides/liquibase)): Handle your database schema migrations with Liquibase | ||
|
||
|
||
## Provided Code | ||
|
||
### Hibernate ORM | ||
|
||
Create your first JPA entity | ||
|
||
[Related guide section...](https://quarkus.io/guides/hibernate-orm) | ||
|
||
[Related Hibernate with Panache section...](https://quarkus.io/guides/hibernate-orm-panache) | ||
|
||
### REST | ||
|
||
Easily start your REST Web Services | ||
|
||
[Related guide section...](https://quarkus.io/guides/getting-started-reactive#reactive-jax-rs-resources) | ||
|
||
### SmallRye GraphQL | ||
|
||
Start coding with this Hello GraphQL Query | ||
|
||
[Related guide section...](https://quarkus.io/guides/smallrye-graphql) | ||
- Kotlin ([guide](https://quarkus.io/guides/kotlin)): Write your services in Kotlin. | ||
- REST Jackson ([guide](https://quarkus.io/guides/rest#json-serialisation)): Jackson serialization support for Quarkus REST. This extension is not compatible with the quarkus-resteasy extension, or any of the extensions that depend on it. | ||
- SmallRye GraphQL ([guide](https://quarkus.io/guides/smallrye-graphql)): Create GraphQL Endpoints using the code-first approach from MicroProfile GraphQL. | ||
- JDBC Driver - PostgreSQL ([guide](https://quarkus.io/guides/datasource)): Connect to the PostgreSQL database via JDBC. | ||
- Quarkus Jdbi ([guide](https://github.com/quarkiverse/quarkus-jdbi)): Makes it possible to use JDBI in native executables. | ||
- Jdbi ([guide](https://jdbi.org)): Provides convenient, idiomatic, access to relational data in Java. | ||
- Liquibase ([guide](https://quarkus.io/guides/liquibase)): Handle your database schema migrations with Liquibase. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,283 +1,44 @@ | ||
# Notes | ||
|
||
This is my very first project in Kotlin. I decided to take a stab at it since that is an awesome language and I have | ||
This is my very first project in Kotlin. I decided to take a stab at it since that it is an awesome language and I have | ||
been meaning to learn it for some time. | ||
This codebase is simple and I'm sure there are other ways, likely better ways, to do it. I really enjoyed working on | ||
This codebase is simple, and I'm sure there are other ways, likely better ways, to do it. I really enjoyed working on | ||
this. | ||
|
||
Here are some notes about this exercise. | ||
|
||
## Process | ||
|
||
1. Setup project scaffolding | ||
2. High level data and api design (mental model) | ||
- What are the inputs and expected outputs of the api? | ||
- How to design the database relations to make it possible to filter endorsements and aggregate counts? | ||
3. Configure database migrations, including sample data | ||
4. Add basic read endpoints and unit tests | ||
5. Write all the GraphQL queries to test resources folder and use them to | ||
6. Write tests for each endpoint (tests will fail, can mark as disabled for now) | ||
7. Experiment with SQL queries | ||
- Write aggregation queries to list restaurants with available capacity, provided restrictions, party size and time | ||
8. Enable each of the unit tests and implement solution until test pass | ||
9. Repeat for all tests | ||
10. Add documentation | ||
This was my process during this exercise. | ||
|
||
- Idea and conceptualization | ||
- Decide between Rest or GraphQL, Programming Languages and Frameworks. | ||
- Setup project scaffolding | ||
- High level data and api design (mental model) | ||
- What are the inputs and expected outputs of the api? | ||
- How to design the database relations to make it possible to filter endorsements and aggregate counts? | ||
- Configure database migrations, including sample data | ||
- Add basic read endpoints and unit tests | ||
- Write all the GraphQL queries to test resources folder and use them to | ||
- Write tests for each endpoint (tests will fail, can mark as disabled for now) | ||
- Experiment with SQL queries | ||
- Write aggregation queries to list restaurants with available capacity, provided restrictions, party size and time | ||
- TROUBLE: Hibernate+Panache is not working and I have wasted a lot of time. | ||
- Rewrite the Data Access Layer to use Jdbi | ||
- Implement Advanced SQL queries for search | ||
- Enable each of the unit tests and implement solution until test pass | ||
- Repeat for all tests | ||
- Add/Update documentation | ||
|
||
## Challenges | ||
|
||
- reactive database sessions didn't work in kotlin. Seems to be an issue with graphql library. Will be watching for | ||
updates. | ||
- reactive database sessions didn't work in kotlin. Seems to be an issue with graphql library. Will be watching for updates. | ||
https://github.com/quarkusio/quarkus/issues/34101 | ||
|
||
- Using data classes as graphql input took some digging to work | ||
https://github.com/smallrye/smallrye-graphql/issues/1853 | ||
|
||
- How to use PostgreSQL `Endorsement[]` mapped to `List<Endorsement>` with Panache? | @todo | ||
|
||
- One of the main challenges was trying to do advanced SQL with Hibernate and Panache. I have come to the conclusion it | ||
was a waste of time. I'm switching to Jdbi, rewriting the model layer. | ||
|
||
## External Database | ||
|
||
To run the service with an external database, | ||
|
||
- uncomment the database configuration settings in `application.properties` | ||
|
||
To disable the data being reset on startup | ||
|
||
- set the `quarkus.liquibase.clean-at-start=true` to `false` | ||
|
||
To disable SQL query log | ||
|
||
- set the `quarkus.hibernate-orm.log.sql=true` to `false` | ||
|
||
## Data Model | ||
|
||
Since this service does not implement authentication, there was no need to have a user table. | ||
User's name and Restrictions are provided during booking of tables. | ||
|
||
![database diagram](database.png) | ||
|
||
## SQL | ||
|
||
Some of the SQL is inline in the DAO and some are included from the resources folder. | ||
In some cases, the SQL is templated using [stringtemplate4](https://github.com/antlr/stringtemplate4/blob/master/doc/templates.md). | ||
[Cheatsheet](https://github.com/antlr/stringtemplate4/blob/master/doc/cheatsheet.md). | ||
|
||
Originally, this project began using Hibernate ORM and Panache. However, that made it nearly impossible to write custom sql queries. | ||
The switch to [Quarkus-Jdbi](https://github.com/quarkiverse/quarkus-jdbi) made all the difference in completing this project. | ||
|
||
### Total Restaurant Capacity | ||
|
||
In the following query `size` is the type of table and `capacity` is the number of tables of that type. | ||
|
||
```postgresql | ||
SELECT r.id, | ||
r.name, | ||
rt.size, | ||
SUM(rt.quantity) as capacity, | ||
rt.size * SUM(rt.quantity) AS seats | ||
FROM restaurant AS r | ||
INNER JOIN restaurant_table AS rt ON rt.restaurant_id = r.id | ||
GROUP BY 1, 2, 3 | ||
ORDER BY 2, 3; | ||
``` | ||
|
||
| id | name | size | capacity | seats | | ||
|:-------------------------------------|:-------------------------|:-----|:---------|:------| | ||
| c52e1d11-757a-48dc-88e8-4bf9866ca53a | Falling Piano Brewing Co | 2 | 5 | 10 | | ||
| c52e1d11-757a-48dc-88e8-4bf9866ca53a | Falling Piano Brewing Co | 4 | 5 | 20 | | ||
| c52e1d11-757a-48dc-88e8-4bf9866ca53a | Falling Piano Brewing Co | 6 | 5 | 30 | | ||
| 635dc3bd-c515-4d41-848b-bc487bb13810 | Lardo | 2 | 4 | 8 | | ||
| 635dc3bd-c515-4d41-848b-bc487bb13810 | Lardo | 4 | 2 | 8 | | ||
| 635dc3bd-c515-4d41-848b-bc487bb13810 | Lardo | 6 | 1 | 6 | | ||
| dfe2cab1-6a39-4426-8937-c1d89403e0f0 | Panadería Rosetta | 2 | 3 | 6 | | ||
| dfe2cab1-6a39-4426-8937-c1d89403e0f0 | Panadería Rosetta | 4 | 2 | 8 | | ||
| dfe2cab1-6a39-4426-8937-c1d89403e0f0 | Panadería Rosetta | 6 | 0 | 0 | | ||
| b1e6728c-da7c-4841-bbf3-ba7e97f7e07c | Tetetlán | 2 | 2 | 4 | | ||
| b1e6728c-da7c-4841-bbf3-ba7e97f7e07c | Tetetlán | 4 | 4 | 16 | | ||
| b1e6728c-da7c-4841-bbf3-ba7e97f7e07c | Tetetlán | 6 | 1 | 6 | | ||
| d42c8608-7d52-4ea3-823f-c59b68a33407 | u.to.pi.a | 2 | 2 | 4 | | ||
| d42c8608-7d52-4ea3-823f-c59b68a33407 | u.to.pi.a | 4 | 0 | 0 | | ||
| d42c8608-7d52-4ea3-823f-c59b68a33407 | u.to.pi.a | 6 | 0 | 0 | | ||
|
||
Filter results by dietary restrictions | ||
|
||
```postgresql | ||
SELECT r.id, | ||
r.name, | ||
rt.size, | ||
SUM(rt.quantity) as capacity, | ||
rt.size * SUM(rt.quantity) AS seats | ||
FROM restaurant AS r | ||
INNER JOIN restaurant_table AS rt ON rt.restaurant_id = r.id | ||
LEFT JOIN restaurant_endorsement AS re ON re.restaurant_id = r.id | ||
WHERE re.endorsement IN ('vegan', 'vegetarian') | ||
GROUP BY 1, 2, 3 | ||
ORDER BY 2, 3; | ||
``` | ||
|
||
| id | name | size | capacity | seats | | ||
|:-------------------------------------|:------------------|:-----|:---------|:------| | ||
| dfe2cab1-6a39-4426-8937-c1d89403e0f0 | Panadería Rosetta | 2 | 3 | 6 | | ||
| dfe2cab1-6a39-4426-8937-c1d89403e0f0 | Panadería Rosetta | 4 | 2 | 8 | | ||
| dfe2cab1-6a39-4426-8937-c1d89403e0f0 | Panadería Rosetta | 6 | 0 | 0 | | ||
| d42c8608-7d52-4ea3-823f-c59b68a33407 | u.to.pi.a | 2 | 4 | 8 | | ||
| d42c8608-7d52-4ea3-823f-c59b68a33407 | u.to.pi.a | 4 | 0 | 0 | | ||
| d42c8608-7d52-4ea3-823f-c59b68a33407 | u.to.pi.a | 6 | 0 | 0 | | ||
|
||
Restaurant Capacity by table size | ||
|
||
```postgresql | ||
SELECT sub.id, | ||
sub.name, | ||
sub.endorsements, | ||
rt.size, | ||
SUM(rt.quantity) as capacity, | ||
rt.size * SUM(rt.quantity) AS seats | ||
FROM (SELECT r.id, | ||
r.name, | ||
ARRAY_AGG(re.endorsement) AS endorsements | ||
FROM restaurant AS r | ||
LEFT JOIN restaurant_endorsement AS re ON re.restaurant_id = r.id | ||
GROUP BY 1, 2) AS sub | ||
INNER JOIN restaurant_table AS rt ON rt.restaurant_id = sub.id | ||
GROUP BY 1, 2, 3, 4 | ||
ORDER BY 2, 3; | ||
``` | ||
|
||
| id | name | endorsements | size | capacity | seats | | ||
|:-------------------------------------|:-------------------------|:--------------------|:-----|:---------|:------| | ||
| c52e1d11-757a-48dc-88e8-4bf9866ca53a | Falling Piano Brewing Co | {} | 2 | 5 | 10 | | ||
| c52e1d11-757a-48dc-88e8-4bf9866ca53a | Falling Piano Brewing Co | {} | 4 | 5 | 20 | | ||
| c52e1d11-757a-48dc-88e8-4bf9866ca53a | Falling Piano Brewing Co | {} | 6 | 5 | 30 | | ||
| 635dc3bd-c515-4d41-848b-bc487bb13810 | Lardo | {gluten} | 2 | 4 | 8 | | ||
| 635dc3bd-c515-4d41-848b-bc487bb13810 | Lardo | {gluten} | 4 | 2 | 8 | | ||
| 635dc3bd-c515-4d41-848b-bc487bb13810 | Lardo | {gluten} | 6 | 1 | 6 | | ||
| dfe2cab1-6a39-4426-8937-c1d89403e0f0 | Panadería Rosetta | {gluten,vegetarian} | 2 | 3 | 6 | | ||
| dfe2cab1-6a39-4426-8937-c1d89403e0f0 | Panadería Rosetta | {gluten,vegetarian} | 4 | 2 | 8 | | ||
| dfe2cab1-6a39-4426-8937-c1d89403e0f0 | Panadería Rosetta | {gluten,vegetarian} | 6 | 0 | 0 | | ||
| b1e6728c-da7c-4841-bbf3-ba7e97f7e07c | Tetetlán | {gluten,paleo} | 2 | 2 | 4 | | ||
| b1e6728c-da7c-4841-bbf3-ba7e97f7e07c | Tetetlán | {gluten,paleo} | 4 | 4 | 16 | | ||
| b1e6728c-da7c-4841-bbf3-ba7e97f7e07c | Tetetlán | {gluten,paleo} | 6 | 1 | 6 | | ||
| d42c8608-7d52-4ea3-823f-c59b68a33407 | u.to.pi.a | {vegan,vegetarian} | 2 | 2 | 4 | | ||
| d42c8608-7d52-4ea3-823f-c59b68a33407 | u.to.pi.a | {vegan,vegetarian} | 4 | 0 | 0 | | ||
| d42c8608-7d52-4ea3-823f-c59b68a33407 | u.to.pi.a | {vegan,vegetarian} | 6 | 0 | 0 | | ||
|
||
This query lists restaurants, their endorsements and total capacity. | ||
In this case total capacity is the number of people the venue can accommodate. | ||
|
||
```postgresql | ||
SELECT sub.id, | ||
sub.name, | ||
sub.endorsements, | ||
SUM(rt.size * rt.quantity) AS total_capacity | ||
FROM (SELECT r.id, | ||
r.name, | ||
ARRAY_AGG(re.endorsement) AS endorsements | ||
FROM restaurant AS r | ||
LEFT JOIN restaurant_endorsement AS re ON re.restaurant_id = r.id | ||
GROUP BY 1, 2) AS sub | ||
INNER JOIN restaurant_table AS rt ON rt.restaurant_id = sub.id | ||
GROUP BY 1, 2, 3 | ||
ORDER BY total_capacity; | ||
``` | ||
|
||
| id | name | endorsements | total\_capacity | | ||
|:-------------------------------------|:-------------------------|:--------------------|:----------------| | ||
| d42c8608-7d52-4ea3-823f-c59b68a33407 | u.to.pi.a | {vegan,vegetarian} | 4 | | ||
| dfe2cab1-6a39-4426-8937-c1d89403e0f0 | Panadería Rosetta | {gluten,vegetarian} | 14 | | ||
| 635dc3bd-c515-4d41-848b-bc487bb13810 | Lardo | {gluten} | 22 | | ||
| b1e6728c-da7c-4841-bbf3-ba7e97f7e07c | Tetetlán | {gluten,paleo} | 26 | | ||
| c52e1d11-757a-48dc-88e8-4bf9866ca53a | Falling Piano Brewing Co | {} | 60 | | ||
|
||
## Find Reserved Capacity | ||
|
||
We have determined that a reservation lasts 2 hours. We are looking for how many tables are booked within 2 hours prior | ||
of TIME. | ||
These are tables that are still in use. | ||
|
||
List of restaurant IDs and their current booked capacity between (TIME - 2 hours) and TIME. | ||
|
||
```postgresql | ||
-- INPUT TIME: 2024-08-03 21:00:00.000000 +00:00 | ||
-- Looking between (TIME - 2) and TIME | ||
SELECT b.restaurant_id, | ||
bt.size, | ||
SUM(bt.quantity) as occupied | ||
FROM reservation AS b | ||
INNER JOIN restaurant AS r ON b.restaurant_id = r.id | ||
INNER JOIN reservation_table AS bt ON bt.reservation_id = b.id | ||
WHERE b.reservation_time > '2024-08-03 19:00:00.000000 +00:00' | ||
AND b.reservation_time <= '2024-08-03 21:00:00.000000 +00:00' | ||
GROUP BY 1, 2; | ||
``` | ||
|
||
| restaurant\_id | size | occupied | | ||
|:-------------------------------------|:-----|:---------| | ||
| 635dc3bd-c515-4d41-848b-bc487bb13810 | 6 | 1 | | ||
| b1e6728c-da7c-4841-bbf3-ba7e97f7e07c | 2 | 1 | | ||
| c52e1d11-757a-48dc-88e8-4bf9866ca53a | 2 | 1 | | ||
| c52e1d11-757a-48dc-88e8-4bf9866ca53a | 6 | 1 | | ||
| d42c8608-7d52-4ea3-823f-c59b68a33407 | 2 | 2 | <-- 2 x size 2 tables | ||
| dfe2cab1-6a39-4426-8937-c1d89403e0f0 | 4 | 1 | | ||
|
||
### Challenges | ||
|
||
If user `A` wants to book at 7pm. At 7pm at the table is available, however, | ||
there is an existing reservation for a table at 7:15. This would cause a conflict. | ||
The existing reservation holder for 7:15 will arrive and potentially not have a table. | ||
|
||
#### Proposals | ||
|
||
Also check in the future that the table capacity is still available. | ||
|
||
## Find Available Capacity | ||
|
||
Building on top of the previous queries, we can now subtract occupied from total, and we have available tables. | ||
|
||
```postgresql | ||
WITH venue AS (SELECT r.id, | ||
rt.size, | ||
SUM(rt.quantity) as capacity | ||
FROM restaurant AS r | ||
INNER JOIN restaurant_table AS rt ON rt.restaurant_id = r.id | ||
GROUP BY 1, 2), | ||
booked AS (SELECT b.restaurant_id AS id, | ||
bt.size, | ||
SUM(bt.quantity) as occupied | ||
FROM reservation AS b | ||
INNER JOIN restaurant AS r ON b.restaurant_id = r.id | ||
INNER JOIN reservation_table AS bt ON bt.reservation_id = b.id | ||
WHERE b.reservation_time >= '2024-08-04 19:00:00.000000 +00:00' | ||
AND b.reservation_time < '2024-08-04 21:00:00.000000 +00:00' | ||
GROUP BY 1, 2) | ||
SELECT venue.id, | ||
venue.size, | ||
COALESCE(venue.capacity, 0) AS capacity, | ||
COALESCE(booked.occupied, 0) AS occupied, | ||
COALESCE(venue.capacity, 0) - COALESCE(booked.occupied, 0) AS available | ||
FROM venue | ||
LEFT JOIN booked on (booked.id = venue.id AND booked.size = venue.size) | ||
ORDER BY 1, 2 | ||
``` | ||
|
||
| id | size | capacity | occupied | available | | ||
|:-------------------------------------|:-----|:---------|:---------|:----------| | ||
| 635dc3bd-c515-4d41-848b-bc487bb13810 | 2 | 4 | 0 | 4 | | ||
| 635dc3bd-c515-4d41-848b-bc487bb13810 | 4 | 2 | 0 | 2 | | ||
| 635dc3bd-c515-4d41-848b-bc487bb13810 | 6 | 1 | 1 | 0 | | ||
| b1e6728c-da7c-4841-bbf3-ba7e97f7e07c | 2 | 2 | 1 | 1 | | ||
| b1e6728c-da7c-4841-bbf3-ba7e97f7e07c | 4 | 4 | 0 | 4 | | ||
| b1e6728c-da7c-4841-bbf3-ba7e97f7e07c | 6 | 1 | 0 | 1 | | ||
| c52e1d11-757a-48dc-88e8-4bf9866ca53a | 2 | 5 | 1 | 4 | | ||
| c52e1d11-757a-48dc-88e8-4bf9866ca53a | 4 | 5 | 0 | 5 | | ||
| c52e1d11-757a-48dc-88e8-4bf9866ca53a | 6 | 5 | 1 | 4 | | ||
| d42c8608-7d52-4ea3-823f-c59b68a33407 | 2 | 2 | 2 | 0 | | ||
| d42c8608-7d52-4ea3-823f-c59b68a33407 | 4 | 0 | 0 | 0 | | ||
| d42c8608-7d52-4ea3-823f-c59b68a33407 | 6 | 0 | 0 | 0 | | ||
| dfe2cab1-6a39-4426-8937-c1d89403e0f0 | 2 | 3 | 0 | 3 | | ||
| dfe2cab1-6a39-4426-8937-c1d89403e0f0 | 4 | 2 | 1 | 1 | | ||
| dfe2cab1-6a39-4426-8937-c1d89403e0f0 | 6 | 0 | 0 | 0 | | ||
- How to use PostgreSQL `Endorsement[]` mapped to `List<Endorsement>` with Panache? | (Fixed with Jdbi) | ||
|
||
- **The main challenge** was trying to do custom SQL with Hibernate and Panache. I have come to the conclusion it | ||
was a waste of time. I switched to Jdbi, rewriting the data access layer. |
Oops, something went wrong.