JPA Auditing
Giriş
Bir e-ticaret uygulaması düşünün. Bir ürünün fiyatı yanlışlıkla 1000 TL yerine 10 TL olarak güncellendi. Müdürünüz soruyor: "Bu değişikliği kim yaptı? Ne zaman yaptı?" Eğer bu bilgileri kaydetmiyorsanız, cevabınız "bilmiyorum" olacaktır. İşte JPA Auditing tam olarak bu sorunu çözer.
Auditing (denetleme), entity'lerin ne zaman oluşturulduğunu, ne zaman güncellendiğini ve bu işlemleri kimin yaptığını otomatik olarak izleme mekanizmasıdır. Bu bilgiler sadece debugging için değil; compliance (uyum), veri izlenebilirliği (traceability), yasal gereklilikler ve güvenlik denetimi için de kritik öneme sahiptir.
Gerçek Dünya Analojisi
Bir bankadaki güvenlik kamerası sistemi gibi düşünün. Kasada para sayılırken, kim sayıyor, saat kaçta sayıyor, hepsi kayıt altında. Eğer bir sorun olursa, kayıtlara bakarak "saat 14:23'te Ali kasada işlem yaptı" diyebilirsiniz. JPA Auditing de veritabanınızın güvenlik kamerasıdır — her veri değişikliğinin kim tarafından ve ne zaman yapıldığını otomatik olarak kaydeder.
Neden Manuel Değil de Otomatik?
"Ben her save() çağrısından önce setCreatedAt(LocalDateTime.now()) yazarım" diyebilirsiniz. Ama düşünün:
50 entity'niz var, her birinde 4 audit alanı = 200 alan
Her service metodunda bunları manuel set etmeniz gerekiyor
Bir tanesini unutursanız → veri eksikliği
Farklı geliştiriciler farklı formatlar kullanabilir
JPA Auditing tüm bu sorunları tek bir konfigürasyonla çözer. "Yaz bir kere, unut gitsin" prensibi.
Spring Data JPA Auditing Kurulumu
Auditing'i aktifleştirmek üç adımdan oluşur. Hepsini sırayla görelim.
Adım 1: @EnableJpaAuditing ile Auditing'i Aktifleştirme
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;
@SpringBootApplication
@EnableJpaAuditing // Bu annotation olmadan hiçbir audit alanı çalışmaz!
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}@EnableJpaAuditing annotation'ı Spring Data JPA'ya "entity'lerdeki audit annotation'larını işle" mesajını verir. Bu olmadan @CreatedDate, @LastModifiedDate gibi annotation'lar hiçbir şey yapmaz.
💡 İpucu: @EnableJpaAuditing annotation'ını ana uygulama sınıfına veya ayrı bir @Configuration sınıfına koyabilirsiniz. Büyük projelerde ayrı bir AuditConfig sınıfı oluşturmak daha temizdir.
Adım 2: BaseEntity ile Ortak Audit Alanları
Her entity'ye ayrı ayrı audit alanları eklemek yerine, ortak bir BaseEntity (veya AuditableEntity) oluşturup tüm entity'lerin bundan türemesini sağlarız:
import jakarta.persistence.*;
import org.springframework.data.annotation.CreatedBy;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedBy;
import org.springframework.data.annotation.LastModifiedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;
import java.time.LocalDateTime;
@MappedSuperclass // Bu sınıf bir entity DEĞİL, sadece ortak alan sağlayıcı
@EntityListeners(AuditingEntityListener.class) // Audit event'lerini dinle
public abstract class BaseEntity {
@CreatedDate
@Column(nullable = false, updatable = false) // Oluşturulma tarihi asla güncellenmez
private LocalDateTime createdAt;
@LastModifiedDate
@Column(nullable = false)
private LocalDateTime updatedAt;
@CreatedBy
@Column(updatable = false) // Oluşturan kişi asla değişmez
private String createdBy;
@LastModifiedBy
private String updatedBy;
// Getter'lar
public LocalDateTime getCreatedAt() { return createdAt; }
public LocalDateTime getUpdatedAt() { return updatedAt; }
public String getCreatedBy() { return createdBy; }
public String getUpdatedBy() { return updatedBy; }
}Bu yapıdaki her annotation'ın rolü:
| Annotation | Açıklama | Ne Zaman Set Edilir |
|---|---|---|
@CreatedDate | Entity ilk kaydedildiğinde tarih/saat | INSERT anında |
@LastModifiedDate | Entity her güncellendiğinde tarih/saat | INSERT ve UPDATE anında |
@CreatedBy | Entity'yi ilk oluşturan kullanıcı | INSERT anında |
@LastModifiedBy | Entity'yi son güncelleyen kullanıcı | INSERT ve UPDATE anında |
⚠️ Dikkat: @MappedSuperclass ile @Entity arasındaki fark kritiktir. @MappedSuperclass sadece alan tanımlarını alt sınıflara aktarır, kendisi bir tablo oluşturmaz. @Entity ise veritabanında bir tablo oluşturur. BaseEntity bir tablo olmamalı, sadece ortak alanları sağlamalıdır.
Adım 3: @EntityListeners Nedir?
@EntityListeners(AuditingEntityListener.class) satırı, JPA'ya "bu entity üzerinde bir lifecycle event (persist, update) olduğunda AuditingEntityListener'ı çağır" der. Bu listener, @CreatedDate gibi annotation'ları tarayarak ilgili alanları otomatik doldurur.
Kamera analojimize dönersek: @EntityListeners, kasanın üstüne kamera yerleştirmektir. Kamera (listener) olmadan, güvenlik sistemi (auditing) ne kadar gelişmiş olursa olsun, görüntü alamazsınız.
Entity'lerde BaseEntity Kullanımı
Artık herhangi bir entity'yi BaseEntity'den türetmek yeterli:
import jakarta.persistence.*;
import java.math.BigDecimal;
@Entity
@Table(name = "products")
public class Product extends BaseEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, length = 200)
private String name;
@Column(nullable = false, precision = 10, scale = 2)
private BigDecimal price;
@Column(length = 500)
private String description;
@Enumerated(EnumType.STRING)
private ProductStatus status = ProductStatus.ACTIVE;
// Constructors
protected Product() {} // JPA zorunlu
public Product(String name, BigDecimal price) {
this.name = name;
this.price = price;
}
// Getter/Setter'lar
public Long getId() { return id; }
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public BigDecimal getPrice() { return price; }
public void setPrice(BigDecimal price) { this.price = price; }
public String getDescription() { return description; }
public void setDescription(String description) { this.description = description; }
}@Entity
@Table(name = "orders")
public class Order extends BaseEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, unique = true)
private String orderNumber;
@Column(nullable = false, precision = 12, scale = 2)
private BigDecimal totalAmount;
@Enumerated(EnumType.STRING)
private OrderStatus status = OrderStatus.PENDING;
// Constructors, getter/setter...
}Hem Product hem Order artık otomatik olarak createdAt, updatedAt, createdBy, updatedBy alanlarına sahip. Sıfır tekrar, sıfır unutma riski.
AuditorAware — "Kim Tarafından?" Sorusunun Cevabı
@CreatedDate ve @LastModifiedDate için Spring, sistem saatini kullanır — ekstra bir şey yapmanız gerekmez. Ama @CreatedBy ve @LastModifiedBy için Spring'e "mevcut kullanıcı kim?" sorusunun cevabını vermeniz gerekir. Bunu AuditorAware interface'ini implement ederek yaparsınız.
Spring Security ile Entegrasyon
import org.springframework.data.domain.AuditorAware;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import java.util.Optional;
@Component
public class SecurityAuditorAware implements AuditorAware<String> {
@Override
public Optional<String> getCurrentAuditor() {
return Optional.ofNullable(SecurityContextHolder.getContext())
.map(SecurityContext::getAuthentication)
.filter(Authentication::isAuthenticated)
.filter(auth -> !auth.getName().equals("anonymousUser"))
.map(Authentication::getName);
}
}Bu implementasyon, Spring Security'nin SecurityContextHolder'ından mevcut kullanıcının adını alır. Eğer kimse giriş yapmamışsa (anonymous), Optional.empty() döner ve @CreatedBy/@LastModifiedBy alanları null kalır.
Spring Security Yoksa (Basit Versiyon)
Eğer uygulamanızda henüz Spring Security yoksa, basit bir implementasyon kullanabilirsiniz:
@Component
public class SimpleAuditorAware implements AuditorAware<String> {
@Override
public Optional<String> getCurrentAuditor() {
// Basit versiyon: Sabit değer veya HTTP header'dan okuma
return Optional.of("system");
}
}JWT Token ile Entegrasyon
Modern uygulamalarda JWT kullanıyorsanız:
@Component
public class JwtAuditorAware implements AuditorAware<String> {
@Override
public Optional<String> getCurrentAuditor() {
return Optional.ofNullable(SecurityContextHolder.getContext())
.map(SecurityContext::getAuthentication)
.filter(Authentication::isAuthenticated)
.map(auth -> {
if (auth.getPrincipal() instanceof UserDetails userDetails) {
return userDetails.getUsername();
}
// JWT'den gelen subject (genellikle email veya userId)
return auth.getName();
});
}
}AuditorAware'i @EnableJpaAuditing ile Bağlama
Eğer birden fazla AuditorAware implementasyonunuz varsa, hangisinin kullanılacağını belirtmeniz gerekir:
@Configuration
@EnableJpaAuditing(auditorAwareRef = "securityAuditorAware")
public class AuditConfig {
// securityAuditorAware → bean adı (sınıf adının camelCase hali)
}Farklı Tip Auditor: String Yerine Long (User ID)
Bazen kullanıcı adı yerine kullanıcı ID'sini kaydetmek istersiniz. AuditorAware generic type ile bunu destekler:
@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
public abstract class BaseEntity {
@CreatedDate
@Column(nullable = false, updatable = false)
private LocalDateTime createdAt;
@LastModifiedDate
@Column(nullable = false)
private LocalDateTime updatedAt;
@CreatedBy
@Column(updatable = false)
private Long createdBy; // String yerine Long
@LastModifiedBy
private Long updatedBy; // String yerine Long
}@Component
public class UserIdAuditorAware implements AuditorAware<Long> {
@Override
public Optional<Long> getCurrentAuditor() {
return Optional.ofNullable(SecurityContextHolder.getContext())
.map(SecurityContext::getAuthentication)
.filter(Authentication::isAuthenticated)
.map(auth -> {
if (auth.getPrincipal() instanceof CustomUserDetails user) {
return user.getId(); // Long userId döner
}
return null;
});
}
}💡 İpucu: User ID ile audit yapmak, username değiştiğinde tutarsızlık yaşamamanızı sağlar. Ancak log okunabilirliği düşer. İkisini birlikte tutmak da bir seçenek: createdById (Long) + createdByName (String).
Tarih/Saat Tipi Seçenekleri
@CreatedDate ve @LastModifiedDate farklı Java tipleriyle kullanılabilir:
@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
public abstract class BaseEntity {
// Seçenek 1: LocalDateTime (en yaygın, timezone bilgisi yok)
@CreatedDate
private LocalDateTime createdAt;
// Seçenek 2: Instant (UTC, timezone-safe — ÖNERİLEN)
@CreatedDate
private Instant createdAt;
// Seçenek 3: ZonedDateTime (timezone bilgisi dahil)
@CreatedDate
private ZonedDateTime createdAt;
// Seçenek 4: Long (epoch millis — eski tarz)
@CreatedDate
private Long createdAt;
// Seçenek 5: java.util.Date (legacy — kullanmayın)
@CreatedDate
private Date createdAt;
}⚠️ Dikkat: Uluslararası uygulamalarda Instant kullanın. LocalDateTime timezone bilgisi taşımaz, farklı sunucularda farklı saatler kayıt olabilir. Instant her zaman UTC'dir ve gösterim anında kullanıcının timezone'una çevrilir.
Bütünleşik Gerçek Dünya Örneği: E-Ticaret Uygulaması
Şimdi tüm parçaları bir araya getiren tam bir örnek görelim:
// ===== BaseEntity =====
@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
public abstract class BaseEntity {
@CreatedDate
@Column(nullable = false, updatable = false)
private Instant createdAt;
@LastModifiedDate
@Column(nullable = false)
private Instant updatedAt;
@CreatedBy
@Column(updatable = false, length = 100)
private String createdBy;
@LastModifiedBy
@Column(length = 100)
private String updatedBy;
// Getter'lar (setter yok — audit alanları dışarıdan set edilmemeli!)
public Instant getCreatedAt() { return createdAt; }
public Instant getUpdatedAt() { return updatedAt; }
public String getCreatedBy() { return createdBy; }
public String getUpdatedBy() { return updatedBy; }
}
// ===== Product Entity =====
@Entity
@Table(name = "products")
public class Product extends BaseEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false)
private String name;
@Column(nullable = false, precision = 10, scale = 2)
private BigDecimal price;
@Column(nullable = false)
@Enumerated(EnumType.STRING)
private ProductStatus status = ProductStatus.DRAFT;
protected Product() {}
public Product(String name, BigDecimal price) {
this.name = name;
this.price = price;
}
// Getter/Setter...
}
// ===== Repository =====
public interface ProductRepository extends JpaRepository<Product, Long> {
List<Product> findByStatus(ProductStatus status);
List<Product> findByCreatedByOrderByCreatedAtDesc(String username);
}
// ===== Service =====
@Service
@RequiredArgsConstructor
public class ProductService {
private final ProductRepository productRepository;
@Transactional
public Product createProduct(String name, BigDecimal price) {
Product product = new Product(name, price);
// createdAt, createdBy otomatik set edilir!
return productRepository.save(product);
}
@Transactional
public Product updatePrice(Long id, BigDecimal newPrice) {
Product product = productRepository.findById(id)
.orElseThrow(() -> new ProductNotFoundException(id));
product.setPrice(newPrice);
// updatedAt, updatedBy otomatik güncellenir!
return productRepository.save(product);
}
public List<Product> getMyProducts(String username) {
return productRepository.findByCreatedByOrderByCreatedAtDesc(username);
}
}
// ===== REST Controller =====
@RestController
@RequestMapping("/api/products")
@RequiredArgsConstructor
public class ProductController {
private final ProductService productService;
@PostMapping
public ResponseEntity<ProductDto> create(@RequestBody CreateProductRequest request) {
Product product = productService.createProduct(request.name(), request.price());
return ResponseEntity.status(HttpStatus.CREATED)
.body(toDto(product));
}
@PutMapping("/{id}/price")
public ProductDto updatePrice(@PathVariable Long id,
@RequestBody UpdatePriceRequest request) {
Product product = productService.updatePrice(id, request.newPrice());
return toDto(product);
}
private ProductDto toDto(Product p) {
return new ProductDto(p.getId(), p.getName(), p.getPrice(),
p.getStatus(), p.getCreatedAt(), p.getUpdatedAt(),
p.getCreatedBy(), p.getUpdatedBy());
}
}Bu yapıda:
Ürün oluşturulduğunda
createdAt,createdBy,updatedAt,updatedByotomatik dolarFiyat güncellendiğinde
updatedAtveupdatedByotomatik güncellenircreatedAtvecreatedByasla değişmez (updatable = false)API response'unda audit bilgileri görünür
Auditing ile Sorgulama
Audit alanları normal JPA alanları gibi sorgulanabilir:
public interface ProductRepository extends JpaRepository<Product, Long> {
// Son 24 saatte oluşturulan ürünler
List<Product> findByCreatedAtAfter(Instant since);
// Belirli kullanıcının oluşturduğu ürünler
List<Product> findByCreatedBy(String username);
// Son güncelleme tarihine göre sıralama
List<Product> findAllByOrderByUpdatedAtDesc();
// Belirli tarih aralığında güncellenen ürünler
@Query("SELECT p FROM Product p WHERE p.updatedAt BETWEEN :start AND :end")
List<Product> findUpdatedBetween(
@Param("start") Instant start,
@Param("end") Instant end
);
// Hiç güncellenmemiş ürünler (createdAt = updatedAt)
@Query("SELECT p FROM Product p WHERE p.createdAt = p.updatedAt")
List<Product> findNeverUpdated();
}@EnableJpaAuditing Gelişmiş Ayarları
@Configuration
@EnableJpaAuditing(
auditorAwareRef = "securityAuditorAware", // AuditorAware bean adı
dateTimeProviderRef = "customDateTimeProvider", // Özel tarih sağlayıcı
modifyOnCreate = true, // Oluşturmada da lastModified set edilsin mi? (default: true)
setDates = true // Tarih alanları set edilsin mi? (default: true)
)
public class AuditConfig {
// Özel DateTimeProvider — test'lerde saati kontrol etmek için
@Bean
public DateTimeProvider customDateTimeProvider() {
return () -> Optional.of(LocalDateTime.now());
}
}Test'lerde DateTimeProvider
Testlerde sabit bir zaman kullanmak isteyebilirsiniz:
@TestConfiguration
public class TestAuditConfig {
@Bean
public DateTimeProvider fixedDateTimeProvider() {
LocalDateTime fixedTime = LocalDateTime.of(2025, 1, 15, 10, 30, 0);
return () -> Optional.of(fixedTime);
}
@Bean
public AuditorAware<String> testAuditorAware() {
return () -> Optional.of("test-user");
}
}Hibernate Alternatifleri: @CreationTimestamp ve @UpdateTimestamp
Spring Data JPA Auditing dışında, Hibernate'in kendi annotation'ları da vardır:
import org.hibernate.annotations.CreationTimestamp;
import org.hibernate.annotations.UpdateTimestamp;
@Entity
public class Article {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String title;
@CreationTimestamp // Hibernate'e özel
private LocalDateTime createdAt;
@UpdateTimestamp // Hibernate'e özel
private LocalDateTime updatedAt;
}Spring Data JPA Auditing vs Hibernate Annotations:
| Özellik | Spring Data JPA | Hibernate |
|---|---|---|
| Tarih otomatik set | ✅ @CreatedDate | ✅ @CreationTimestamp |
| Kullanıcı bilgisi | ✅ @CreatedBy | ❌ Yok |
| JPA Provider bağımsız | ✅ Evet | ❌ Sadece Hibernate |
| Ekstra konfigürasyon | @EnableJpaAuditing gerekli | Gerekmez |
| Test desteği | DateTimeProvider ile | Zor |
💡 İpucu: Sadece tarih takibi yeterliyse ve hızlı bir çözüm istiyorsanız Hibernate annotation'ları kullanabilirsiniz. Ama kullanıcı takibi ve test kontrolü istiyorsanız Spring Data JPA Auditing tercih edin.
Soft Delete ile Auditing Birlikte
Gerçek dünya uygulamalarında veriler genellikle fiziksel olarak silinmez, soft delete uygulanır. Auditing ile soft delete'i birleştirmek yaygın bir pattern'dir:
@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
public abstract class BaseEntity {
@CreatedDate
@Column(nullable = false, updatable = false)
private Instant createdAt;
@LastModifiedDate
@Column(nullable = false)
private Instant updatedAt;
@CreatedBy
@Column(updatable = false)
private String createdBy;
@LastModifiedBy
private String updatedBy;
// Soft delete alanları
@Column(nullable = false)
private boolean deleted = false;
private Instant deletedAt;
private String deletedBy;
// Soft delete metodu
public void softDelete(String deletedByUser) {
this.deleted = true;
this.deletedAt = Instant.now();
this.deletedBy = deletedByUser;
}
// Getter'lar...
}public interface ProductRepository extends JpaRepository<Product, Long> {
// Silinmemiş ürünler
List<Product> findByDeletedFalse();
// Silinmiş ürünleri kimin sildiğine göre bul
List<Product> findByDeletedTrueAndDeletedBy(String username);
}Yaygın Hatalar ve Çözümleri
Hata 1: @EnableJpaAuditing Unutuldu
// ❌ YANLIŞ — @EnableJpaAuditing yok
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
// Sonuç: createdAt, updatedAt hep NULL kalır!// ✅ DOĞRU
@SpringBootApplication
@EnableJpaAuditing
public class Application { ... }Hata 2: @EntityListeners Unutuldu
// ❌ YANLIŞ — @EntityListeners yok
@MappedSuperclass
public abstract class BaseEntity {
@CreatedDate
private LocalDateTime createdAt; // Çalışmaz!
}
// ✅ DOĞRU
@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
public abstract class BaseEntity { ... }Hata 3: AuditorAware Bean'i Tanımlanmadı
// @CreatedBy kullanılıyor ama AuditorAware bean'i yok
// Sonuç: "Required a bean of type 'AuditorAware'" hatası
// Çözüm: AuditorAware implementasyonu oluşturun
@Component
public class SecurityAuditorAware implements AuditorAware<String> {
@Override
public Optional<String> getCurrentAuditor() {
return Optional.of("system");
}
}Hata 4: Audit Alanlarına Setter ile Müdahale
// ❌ YANLIŞ — Audit alanlarını manuel set etmeyin
product.setCreatedAt(LocalDateTime.now()); // Auditing bunu zaten yapıyor!
product.setCreatedBy("admin"); // Bu override edilir
// ✅ DOĞRU — Audit alanları için setter tanımlamayın bile
// BaseEntity'de sadece getter'lar olsunHata 5: Test'lerde Auditing Sorunları
// ❌ Test'te "No qualifying bean of type 'AuditorAware'" hatası
@DataJpaTest
class ProductRepositoryTest {
// @DataJpaTest, AuditorAware bean'ini yüklemez!
}
// ✅ DOĞRU — Test'e import ekleyin
@DataJpaTest
@Import(SecurityAuditorAware.class)
class ProductRepositoryTest { ... }
// veya test configuration kullanın
@DataJpaTest
@Import(TestAuditConfig.class)
class ProductRepositoryTest { ... }Hata 6: @EnableJpaAuditing ve @WebMvcTest Çakışması
// @WebMvcTest sadece web katmanını yükler, JPA'yı yüklemez
// Ama @SpringBootApplication'daki @EnableJpaAuditing JPA gerektirir → HATA!
// Çözüm: @EnableJpaAuditing'i ayrı bir @Configuration'a taşıyın
@Configuration
@EnableJpaAuditing
public class AuditConfig { }
// Ana sınıfta sadece @SpringBootApplication kalsın
@SpringBootApplication
public class Application { }⚠️ Dikkat: Bu en sık karşılaşılan ve en sinir bozucu hatadır. @EnableJpaAuditing'i her zaman ayrı bir @Configuration sınıfına koyun. Böylece @WebMvcTest gibi slice test'lerde sorun çıkmaz.
Performans Değerlendirmesi
JPA Auditing'in performans etkisi neredeyse sıfırdır:
@CreatedDate/@LastModifiedDate: SadeceLocalDateTime.now()çağrısı — nanosaniye seviyesi@CreatedBy/@LastModifiedBy:SecurityContextHolder'dan okuma — microsaniye seviyesiEkstra SQL sorgusu çalıştırmaz — değerler Java tarafında entity'ye set edilir
Audit alanları için index oluşturmayı düşünün:
@Entity
@Table(name = "products", indexes = {
@Index(name = "idx_product_created_at", columnList = "createdAt"),
@Index(name = "idx_product_created_by", columnList = "createdBy"),
@Index(name = "idx_product_updated_at", columnList = "updatedAt")
})
public class Product extends BaseEntity { ... }Özet
JPA Auditing, entity'lerin oluşturulma/güncelleme zamanını ve kimin yaptığını otomatik takip eder
Kurulum:
@EnableJpaAuditing+@EntityListeners(AuditingEntityListener.class)+@MappedSuperclassBaseEntityTarih takibi:
@CreatedDateve@LastModifiedDate— ekstra konfigürasyon gerektirmezKullanıcı takibi:
@CreatedByve@LastModifiedBy—AuditorAwareimplementasyonu gerektirirBaseEntity pattern: Audit alanlarını tek yerde tanımlayın, tüm entity'ler extend etsin
Test'lerde
@EnableJpaAuditing'i ayrı@Configuration'a taşıyın — slice test'lerle çakışmayı önler`updatable = false` ile
createdAtvecreatedBy'ın değişmesini engelleyin
AI Asistan
Sorularını yanıtlamaya hazır