Skip to content

Automatic mappings

David D'Amico edited this page Dec 15, 2021 · 2 revisions

Note: we suggest you read the Usage section and the ResultSetRowMapper's Javadoc before moving on. It contains further explanations on object mappings.

The ClassMapper class

The ClassMapper class automatically maps Java objects for you when you retrieve them from the database. This is useful when you don't have to focus on the ResultSetRowMapper's implementation.
Be aware that this choice may impact performances, though this is very unlike to happen with small databases.

How does this work?

To make this works, we need to follow some mandatory rules about the class structure.
The class (formally called Java Bean):

  • Must be a top-level class
  • It must have a public no-arg constructor
  • Those fields which need to be mapped must have a public setter method More info here.

The mapping is done by following these steps:

  1. An instance of the bean class gets instantiated already (via no-arg constructor) with Reflections
  2. ClassMapper scans every field of the targeted class, retrieving its setter method (if it exists, ignoring the field otherwise)
  3. Then, the fields to map are linked to the table columns with a name comparison
  4. Every field gets filled by invoking its setter method. The given value is obtained by the internal ResultSet
  5. The bean is returned

For this example, we will use the same class in the Usage section.

db.queryForList("SELECT * FROM users", ClassMapper.of(User.class)).whenComplete((users, e) -> {
    if (e != null) {
        // you can handle the error
        return;
    }

    for (User u : users) {
        // you can iterate over user objects, mapped by ClassMapper
    }
}

You can use ClassMapper in every "queryFor" method that takes a ResultSetRowMapper implementation as a parameter since it's an implementation of the ResultSetRowMapper interface itself.

Handling null values from the database

Objects

If a null value is passed to a setter that takes an object, the field will be set as null.

Primitive types

Primitive types have their own default value:

Data Type Default Value (for fields)
byte 0
short 0
int 0
long 0L
float 0.0f
double 0.0d
char '\u0000'
boolean false

Knowing this, setting null to a primitive type is an illegal operation that throws a TypeMismatchException. ClassMapper can be configured (using the 'defaultNullValueForPrimitives' property, true by default) to catch this exception and use the primitive's default value.
ATTENTION: if you use the values from the generated bean to update the database, the primitive value will have been set to the primitive's default value instead of null.

Different names between fields and table's columns

From now on, we will change our example table and class, replacing those with new ones. We will describe these new elements in the following sections.

Users table:
https://i.imgur.com/XlMfvCP.png

User class:

public class User {

    private int id;
    private String name;
    private UUID uuid;
    private int points;

    private ComplexObject o;

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    @Name(columnName = "username")
    public void setName(String name) {
        this.name = name;
    }

    public UUID getUuid() {
        return uuid;
    }

    public void setUuid(@MapWith(StringToUUID.class) UUID uuid) {
        this.uuid = uuid;
    }

    public void setPoints(int points) {
        this.points = points;
    }

    public int getPoints() {
        return points;
    }

    @Skip
    public void setComplex(ComplexObject o) {
        this.o = o;
    }

    public ComplexObject getComplex() {
        return o;
    }
}

Fields should be names after the columns names. Howevers, this is not always possible. To help matching the names during the mapping process, you can annotate the setter method with @Name and specify the column name.

@Name(columnName = "username") // this refers to the column name
public void setName(String name) {
    this.name = name;
}

Skipping fields

To skip a field, just annotate its setter method with the @Skip annotation.

@Skip
public void setSomething(Something s) {
    // ...
}

When you skip a field, its default value depends on the field's nature.
As always: null for object, default value for a primitive type.

Converters

Some types cannot be represented with the SQL's ones.
Complex objects may need to be instantiated with the value retrieved from the database, with more steps to add to the latter.

MapWith

The annotation @MapWith allows us to tell the ClassMapper how it should handle complex object creations, before passing the right value to a field's setter.

In this example we need to fil an UUID field, but we just have its string representation.
With a converter, we can transform that string into the corresponding UUID object.

// ClassMapper will try to convert the value obtained from the database to the desired type.
public void setUuid(@MapWith(StringToUUID.class) UUID uuid) {
    this.uuid = uuid;
}

If you need your own converter you can implement the interface Converter. Read more about this in the Javadoc.
The MapWith annotation's syntax is:

public void setterMethod(@MapWith(YourConverter.class) SomeParamether sp) {
    this.sp = sp;
}