{"id":22544418,"url":"https://github.com/hardiksinghbehl/entity-activity-automator","last_synced_at":"2025-06-13T08:34:50.719Z","repository":{"id":118947391,"uuid":"430977320","full_name":"hardikSinghBehl/entity-activity-automator","owner":"hardikSinghBehl","description":"POC to demonstrate automation of entity fields such as created_at, created_by, updated_by using JPA @Embedded, @Embeddable and ThreadLocal","archived":false,"fork":false,"pushed_at":"2021-12-25T10:46:56.000Z","size":102,"stargazers_count":6,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-04-09T23:51:03.018Z","etag":null,"topics":["automation","java","jpa-embaddable","jpa-embedded","jpa-hibernate","spring-boot","spring-data-jpa","threadlocal"],"latest_commit_sha":null,"homepage":"","language":"Java","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/hardikSinghBehl.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2021-11-23T05:52:22.000Z","updated_at":"2022-02-23T08:02:10.000Z","dependencies_parsed_at":null,"dependency_job_id":"867ab2bf-1c2d-433f-8b08-54c9a22a72d3","html_url":"https://github.com/hardikSinghBehl/entity-activity-automator","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/hardikSinghBehl/entity-activity-automator","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/hardikSinghBehl%2Fentity-activity-automator","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/hardikSinghBehl%2Fentity-activity-automator/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/hardikSinghBehl%2Fentity-activity-automator/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/hardikSinghBehl%2Fentity-activity-automator/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/hardikSinghBehl","download_url":"https://codeload.github.com/hardikSinghBehl/entity-activity-automator/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/hardikSinghBehl%2Fentity-activity-automator/sbom","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":259610521,"owners_count":22884277,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["automation","java","jpa-embaddable","jpa-embedded","jpa-hibernate","spring-boot","spring-data-jpa","threadlocal"],"created_at":"2024-12-07T14:07:14.759Z","updated_at":"2025-06-13T08:34:50.711Z","avatar_url":"https://github.com/hardikSinghBehl.png","language":"Java","funding_links":[],"categories":[],"sub_categories":[],"readme":"## Automating Entity Activity-fields in a Java Spring-boot project\n#### \u003cins\u003eActivity-fields\u003c/ins\u003e in this context are being used to refer to fields such as `created_at`, `created_by`, `updated_at` and `updated_by` that are common in any entity for which CRUD APis are exposed\n#### Approach Implemented using\n* JPA's\n  * @Embedded\n  * @Embeddable \n  * @PrePersist\n  * @PreUpdate\n* ThreadLocal.class\n* Http Request Filters (OncePerRequestFilter.class)\n---\n\n#### [Deployed Application Link](https://entity-activity-automator.herokuapp.com/swagger-ui.html) 🖥🥸\n#### can ⭐️ Repository if found helpful or leave comments in [Discussions](https://github.com/hardikSinghBehl/entity-activity-automator/discussions) 💬😄\n#### Sample Screen-recording added below (120-seconds) 🤝\n\n---\n### Important classes and files\n* [Activity.class](https://github.com/hardikSinghBehl/entity-activity-automator/blob/main/src/main/java/com/behl/freezo/entity/embeddable/Activity.java)\n* [Patient.class](https://github.com/hardikSinghBehl/entity-activity-automator/blob/main/src/main/java/com/behl/freezo/entity/Patient.java)\n* [LoggedInDoctorDetailProvider.class](https://github.com/hardikSinghBehl/entity-activity-automator/blob/main/src/main/java/com/behl/freezo/configuration/security/LoggedInDoctorDetailProvider.java)\n* [LoggedInDoctorDetailStorageFilter.class](https://github.com/hardikSinghBehl/entity-activity-automator/blob/main/src/main/java/com/behl/freezo/configuration/security/filter/LoggedInDoctorDetailStorageFilter.java)\n* [SecurityConfiguration.class](https://github.com/hardikSinghBehl/entity-activity-automator/blob/main/src/main/java/com/behl/freezo/configuration/security/SecurityConfiguration.java)\n* [Migration .sql file](https://github.com/hardikSinghBehl/entity-activity-automator/blob/main/src/main/resources/db/migration/V001__creating_tables.sql)\n---\n### Explanation\n* #### Problem Statement\nFor every entity for which CRUD operation is performed, there are some columns that are repeated for all of them (created_at, created_by etc are used in this POC).\nIn every POST and PUT operation the developer has to expicility provide values in the columns.\n\n* #### Goal\n**EXAMPLE** : If a table contains 5 columns, `full_name` and the 4 activity columns (mentioned above), the code in the service layer would look like the one below:\n```\nvar user = new User();\nuser.setFullName(\"Whatever came from frontend\");\nuser.setCreatedAt(current-time);\nuser.setCreatedBy(id-from-decoding-JWT);\nuser.setUpdatedAt(current-time);\nuser.setUpdatedBy(id-from-decoding-JWT);\n\ndb.save(user);\n```\nThe **goal** of this POC is to convert the above code to the one below, with the values in the remaining column being filled automatically:\n```\nvar user = new User();\nuser.setFullName(\"Whatever came from frontend\");\ndb.save(user);\n```\nwhile the migration scipt remains the same 😄\n```\nCREATE TABLE users (\n  id UUID PRIMARY KEY DEFAULT UUID(),\n  full_name VARCHAR(100) NOT NULL,\n  created_at TIMESTAMP NOT NULL,\n  created_by UUID NOT NULL, \n  updated_at TIMESTAMP NOT NULL,\n  updated_by UUID NOT NULL\n);\n```\n* #### Solution\n1. Create a class `Activity` and annotate it with `@Embeddable`, define the columns that are to be kept common in the required entities. Using @PrePersist and @PreUpdate we'll take care of putting the values in the 4 fields.\n\n```\n@Embeddable\n@Data\npublic class Activity {\n\n    @Column(name = \"created_at\", nullable = false, updatable = false)\n    private LocalDateTime createdAt;\n    @Column(name = \"created_by\", nullable = false, updatable = false)\n    private UUID createdBy;\n    @Column(name = \"updated_at\", nullable = false)\n    private LocalDateTime updatedAt;\n    @Column(name = \"updated_by\", nullable = false)\n    private UUID updatedBy;\n}\n```\n2. Using `@Embedded` annotation, define an instance of the above created class in the Entity classes\n```\n@Entity\n@Table(name = \"patients\")\n@Data\npublic class Patient implements Serializable {\n\n    private static final long serialVersionUID = 7906541761495255102L;\n\n    @Id\n    private UUID id;\n\n    @Column(name = \"full_name\", nullable = false, length = 100)\n    private String fullName;\n\n    @Embedded\n    @Setter(AccessLevel.NONE)\n    private Activity activity = new Activity();\n\n}\n```\n3. Taking care of `created_at` and `updated_at` (timesatmp fields) using JPA's `@PrePersist` and `@PreUpdate` inside the Activity class created in the first step\n```\n@PrePersist\nvoid onCreate() {\n    this.createdAt = LocalDateTime.now(ZoneId.of(\"+00:00\"));\n    this.updatedAt = LocalDateTime.now(ZoneId.of(\"+00:00\"));\n}\n\n@PreUpdate\nvoid onUpdate() {\n    this.updatedAt = LocalDateTime.now(ZoneId.of(\"+00:00\"));\n}\n```\n4. Defining a class with an instance of [ThreadLocal.class](https://www.baeldung.com/java-threadlocal) to store primary-id of logged-in user with `Getters` and `Setters`\n```\npublic class LoggedInDoctorDetailProvider {\n\n    private static final ThreadLocal\u003cUUID\u003e userId = new ThreadLocal\u003cUUID\u003e();\n\n    public static void setUserId(final UUID id) {\n        userId.set(id);\n    }\n\n    public static UUID getId() {\n        return userId.get();\n    }\n\n}\n```\n5. Creating a filter class extending OncePerRequestFilter.class, to intercept the details of logged-in user who's already authenticated in the [JwtAuthenticationFilter.java](https://github.com/hardikSinghBehl/entity-activity-automator/blob/main/src/main/java/com/behl/freezo/configuration/security/filter/JwtAuthenticationFilter.class)\n```\nMake sure the created filter executes after the Authentication filter, in this POC it's added in the filter-chain using Spring-security with addAfter()\n```\nThe primary-id (UUID in this case) is to be stored in the above created class in the ThreadLocal instance, so that only that particular thread can access it (Thread-per-request model is used in a REST-API project) using the defined setter method\n\n```\nif (authentication != null) {\n    final String authorizationHeader = request.getHeader(\"Authorization\");\n    final String token = authorizationHeader.substring(7);\n    final UUID userId = jwtUtils.extractDoctorId(token);\n\n    LoggedInDoctorDetailProvider.setUserId(userId);\n}\n```\n6. Taking care of `created_by` and `updated_by` using JPA's `@PrePersist` and `@PreUpdate` inside the Activity class created in the first step, ThreadLocal instance will provide the primary-id of the authenticated/logged-in user\n```\n@PrePersist\nvoid onCreate() {\n    this.createdBy = LoggedInDoctorDetailProvider.getId();\n    this.updatedBy = LoggedInDoctorDetailProvider.getId();\n}\n\n@PreUpdate\nvoid onUpdate() {\n    this.updatedBy = LoggedInDoctorDetailProvider.getId();\n}\n``` \n#### Recording Demonstration\nhttps://user-images.githubusercontent.com/69693621/143672072-c97f3b59-fbec-45f5-8199-a8c96fa62ed5.mov\n\n\n---\n### Local Setup Guide\n* Install Java 17 (recommended to use [SdkMan](https://sdkman.io))\n```\nsdk install java 17-open\n```\n* Install Maven (recommended to use [SdkMan](https://sdkman.io))\n```\nsdk install maven\n```\n\n* Clone the repo and run the below command in the core\n```\ngit clone https://github.com/hardikSinghBehl/entity-activity-automator.git\n```\n```\nmvn clean install\n```\n\n* To start the application, run any of the below 2 commands in the core\n```\nmvn spring-boot:run \u0026\n```\n\n```\njava -jar target/entity-activity-embeder-0.0.1-SNAPSHOT.jar \u0026\n```\n* Access the swagger-ui\n```\nhttp://localhost:8080/swagger-ui.html\n```\n\n\n#### References\n* [Baeldungs Article on @Embedded and @Embeddable](https://www.baeldung.com/jpa-embedded-embeddable)\n* [Baeldungs Article on ThreadLocal.class](https://www.baeldung.com/java-threadlocal)\n* [JPA Entity life cycle events](https://www.baeldung.com/jpa-entity-lifecycle-events)\n* [OncePerRequestFilter.class](https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/web/filter/OncePerRequestFilter.html)\n* Spring Security Filter chain\n  * [Video Explanation](https://www.youtube.com/watch?v=EeXFwR21J1A)\n  * [Adding custom filters](https://www.baeldung.com/spring-security-custom-filter)\n\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fhardiksinghbehl%2Fentity-activity-automator","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fhardiksinghbehl%2Fentity-activity-automator","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fhardiksinghbehl%2Fentity-activity-automator/lists"}