I annotated fields for created and updated at behavior, but is not working.
Cześć!
Rozumiem problem. Błąd PropertyValueException dla pola createdAt pojawia się, ponieważ adnotacje @CreatedDate i @LastModifiedDate nie działają "same z siebie". Są to adnotacje ze Spring Data JPA i wymagają włączenia specjalnego mechanizmu zwanego Auditingiem (inspekcją).
Hibernate/JPA, widząc w Twojej encji nullable = false, próbuje zapisać encję do bazy danych z wartością null dla createdAt, ponieważ nikt tej wartości nie ustawił. Mechanizm Springa, który powinien to zrobić, nie jest jeszcze aktywny.
Jak to naprawić?
Musisz zrobić dwie rzeczy:
- Włączyć JPA Auditing w Twojej aplikacji Spring Boot.
- Dodać odpowiedniego "słuchacza" (
EntityListener) do Twojej encji.
Krok 1: Włączenie Auditingu JPA w konfiguracji Springa
W głównej klasie Twojej aplikacji (tej z adnotacją @SpringBootApplication) dodaj adnotację @EnableJpaAuditing.
Przykład:
package org.sstec.marketplace;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.data.jpa.repository.config.EnableJpaAuditing; // <--- IMPORT
@SpringBootApplication
@EnableJpaAuditing // <--- DODAJ TĘ ADNOTACJĘ
public class MarketplaceApplication {
public static void main(String[] args) {
SpringApplication.run(MarketplaceApplication.class, args);
}
}
Ta adnotacja aktywuje mechanizm inspekcji w całym kontekście Springa.
Krok 2: Dodanie EntityListeners do Twojej encji
Teraz musisz poinformować swoją encję AssetEntity, że powinna korzystać z tego mechanizmu. Robi się to przez dodanie adnotacji @EntityListeners nad definicją klasy.
Twoja poprawiona klasa AssetEntity:
package org.sstec.marketplace.assets.entities;
import jakarta.persistence.*;
import lombok.*;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener; // <--- IMPORT
import org.sstec.marketplace.assets.domain.core.AssetStatus;
import org.sstec.marketplace.assets.domain.core.AssetType;
import java.io.Serializable;
import java.time.OffsetDateTime;
import java.util.UUID;
@AllArgsConstructor
@Builder
@NoArgsConstructor
@Getter
@Setter
@Entity
@Table(name = "assets", schema = "assets")
@EntityListeners(AuditingEntityListener.class) // <--- DODAJ TĘ ADNOTACJĘ
public class AssetEntity implements Serializable {
@Id
@GeneratedValue(strategy = GenerationType.UUID)
private UUID id;
@Column(nullable = false)
@Enumerated(EnumType.STRING)
private AssetType assetType;
@Column(nullable = false)
@Enumerated(EnumType.STRING)
private AssetStatus status;
@CreatedDate
@Column(name = "created_at", nullable = false, updatable = false)
private OffsetDateTime createdAt;
@LastModifiedDate
@Column(name = "updated_at", nullable = false)
private OffsetDateTime updatedAt;
}
Adnotacja @EntityListeners(AuditingEntityListener.class) mówi JPA, aby przed operacjami zapisu/aktualizacji tej encji uruchomić logikę z klasy AuditingEntityListener. To właśnie ta klasa jest odpowiedzialna za sprawdzenie adnotacji @CreatedDate i @LastModifiedDate i wstawienie aktualnego czasu.
Podsumowując, co się dzieje po zmianach:
- Wywołujesz
repository.save(newAsset). - Dzięki
@EnableJpaAuditingmechanizm inspekcji jest aktywny. - Dzięki
@EntityListenersTwoja encjaAssetEntity"słucha" zdarzeń zapisu. AuditingEntityListenerjest uruchamiany przed operacją zapisu.- Widzi pole
createdAtz adnotacją@CreatedDatei ustawia mu aktualną datę i czas. - Widzi pole
updatedAtz adnotacją@LastModifiedDatei również ustawia mu aktualną datę i czas. - Encja z już uzupełnionymi polami
createdAtiupdatedAtjest przekazywana do Hibernate w celu zapisania w bazie danych. - Problem
PropertyValueExceptionznika, ponieważ pole nie jest jużnull.
Po wprowadzeniu tych dwóch zmian, wszystko powinno działać zgodnie z oczekiwaniami. Daj znać, czy pomogło
But I get errors like my db type for created and updated at fields is wrong.
Doskonały trop! Ten błąd jest bardzo konkretny i pokazuje nam dokładnie, gdzie leży problem.
To nie jest błąd w Twojej logice, tylko w konfiguracji mechanizmu audytu w Spring Data.
Na czym polega problem?
Stack trace mówi nam: Cannot convert unsupported date type java.time.LocalDateTime to java.time.OffsetDateTime.
Oznacza to, że:
- Mechanizm audytu Springa został poprawnie uruchomiony (dzięki
@EnableJpaAuditing). - Domyślnie, gdy Spring chce wstawić "aktualny czas", generuje obiekt typu
java.time.LocalDateTime. - Następnie próbuje przypisać ten
LocalDateTimedo Twojego polacreatedAt, które ma typjava.time.OffsetDateTime. - Spring nie wie, jak automatycznie przekonwertować
LocalDateTime(który nie ma informacji o strefie czasowej/przesunięciu) naOffsetDateTime(który takiej informacji wymaga). W związku z tym rzuca wyjątkiem.
Lista wspieranych typów, którą podaje w błędzie, wyraźnie pokazuje, że OffsetDateTime nie jest domyślnie obsługiwany przez dostawcę czasu w Spring Data Auditing.
Rozwiązanie (Zalecane i "Poprawne")
Musimy "nauczyć" Springa, jak generować OffsetDateTime zamiast LocalDateTime. Robi się to, tworząc własnego dostawcę czasu (DateTimeProvider) i podpinając go do mechanizmu audytu.
Jest to bardzo proste i składa się z dwóch kroków:
Krok 1: Stwórz własną implementację DateTimeProvider
Stwórz nową klasę w dowolnym pakiecie konfiguracyjnym. Ta klasa będzie odpowiedzialna za dostarczanie aktualnego czasu w formacie, jakiego potrzebujesz.
package org.sstec.marketplace.config; // lub inny pakiet konfiguracyjny
import org.springframework.data.auditing.DateTimeProvider;
import org.springframework.stereotype.Component;
import java.time.OffsetDateTime;
import java.time.temporal.TemporalAccessor;
import java.util.Optional;
// Zarejestruj tę klasę jako bean, aby Spring mógł ją znaleźć.
// Nazwa beana "auditingDateTimeProvider" jest ważna, użyjemy jej w następnym kroku.
@Component("auditingDateTimeProvider")
public class AuditingDateTimeProvider implements DateTimeProvider {
@Override
public Optional<TemporalAccessor> getNow() {
// Zwracaj aktualny czas jako OffsetDateTime z przesunięciem systemowym
return Optional.of(OffsetDateTime.now());
}
}
Krok 2: Zaktualizuj adnotację @EnableJpaAuditing
Teraz w Twojej głównej klasie aplikacji musisz wskazać Springowi, że ma używać Twojego nowego dostawcy czasu. Robi się to za pomocą atrybutu dateTimeProviderRef.
package org.sstec.marketplace;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;
@SpringBootApplication
// Wskaż Springowi, aby używał beana o nazwie "auditingDateTimeProvider"
@EnableJpaAuditing(dateTimeProviderRef = "auditingDateTimeProvider")
public class MarketplaceApplication {
public static void main(String[] args) {
SpringApplication.run(MarketplaceApplication.class, args);
}
}
Nazwa w dateTimeProviderRef ("auditingDateTimeProvider") musi być taka sama jak nazwa beana, którą zdefiniowałeś w adnotacji @Component w kroku 1.
I to wszystko! Po ponownym uruchomieniu aplikacji:
- Spring Auditing nadal będzie się uruchamiał automatycznie.
- Jednak zamiast swojego domyślnego dostawcy czasu, użyje Twojej klasy
AuditingDateTimeProvider. - Twoja klasa zwróci
OffsetDateTime.now(). - Ten typ będzie idealnie pasował do pól
createdAtiupdatedAtw Twojej encji. Problem z konwersją zniknie.
Alternatywne rozwiązanie (Prostsze, ale ze skutkami ubocznymi)
Jeśli nie zależy Ci na przechowywaniu informacji o przesunięciu czasowym (offset) w bazie danych, możesz po prostu zmienić typ pól w encji na LocalDateTime.
// W klasie AssetEntity
// ...
@CreatedDate
@Column(name = "created_at", nullable = false, updatable = false)
private LocalDateTime createdAt; // <--- ZMIANA
@LastModifiedDate
@Column(name = "updated_at", nullable = false)
private LocalDateTime updatedAt; // <--- ZMIANA
Kiedy wybrać to rozwiązanie?
Jeśli Twoja aplikacja zawsze będzie działać na serwerach w tej samej strefie czasowej i nie ma wymogu obsługi klientów/danych z różnych części świata. OffsetDateTime (lub ZonedDateTime) jest uważany za lepszą praktykę w systemach rozproszonych, ponieważ jednoznacznie określa punkt w czasie, niezależnie od strefy czasowej serwera czy klienta.
Podsumowując, jeśli OffsetDateTime jest świadomym wyborem projektowym (co jest bardzo dobrą praktyką!), zdecydowanie polecam pierwsze rozwiązanie. Jest to czyste, zgodne z duchem Springa i rozwiązuje problem u jego źródła, nie zmuszając Cię do kompromisów w modelu danych.