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)