Hot questions for Spring Data JDBC

Top 10 Java Open Source / Spring / Spring Data JDBC

Question:

I'm playing around with spring-data-jdbc and discovered a problem, with I can't solve using Google.

No matter what I try to do, I just can't push a trivial object into the database (Bean1.java:25): carRepository.save(new Car(2L, "BMW", "5"));

Both, without one and with a TransactionManager +@Transactional the database (apparently) does not commit the record.

The code is based on a Postgres database, but you might also simply use a H2 below and get the same result.

Here is the (minimalistic) source code: https://github.com/bitmagier/spring-data-jdbc-sandbox/tree/stackoverflow-question

Can somebody tell me, why the car is not inserted into the database?


Answer:

This is not related to transactions not working. Instead, it's about Spring Data JDBC considering your instance an existing instance that needs updating (instead of inserting).

You can verify this is the problem by activating logging for org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate. You should see an update but no insert.

By default, Spring Data JDBC considers an entity as new when it has an id of an object type and a value of null or of a primitive type (e.g. int or long) and a value of 0.

You have the following options in order to make it work:

  1. Set the id to null and configure your database schema so that it will automatically create a new value on insert. After the save your entity instance will contain the generated value from the database.

    Note: Spring Data JDBC will set the id even if it is final in your entity.

  2. Leave the id null and set it in a Before-Save listener to the desired value.

  3. Let your entity implement Persistable. This allows you to control when an entity is considered new. You'll probably need a listener as well so you can let the entity know it is not new any longer.

  4. Beginning with version 1.1 of Spring Data JDBC you'll also be able to use a JdbcAggregateTemplate to do a direct insert, without inspecting the id, see https://jira.spring.io/browse/DATAJDBC-282. Of course, you can do that in a custom method of your repository, as is done in this example: https://github.com/spring-projects/spring-data-examples/pull/441

Question:

In Spring Data JPA we can map an entity to a specific table by using @Table annotation where we can specify schema and name.

But Spring Data JDBC uses a NamingStrategy to map an entity to a table name by converting the entities class name. For example, if we have the entity class named MetricValue then the table should be named metricvalue in default schema. But I need to map MetricValue to the metric_value table in app schema.

Is there any way to override this mapping by annotation or any other?


Answer:

Spring Data JDBC has it's own @Table annotation and also an @Column one.

You just add the annotation to your entity and specify the name as the value of the annotation.

To give some examples:

@Table("entity") 
class MyEntity {

    private @Column("last_name") String name;

    @Column(value = "entity_id", keyColumn = "entity_index") 
    private List<SomeOtherEntity> someList;
}

This will read and write MyEntity into/from the table entity instead of the default my_entity. The attribute name will get stored in the column last_name. And the columns for backreferencing from the some_other_entity to entity will be named entity_id for the foreign key column which normally would be entity (the table name of the referenced table). And the list index will be stored in entity_index instead of the default entity_key.

I created an issue for improving the documentation.

Question:

I want to model a OneToMany Relation with Spring Data JDBC. I´ve read on this very useful blog https://spring.io/blog/2018/09/24/spring-data-jdbc-references-and-aggregates that you should use references when you want to model ToMany Reference:

Therefore any Many-to-One and Many-to-Many relationship must be modeled by just referencing the id.

So I have this scenario: One Student can have multiple Registration. And one Registration can have exactly one Student. If you delete Registration the assigned Student should not get deleted cascading. I ended up with this modelling:

@Data
@AllArgsConstructor(access = AccessLevel.PRIVATE, onConstructor = @__(@PersistenceConstructor))
public class Registration {

    private final @Id
    @Wither
    long registrationId;

    @NotNull
    private String electiveType;

    @NotNull
    private LocalDateTime created = LocalDateTime.now();

    @NotNull
    private StudentRegistrationReference studentRegistrationReference;

}

