1. java style initialization

@Builder
@NoArgsConstructor
@Getter
@Setter
@AllArgsConstructor
@Entity
public class Bar {

    @Id
    @GeneratedValue
    private Long id;

    private String stringField = "123";
    private int intPrimitiveField = 456;
    private Long intNonPrimitiveField = 789L;
}


public interface BarRepository extends JpaRepository<Bar, Long> {
}


@Service
public class BarService {

    private final BarRepository barRepository;

    @Autowired
    public BarService(BarRepository barRepository) {
        this.barRepository = barRepository;
    }

    @Transactional
    public Bar persist(Bar entity){
        return barRepository.save(entity);
    }

    @Transactional(readOnly = true)
    public Bar get(Long id){
        return barRepository.getOne(id);
    }
}

проверим:

public class BarTest extends BasePostgresIT {

    @Autowired
    private BarService barService;

    @Test
    public void testInit() throws Exception {

        Bar persistedBar = barService.persist(new Bar());

        // Asserts
        Bar bar = getSession().get(Bar.class, persistedBar.getId());
        Assertions.assertThat(bar)
                  .isNotNull()
                  .extracting(Bar::getStringField,
                              Bar::getIntPrimitiveField,
                              Bar::getIntNonPrimitiveField)
                  .containsOnly("123", 456, 789L); (1)

    }

    @Test
    public void testInitUnexpected() throws Exception {
        Bar persistedBar = barService.persist(Bar.builder().build()); (2)

        // Asserts
        Bar bar = getSession().get(Bar.class, persistedBar.getId());
        Assertions.assertThat(bar)
                  .isNotNull()
                  .extracting(Bar::getStringField,
                              Bar::getIntPrimitiveField,
                              Bar::getIntNonPrimitiveField)
                  .containsOnly(null, 0, null);
    }
}
1 вроде все хорошо и все инициализиаруется как надо
2 но вот только стоит забыть о том как будет создаваться объект в билдере и надежды на дефолтные значения могут быть не оправданы

2. DataBase default

Чтобы не переживать о том как именно создавали объект, попробуем переложить ответственность за дефолтные значения на нашу БД

@Builder
@NoArgsConstructor
@Getter
@Setter
@AllArgsConstructor
@Entity
public class Foo {

    @Id
    @GeneratedValue
    private Long id;

    @ColumnDefault("1")  (1)
    private long fieldA;

    @ColumnDefault("2")  (2)
    private Long fieldB;

    @Column(columnDefinition = "int default 3")  (3)
    private int fieldC;

    @Column(columnDefinition = "int default 4")  (4)
    private Integer fieldD;

    @Column(name = "one_more", columnDefinition = "bigint default 5", insertable = false, updatable = true)  (5)
    private int fieldE;
}
1 так сработает?
2 а если не примитивный тип
3 или может быть так?
4 а если тут не примитивный тип?
5 все до кучи, чтобы уж наверняка =)

тестируем:

@Test
public void testDataBaseInitializationField() throws Exception {

    Foo result = fooService.persist(new Foo());

    // посмотрим что вернулось
    Assertions.assertThat(result)
              .extracting(Foo::getFieldA,
                          Foo::getFieldB,
                          Foo::getFieldC,
                          Foo::getFieldD,
                          Foo::getFieldE)
              .containsOnly(0L, null, 0, null, 0);

    // често перезапрашиваем объект из другой сессии
    result = getSession().get(Foo.class, result.getId());
    Assertions.assertThat(result)
              .extracting(Foo::getFieldA,
                          Foo::getFieldB,
                          Foo::getFieldC,
                          Foo::getFieldD,
                          Foo::getFieldE)
              .containsOnly(0L, null, 0, null, 5);
             // в итоге сработает только вариант для которого явно
             // указано не инсертить потому что все остальные поля будут
             // переданы в инсерт, хоть нулами, хоть нулями но будут там
             // и БД уже не будет выдавать значение по умолчанию
}

посмотрим запрос:

16:11:12.722 [main] DEBUG org.hibernate.SQL - insert into foo (fielda, fieldb, fieldc, fieldd, id) values (?, ?, ?, ?, ?)
Hibernate: insert into foo (fielda, fieldb, fieldc, fieldd, id) values (0, null, 0, null, 1)

Выходит что единственное поле, которое честно проинициализировалось дефолтным значением, не отправляется в инсерте.

