April 2024
The idea of creating came from an old project. A few years ago I made a student management system in which I used raw SQL queries. This meant I needed to write a lot of SELECT and INSERT queries. During that time it didn't feel tedious, because I was making a new project and learning SQL. But a few months ago while I was going through my old projects I saw this project and thought by myself: 'Couldn't I do it easier?' So with the knowledge I gained from having a few projects made in Spring Boot and with hibernate.
I decided that I could make my own version. And so I did. I made a custom ORM-tool that would map Java object to their respective SQL tables. At the moment of writing there is also support for many to one and one to one links. Many to many is currently under progress.
So how does this project work under the hood? Well with a brief explanation, there are custom annotations that describe what kind of table a class is and what kind of columns it has. Does it have an id? Does it have a text column? This information is all given with annotations. And then we can loop through all the fields with relfection and then find out what kind of columns there are.
Let's take for example the Column annotation. Here we can say to the mapper that a field is of a specific type and how long it should be. The default would be of type VARCHAR with a size of 64. However we can also say that it is of type TEXT, INTEGER, etc.
1@Target(ElementType.FIELD)
2@Retention(RetentionPolicy.RUNTIME)
3public @interface Column {
4 ColumnType type() default ColumnType.VARCHAR;
5 int size() default 64;
6}
7
Besides the Column annotation we have more, such as the Id or the Entity. If we combine these annotations together we would have something like this:
1@Entity
2public class Category implements IModel {
3 @Id
4 public int id;
5 @Column(type = ColumnType.VARCHAR)
6 public String label;
7
8 public Category() { }
9
10 public Category(int id, String label) {
11 this.id = id;
12 this.label = label;
13 }
14
15}
With the magic of reflections we can get all the available fields from a class. In the code example below we provide a class object to the function and which will grab all the available fields and return them.
1public List<Field> getFieldsOfModel(Class<? extends IModel> model) {
2 return Arrays.stream(model.getFields()).toList();
3}
With the function above to get all the fields of a model, there is also a function that will get all the fields except for the specified annotation:
1public List<Field> getFieldsOfModelWithoutTypes(Class<? extends IModel> model, List<Class<? extends Annotation>> annotations) {
2 return getFieldsOfModel(model)
3 .stream()
4 .filter(field -> annotations.stream().noneMatch(field::isAnnotationPresent))
5 .toList();
6}
Besides the get without type function, there is also one that will grab the fields with the specified annotations. In that case it's just reversed.
And then last but not least to finish the small reflection section. We also have a function that takes a model in and that will grab the value of a field specified by name:
1public Object getValueOfField(IModel model, String fieldName) throws NoSuchFieldException, IllegalAccessException {
2 return getFieldByName(model.getClass(), fieldName).get(model);
3}
Let's take a look at the INSERT query. The generateInsertQuery function takes a class as input and builds an INSERT query by analyzing the fields and annotations. First it retrieves the table name from the class that is provided to the function. Then, it will iterate over all the declared fields and append their names to the query. After we collected all the column names, it will add it to the query and send that query straight to the database.
Happy coding! 🎉