@Data
@AllArgsConstructor(access = AccessLevel.PRIVATE, onConstructor = @__(@PersistenceConstructor))
public class StudentRegistrationReference {
    private long student;
    private long registration;
}

@Data
@AllArgsConstructor(access = AccessLevel.PRIVATE, onConstructor = @__(@PersistenceConstructor))
public class Student {

    private final @Id
    @Wither
    long studentId;

    @NotNull
    @Size(min = 4, max = 20)
    private String userId;

    @NotNull
    @Min(0)
    private int matriculationNumber;

    @NotNull
    @Email
    private String eMail;

    private Set<StudentRegistrationReference> studentRegistrationReferences = new HashSet<>();

}

My question is whether my modeling is correctly implemented?


Answer:

You are quoting the article talking about "Many-To-X" but you talk yourself about "X-To-Many". You can model a One-To-One or a One-To-Many relationship with a direct reference, or a List/Set/Map of entities.

What you should avoid are bidirectional relationships. While you probably can make them work with the approach you are using, you really shouldn't.

Which brings us to the question: How should this model look like?

The central decision to make is how many aggregates are involved?

A Student certainly is an aggregate and the Student class is its aggregate root. It can exist on its own.

But what about Registration? I'd argue, it is probably part of the same aggregate. The delete test is a good one. If you delete a Student from the system, do the registrations of that Student still have value? Or should the disappear together with the Student?

As an exercise let's do both variants. I start with: Just one aggregate:

class Registration {

    @Id private long Id;

    String electiveType;
    LocalDateTime created = LocalDateTime.now();
}

class Student {

    @Id private long Id;

    String userId;
    int matriculationNumber;
    String eMail;
    Set<Registration> registrations = new HashSet<>();
}

With this, you would have a single repository:

interface StudentRepository extends CrudRepository<Student, Long>{}

I removed all the Lombok annotations since they aren't really relevant to the problem. Spring Data JDBC can operate on simple attributes.

If Registration and Student both are aggregates it gets a little more involved: You need to decide which side owns the reference.

First case: The Registration owns the reference.

class Registration {

    @Id private long Id;

    String electiveType;
    LocalDateTime created = LocalDateTime.now();

    Long studentId;
}

public class Student {

    @Id private long Id;

    String userId;
    int matriculationNumber;
    String eMail;
}

Second case: The Student owns the reference

class Registration {

    @Id private long Id;

    String electiveType;
    LocalDateTime created = LocalDateTime.now();
}

class Student {

    @Id private long Id;

    String userId;
    int matriculationNumber;
    String eMail;

    Set<RegistrationRef> registrations = new HashSet<>();
}

class RegistrationRef {

    Long registrationId;
}

Note that the RegistrationRef doesn't have a studentId or similar. The table assumed for the registrations property will have a student_id column.

Question:

I have an entity that has a filed of type java.util.Date (or Timestamp, doesn't matter for the case).

public class StatusReason {
    @Column("SRN_ID")
    private Long id;
    @Column("SRN_CREATOR")
    private String creator;
    @Column("SRN_REASON")
    private String reason;
    @Column("SRN_STATUS")
    private String status;
    @Column("SRN_TIMESTAMP")
    private Date timestamp;
//getters, setters, etc...
}

The database is Oracle and the corresponding column is of type TIMESTAMP(6) WITH TIME ZONE

When I call any of the default findById or findAll methods of the repository I get: ConverterNotFoundException: No converter found capable of converting from type [oracle.sql.TIMESTAMPTZ] to type [java.util.Date].

I can create a custom RowMapper for the type and it will work fine. I was just wondering if it's possible to register a custom converter (in my case from oracle.sql.TIMESTAMPTZ to java.util.Date) so can still benefit from the default mapping and use the converter through the whole app.


Answer:

You can register custom conversions by inheriting your configuration from JdbcConfiguration and overwriting the method jdbcCustomConversions().

JdbcCustomConversions takes a list of Converter as an argument.