{"id":2291,"date":"2026-07-20T12:43:14","date_gmt":"2026-07-20T10:43:14","guid":{"rendered":"https:\/\/kindsonthegenius.com\/blog\/jpa-auditing-in-spring-boot-using-mysql-tracking-changes\/"},"modified":"2026-08-26T20:46:12","modified_gmt":"2026-08-26T18:46:12","slug":"jpa-auditing-in-spring-boot-using-mysql-tracking-changes","status":"publish","type":"post","link":"https:\/\/kindsonthegenius.com\/blog\/jpa-auditing-in-spring-boot-using-mysql-tracking-changes\/","title":{"rendered":"JPA Auditing in Spring Boot Using MySQL (Tracking Changes)"},"content":{"rendered":"<p><!-- ktg-updated-banner --><\/p>\n<p><em>Updated August 2026 \u2014 full tutorial restored after the truncated network copy was migrated empty.<\/em><\/p>\n<p>In this tutorial, I will teach you how to implement <strong>JPA Auditing<\/strong> in a Spring Boot application that uses <strong>MySQL<\/strong>. Auditing lets you track who created or changed a row, and when \u2014 without writing that boilerplate on every save.<\/p>\n<p>Previously we covered:<\/p>\n<ul>\n<li><a href=\"https:\/\/kindsonthegenius.com\/blog\/introduction-to-spring-security-a-practical-tutorial\/\"><strong>Part 1<\/strong>: How to add Login to your application using Spring Security<\/a><\/li>\n<li><a href=\"https:\/\/kindsonthegenius.com\/blog\/spring-security-tutorial-storing-user-credential-in-mysql-database\/\"><strong>Part 2<\/strong>: How to store username and password in a MySQL database<\/a><\/li>\n<\/ul>\n<p>A closely related walkthrough (same auditing steps, slightly different framing) is also here: <a href=\"https:\/\/kindsonthegenius.com\/blog\/auditing-in-spring-bootstep-by-step-tutorial\/\">Auditing in Spring Boot (Step by Step)<\/a>.<\/p>\n<p>We will cover:<\/p>\n<ol>\n<li><a href=\"#t1\">What is Auditing in Spring Data JPA?<\/a><\/li>\n<li><a href=\"#t2\">Create an Auditable base class<\/a><\/li>\n<li><a href=\"#t3\">Make your entities extend Auditable<\/a><\/li>\n<li><a href=\"#t4\">Implement the AuditorAware interface<\/a><\/li>\n<li><a href=\"#t5\">Enable JPA Auditing and register the bean<\/a><\/li>\n<li><a href=\"#t6\">MySQL columns and a quick test<\/a><\/li>\n<\/ol>\n<p><strong id=\"t1\">1. What is Auditing in Spring Data JPA?<\/strong><\/p>\n<p>Auditing helps you track changes to a table. For example, you need to know:<\/p>\n<ul>\n<li>who created the record<\/li>\n<li>when it was created<\/li>\n<li>who modified the record last<\/li>\n<li>when it was last modified<\/li>\n<\/ul>\n<p>Before Spring Data JPA auditing, you often added those fields to every entity and set them manually on insert\/update. With auditing enabled, Spring fills them automatically when you save through a repository.<\/p>\n<p>Typical fields (and the annotations that drive them):<\/p>\n<ul>\n<li><code>createdBy<\/code> \u2014 <code>@CreatedBy<\/code><\/li>\n<li><code>createdDate<\/code> \u2014 <code>@CreatedDate<\/code><\/li>\n<li><code>lastModifiedBy<\/code> \u2014 <code>@LastModifiedBy<\/code><\/li>\n<li><code>lastModifiedDate<\/code> \u2014 <code>@LastModifiedDate<\/code><\/li>\n<\/ul>\n<p>With MySQL, those map cleanly to <code>VARCHAR<\/code>\/<code>DATETIME<\/code> (or <code>TIMESTAMP<\/code>) columns on each audited table.<\/p>\n<p><strong id=\"t2\">2. Create an Auditable base class<\/strong><\/p>\n<p>Create an abstract class that holds the audit fields. Any entity you want to audit will extend this class.<\/p>\n<p><strong>Step 1:<\/strong> In your models package, create abstract class <code>Auditable&lt;U&gt;<\/code> with:<\/p>\n<ul>\n<li><code>createdBy<\/code><\/li>\n<li><code>createdDate<\/code><\/li>\n<li><code>lastModifiedBy<\/code><\/li>\n<li><code>lastModifiedDate<\/code><\/li>\n<\/ul>\n<p>For <code>@Temporal(TIMESTAMP)<\/code>, import:<\/p>\n<pre><code>import static javax.persistence.TemporalType.TIMESTAMP;\n<\/code><\/pre>\n<p>(On Jakarta Persistence \/ newer Spring Boot, use <code>jakarta.persistence<\/code> instead of <code>javax.persistence<\/code>.)<\/p>\n<p><strong>Step 2:<\/strong> Annotate each field with the matching Spring Data annotation (<code>@CreatedBy<\/code>, <code>@CreatedDate<\/code>, etc.).<\/p>\n<p><strong>Step 3:<\/strong> Annotate the class with <code>@MappedSuperclass<\/code> (no table of its own).<\/p>\n<p><strong>Step 4:<\/strong> Annotate the class with <code>@EntityListeners(AuditingEntityListener.class)<\/code>.<\/p>\n<p><strong>Step 5:<\/strong> Generate getters and setters (omitted below for brevity).<\/p>\n<pre><code>@MappedSuperclass\n@EntityListeners(AuditingEntityListener.class)\npublic abstract class Auditable&lt;U&gt; {\n\n    @CreatedBy\n    protected U createdBy;\n\n    @CreatedDate\n    @Temporal(TIMESTAMP)\n    protected Date createdDate;\n\n    @LastModifiedBy\n    protected U lastModifiedBy;\n\n    @LastModifiedDate\n    @Temporal(TIMESTAMP)\n    protected Date lastModifiedDate;\n\n    \/\/ getters and setters\n}\n<\/code><\/pre>\n<p>Tip: on modern Spring Data you can also use <code>LocalDateTime<\/code> \/ <code>Instant<\/code> with <code>@CreatedDate<\/code> \/ <code>@LastModifiedDate<\/code> and skip <code>@Temporal<\/code>.<\/p>\n<p><strong id=\"t3\">3. Make your entities extend Auditable<\/strong><\/p>\n<p><strong>Step 1:<\/strong> Change entities such as <code>Post<\/code>, <code>User<\/code>, or <code>Location<\/code> to extend <code>Auditable&lt;String&gt;<\/code> (use <code>String<\/code> if the auditor is a username).<\/p>\n<p><strong>Step 2:<\/strong> Start the application so Hibernate can add the four columns to the MySQL tables (<code>spring.jpa.hibernate.ddl-auto=update<\/code> in dev, or add a Flyway\/Liquibase migration in production).<\/p>\n<p><strong>Step 3:<\/strong> Confirm the columns exist (MySQL Workbench, <code>DESCRIBE your_table;<\/code>, or H2 console if you use H2 locally).<\/p>\n<p><strong id=\"t4\">4. Implement the AuditorAware interface<\/strong><\/p>\n<p>Spring needs a bean that returns the <em>current auditor<\/em> (usually the logged-in username from Spring Security \u2014 see Parts 1 and 2).<\/p>\n<p><strong>Step 1:<\/strong> Create a class that implements <code>AuditorAware&lt;String&gt;<\/code>.<\/p>\n<p><strong>Step 2:<\/strong> Override <code>getCurrentAuditor()<\/code>. For a quick demo you can hard-code a name; for a real app, read the SecurityContext.<\/p>\n<pre><code>public class SpringSecurityAuditorAware implements AuditorAware&lt;String&gt; {\n\n    @Override\n    public Optional&lt;String&gt; getCurrentAuditor() {\n        \/\/ Demo only \u2014 replace with SecurityContextHolder in production\n        return Optional.ofNullable(\"Kindson\").filter(s -&gt; !s.isEmpty());\n    }\n}\n<\/code><\/pre>\n<p>Production-style sketch with Spring Security:<\/p>\n<pre><code>@Override\npublic Optional&lt;String&gt; getCurrentAuditor() {\n    Authentication auth = SecurityContextHolder.getContext().getAuthentication();\n    if (auth == null || !auth.isAuthenticated()) {\n        return Optional.empty();\n    }\n    return Optional.ofNullable(auth.getName());\n}\n<\/code><\/pre>\n<p><strong id=\"t5\">5. Enable JPA Auditing and register the bean<\/strong><\/p>\n<p><strong>Step 1:<\/strong> Annotate a configuration class (often the main application class) with:<\/p>\n<pre><code>@EnableJpaAuditing(auditorAwareRef = \"auditorAware\")\n<\/code><\/pre>\n<p><strong>Step 2:<\/strong> Expose an <code>AuditorAware<\/code> bean:<\/p>\n<pre><code>@EnableJpaAuditing(auditorAwareRef = \"auditorAware\")\n@SpringBootApplication\npublic class RelationshipDemoApplication {\n\n    @Bean\n    public AuditorAware&lt;String&gt; auditorAware() {\n        return new SpringSecurityAuditorAware();\n    }\n\n    public static void main(String[] args) {\n        SpringApplication.run(RelationshipDemoApplication.class, args);\n    }\n}\n<\/code><\/pre>\n<p><strong id=\"t6\">6. MySQL columns and a quick test<\/strong><\/p>\n<p>Example MySQL-oriented column types once Hibernate (or your migration) has run:<\/p>\n<ul>\n<li><code>created_by<\/code> \/ <code>last_modified_by<\/code> \u2014 <code>VARCHAR(255)<\/code><\/li>\n<li><code>created_date<\/code> \/ <code>last_modified_date<\/code> \u2014 <code>DATETIME(6)<\/code> or <code>TIMESTAMP<\/code><\/li>\n<\/ul>\n<p><strong>Test steps:<\/strong><\/p>\n<ol>\n<li>Launch the application connected to MySQL.<\/li>\n<li>Insert or update a row through a REST client (Postman \/ Insomnia) or your UI.<\/li>\n<li>Query the table and confirm the four audit fields are populated.<\/li>\n<\/ol>\n<pre><code>SELECT id, created_by, created_date, last_modified_by, last_modified_date\nFROM your_table\nORDER BY id DESC\nLIMIT 5;\n<\/code><\/pre>\n<p><strong>Next steps<\/strong><\/p>\n<ul>\n<li>Wire <code>AuditorAware<\/code> to the logged-in user from <a href=\"https:\/\/kindsonthegenius.com\/blog\/introduction-to-spring-security-a-practical-tutorial\/\">Spring Security login<\/a> and your <a href=\"https:\/\/kindsonthegenius.com\/blog\/spring-security-tutorial-storing-user-credential-in-mysql-database\/\">MySQL user store<\/a>.<\/li>\n<li>Prefer schema migrations over <code>ddl-auto=update<\/code> in production.<\/li>\n<li>For a full change history (old vs new values), look at Hibernate Envers \u2014 that is beyond basic JPA auditing.<\/li>\n<\/ul>\n","protected":false},"excerpt":{"rendered":"<p>Updated August 2026 \u2014 full tutorial restored after the truncated network copy was migrated empty. In this tutorial, I will teach you how to implement &hellip; <\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"_acf_changed":false,"pagelayer_contact_templates":[],"_pagelayer_content":"","footnotes":""},"categories":[85],"tags":[],"class_list":["post-2291","post","type-post","status-publish","format-standard","hentry","category-java"],"acf":[],"_links":{"self":[{"href":"https:\/\/kindsonthegenius.com\/blog\/wp-json\/wp\/v2\/posts\/2291","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/kindsonthegenius.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/kindsonthegenius.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/kindsonthegenius.com\/blog\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/kindsonthegenius.com\/blog\/wp-json\/wp\/v2\/comments?post=2291"}],"version-history":[{"count":3,"href":"https:\/\/kindsonthegenius.com\/blog\/wp-json\/wp\/v2\/posts\/2291\/revisions"}],"predecessor-version":[{"id":2435,"href":"https:\/\/kindsonthegenius.com\/blog\/wp-json\/wp\/v2\/posts\/2291\/revisions\/2435"}],"wp:attachment":[{"href":"https:\/\/kindsonthegenius.com\/blog\/wp-json\/wp\/v2\/media?parent=2291"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/kindsonthegenius.com\/blog\/wp-json\/wp\/v2\/categories?post=2291"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/kindsonthegenius.com\/blog\/wp-json\/wp\/v2\/tags?post=2291"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}