Что будет если мы укажем значение этого поля отличное от дефолтного?

/**
 * А теперь вот интересно что будет если это поле мы все таки заполнили в сущности,
 * и нам не нужно значение по умолчанию.
 */
@Test
public void testInsert() throws Exception {

    Foo result = fooService.persist(Foo.builder().fieldE(7).build());

    // посмотрим что вернулось
    Assertions.assertThat(result)
              .extracting(Foo::getFieldA,
                          Foo::getFieldB,
                          Foo::getFieldC,
                          Foo::getFieldD,
                          Foo::getFieldE)
              .containsOnly(0L, null, 0, null, 7);

    // вроде бы все хорошо, но что если перезапросить:
    result = getSession().get(Foo.class, result.getId());
    Assertions.assertThat(result)
              .extracting(Foo::getFieldA,
                          Foo::getFieldB,
                          Foo::getFieldC,
                          Foo::getFieldD,
                          Foo::getFieldE)
              .containsOnly(0L, null, 0, null, 5);
              // и тут мы видим что при инсерте сущности в БД, это поле никуда не ушло
              // что собственно и дала анотация, но нам-то нужно другое поведение
}

посмотрим запрос:

16:11:12.511 [main] DEBUG org.hibernate.SQL - insert into foo (fielda, fieldb, fieldc, fieldd, id) values (?, ?, ?, ?, ?)
Hibernate: insert into foo (fielda, fieldb, fieldc, fieldd, id) values (0, null, 0, null, 1)

Никакой магии не произошло, запрос остался прежним, поле помеченное как insertable = false никогда не отправляется в insert.

Какие варианты?

/**
 * Странный вариант решения - это делать после инсерта update
 */
@Test
public void testUpdate() throws Exception {
    Foo result = fooService.persist(Foo.builder().fieldE(7).build());

    result.setFieldE(123);
    result = fooService.persist(result);

    // посмотрим что вернулось
    Assertions.assertThat(result)
              .extracting(Foo::getFieldA,
                          Foo::getFieldB,
                          Foo::getFieldC,
                          Foo::getFieldD,
                          Foo::getFieldE)
              .containsOnly(0L, null, 0, null, 123);

    // на всякий случай проверим, мы уже никому не верим
    result = getSession().get(Foo.class, result.getId());
    Assertions.assertThat(result)
              .extracting(Foo::getFieldA,
                          Foo::getFieldB,
                          Foo::getFieldC,
                          Foo::getFieldD,
                          Foo::getFieldE)
              .containsOnly(0L, null, 0, null, 123);
}

посмотрим запросы:

16:11:12.670 [main] DEBUG org.hibernate.SQL - insert into foo (fielda, fieldb, fieldc, fieldd, id) values (?, ?, ?, ?, ?)
Hibernate: insert into foo (fielda, fieldb, fieldc, fieldd, id) values (0, null, 0, null, 1)

16:11:12.690 [main] DEBUG org.hibernate.SQL - update foo set fielda=?, fieldb=?, fieldc=?, fieldd=?, one_more=? where id=?
Hibernate: update foo set fielda=0, fieldb=null, fieldc=0, fieldd=null, one_more=123 where id=1

тут мы видим что в update значение этого поля можно изменить, однако это нам стоило двух запросов к БД.

3. PrePersist

Один из способов гарантировать при любом раскладе дефолтные значения в сущности - это использовать PrePersist

@Builder
@NoArgsConstructor
@Getter
@Setter
@AllArgsConstructor
@Entity
public class Baz {

    @Id
    @GeneratedValue
    private Long id;

    private int field;

    @PrePersist
    void init(){
        if(getField() == 0) {
          setField(7);
        }
    }
}

проверяем:

@Test
public void testDataBaseInitializationField() throws Exception {

    Baz result = bazService.persist(new Baz());

    // посмотрим что вернулось
    Assertions.assertThat(result)
              .extracting(Baz::getField)
              .containsOnly(7);

    // често перезапрашиваем объект из другой сессии
    result = getSession().get(Baz.class, result.getId());
    Assertions.assertThat(result)
              .extracting(Baz::getField)
              .containsOnly(7);
}

посмотрим запросы:

18:47:25.340 [main] DEBUG org.hibernate.SQL - insert into baz (field, id) values (?, ?)
Hibernate: insert into baz (field, id) values (7, 1)