From 1a43df84f508fd373bcb0ffa38bd6455a02e641d Mon Sep 17 00:00:00 2001 From: Anders Swanson Date: Tue, 11 Mar 2025 13:30:40 -0700 Subject: [PATCH 01/13] Basic builder Signed-off-by: Anders Swanson --- .../pom.xml | 118 ++++++++++++++ .../duality/builder/DualityViewBuilder.java | 72 +++++++++ .../duality/builder/DualityViewScanner.java | 32 ++++ .../builder/JsonRelationalDualityView.java | 14 ++ .../json/duality/builder/RootSnippet.java | 33 ++++ .../json/duality/builder/ViewEntity.java | 152 ++++++++++++++++++ .../JsonRelationalDualityViewRepository.java | 7 + ...leJsonRelationalDualityViewRepository.java | 73 +++++++++ .../spring/json/duality/Application.java | 15 ++ .../json/duality/SpringBootDualityTest.java | 27 ++++ .../json/duality/builder/ViewEntityTest.java | 18 +++ .../spring/json/duality/model/Student.java | 97 +++++++++++ .../src/test/resources/application.yaml | 5 + database/starters/pom.xml | 1 + 14 files changed, 664 insertions(+) create mode 100644 database/starters/oracle-spring-boot-json-relational-duality-views/pom.xml create mode 100644 database/starters/oracle-spring-boot-json-relational-duality-views/src/main/java/com/oracle/spring/json/duality/builder/DualityViewBuilder.java create mode 100644 database/starters/oracle-spring-boot-json-relational-duality-views/src/main/java/com/oracle/spring/json/duality/builder/DualityViewScanner.java create mode 100644 database/starters/oracle-spring-boot-json-relational-duality-views/src/main/java/com/oracle/spring/json/duality/builder/JsonRelationalDualityView.java create mode 100644 database/starters/oracle-spring-boot-json-relational-duality-views/src/main/java/com/oracle/spring/json/duality/builder/RootSnippet.java create mode 100644 database/starters/oracle-spring-boot-json-relational-duality-views/src/main/java/com/oracle/spring/json/duality/builder/ViewEntity.java create mode 100644 database/starters/oracle-spring-boot-json-relational-duality-views/src/main/java/com/oracle/spring/json/duality/repository/JsonRelationalDualityViewRepository.java create mode 100644 database/starters/oracle-spring-boot-json-relational-duality-views/src/main/java/com/oracle/spring/json/duality/repository/SimpleJsonRelationalDualityViewRepository.java create mode 100644 database/starters/oracle-spring-boot-json-relational-duality-views/src/test/java/com/oracle/spring/json/duality/Application.java create mode 100644 database/starters/oracle-spring-boot-json-relational-duality-views/src/test/java/com/oracle/spring/json/duality/SpringBootDualityTest.java create mode 100644 database/starters/oracle-spring-boot-json-relational-duality-views/src/test/java/com/oracle/spring/json/duality/builder/ViewEntityTest.java create mode 100644 database/starters/oracle-spring-boot-json-relational-duality-views/src/test/java/com/oracle/spring/json/duality/model/Student.java create mode 100644 database/starters/oracle-spring-boot-json-relational-duality-views/src/test/resources/application.yaml diff --git a/database/starters/oracle-spring-boot-json-relational-duality-views/pom.xml b/database/starters/oracle-spring-boot-json-relational-duality-views/pom.xml new file mode 100644 index 00000000..f77bbbab --- /dev/null +++ b/database/starters/oracle-spring-boot-json-relational-duality-views/pom.xml @@ -0,0 +1,118 @@ + + + + + 4.0.0 + + oracle-spring-boot-starters + com.oracle.database.spring + 25.2.0 + ../pom.xml + + + oracle-spring-boot-json-relational-duality-views + 25.2.0 + + Oracle Spring Boot - JSON Relational Duality Views + Spring Boot for Oracle Database JSON Relational duality Views + https://github.com/oracle/spring-cloud-oracle/tree/main/database/starters/oracle-spring-boot-json-relational-duality-views + + + Oracle America, Inc. + https://www.oracle.com + + + + + Oracle + obaas_ww at oracle.com + Oracle America, Inc. + https://www.oracle.com + + + + + + The Universal Permissive License (UPL), Version 1.0 + https://oss.oracle.com/licenses/upl/ + repo + + + + + https://github.com/oracle/spring-cloud-oracle + scm:git:https://github.com/oracle/spring-cloud-oracle.git + scm:git:git@github.com:oracle/spring-cloud-oracle.git + + + + + com.oracle.database.spring + oracle-spring-boot-starter-ucp + ${project.version} + + + + org.springframework.boot + spring-boot-starter-jdbc + + + + org.springframework.boot + spring-boot-starter-data-jpa + + + + + jakarta.json + jakarta.json-api + + + + org.eclipse.parsson + parsson + + + + jakarta.json.bind + jakarta.json.bind-api + + + + org.eclipse + yasson + + + + org.testcontainers + junit-jupiter + test + + + + org.testcontainers + testcontainers + test + + + + org.testcontainers + oracle-free + test + + + + org.springframework.boot + spring-boot-starter-test + test + + + + org.springframework.boot + spring-boot-testcontainers + test + + + diff --git a/database/starters/oracle-spring-boot-json-relational-duality-views/src/main/java/com/oracle/spring/json/duality/builder/DualityViewBuilder.java b/database/starters/oracle-spring-boot-json-relational-duality-views/src/main/java/com/oracle/spring/json/duality/builder/DualityViewBuilder.java new file mode 100644 index 00000000..7c586c8d --- /dev/null +++ b/database/starters/oracle-spring-boot-json-relational-duality-views/src/main/java/com/oracle/spring/json/duality/builder/DualityViewBuilder.java @@ -0,0 +1,72 @@ +package com.oracle.spring.json.duality.builder; + +import java.sql.Connection; +import java.sql.SQLException; +import java.sql.Statement; +import java.util.ArrayList; +import java.util.List; + +import javax.sql.DataSource; +import org.springframework.beans.factory.DisposableBean; +import org.springframework.boot.autoconfigure.orm.jpa.HibernateProperties; +import org.springframework.boot.autoconfigure.orm.jpa.JpaProperties; +import org.springframework.stereotype.Component; + +@Component +public final class DualityViewBuilder implements DisposableBean { + private final DataSource dataSource; + private final boolean isShowSql; + private final RootSnippet rootSnippet; + private final List dualityViews = new ArrayList<>(); + + public DualityViewBuilder(DataSource dataSource, + JpaProperties jpaProperties, + HibernateProperties hibernateProperties) { + this.dataSource = dataSource; + this.isShowSql = jpaProperties.isShowSql(); + this.rootSnippet = RootSnippet.fromDdlAuto( + hibernateProperties.getDdlAuto() + ); + } + + void build(Class javaType, JsonRelationalDualityView dvAnnotation) { + if (rootSnippet.equals(RootSnippet.NONE)) { + return; + } + ViewEntity ve = new ViewEntity(javaType, new StringBuilder(), rootSnippet, 0); + String ddl = ve.build().toString(); + if (isShowSql) { + // TODO: log sql statement + } + if (rootSnippet.equals(RootSnippet.VALIDATE)) { + // TODO: handle duality view validation + return; + } + runDDL(ddl); + } + + private void runDDL(String ddl) { + try (Connection conn = dataSource.getConnection(); + Statement stmt = conn.createStatement()) { + stmt.executeUpdate(ddl); + } catch (SQLException e) { + throw new RuntimeException(e); + } + } + + + @Override + public void destroy() throws Exception { + if (rootSnippet.equals(RootSnippet.CREATE_DROP) && !dualityViews.isEmpty()) { + final String dropView = """ + drop view %s + """; + try (Connection conn = dataSource.getConnection(); + Statement stmt = conn.createStatement()) { + for (String view : dualityViews) { + stmt.execute(dropView.formatted(view)); + } + } + } + } +} diff --git a/database/starters/oracle-spring-boot-json-relational-duality-views/src/main/java/com/oracle/spring/json/duality/builder/DualityViewScanner.java b/database/starters/oracle-spring-boot-json-relational-duality-views/src/main/java/com/oracle/spring/json/duality/builder/DualityViewScanner.java new file mode 100644 index 00000000..053f219a --- /dev/null +++ b/database/starters/oracle-spring-boot-json-relational-duality-views/src/main/java/com/oracle/spring/json/duality/builder/DualityViewScanner.java @@ -0,0 +1,32 @@ +package com.oracle.spring.json.duality.builder; + +import java.util.Set; + +import jakarta.annotation.PostConstruct; +import jakarta.persistence.EntityManager; +import jakarta.persistence.metamodel.EntityType; +import org.springframework.stereotype.Component; + +@Component +final public class DualityViewScanner { + private final DualityViewBuilder dualityViewBuilder; + private final EntityManager entityManager; + + public DualityViewScanner(DualityViewBuilder dualityViewBuilder, EntityManager entityManager) { + this.dualityViewBuilder = dualityViewBuilder; + this.entityManager = entityManager; + } + + @PostConstruct + public void scan() { + Set> entities = entityManager.getMetamodel().getEntities(); + for (EntityType entityType : entities) { + Class javaType = entityType.getJavaType(); + JsonRelationalDualityView dvAnnotation = javaType.getAnnotation(JsonRelationalDualityView.class); + if (dvAnnotation != null) { + dualityViewBuilder.build(javaType, dvAnnotation); + } + } + + } +} diff --git a/database/starters/oracle-spring-boot-json-relational-duality-views/src/main/java/com/oracle/spring/json/duality/builder/JsonRelationalDualityView.java b/database/starters/oracle-spring-boot-json-relational-duality-views/src/main/java/com/oracle/spring/json/duality/builder/JsonRelationalDualityView.java new file mode 100644 index 00000000..eeef9217 --- /dev/null +++ b/database/starters/oracle-spring-boot-json-relational-duality-views/src/main/java/com/oracle/spring/json/duality/builder/JsonRelationalDualityView.java @@ -0,0 +1,14 @@ +package com.oracle.spring.json.duality.builder; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Documented +@Target({ElementType.TYPE}) +@Retention(RetentionPolicy.RUNTIME) +public @interface JsonRelationalDualityView { + String name() default ""; +} diff --git a/database/starters/oracle-spring-boot-json-relational-duality-views/src/main/java/com/oracle/spring/json/duality/builder/RootSnippet.java b/database/starters/oracle-spring-boot-json-relational-duality-views/src/main/java/com/oracle/spring/json/duality/builder/RootSnippet.java new file mode 100644 index 00000000..e97b96ec --- /dev/null +++ b/database/starters/oracle-spring-boot-json-relational-duality-views/src/main/java/com/oracle/spring/json/duality/builder/RootSnippet.java @@ -0,0 +1,33 @@ +package com.oracle.spring.json.duality.builder; + +public enum RootSnippet { + NONE(null), + VALIDATE(null), + CREATE("create force editionable json relational duality view"), + CREATE_DROP(CREATE.snippet), + UPDATE("create or replace force editionable json relational duality view"); + private final String snippet; + + RootSnippet(String snippet) { + this.snippet = snippet; + } + + public String getSnippet() { + return snippet; + } + + public static RootSnippet fromDdlAuto(String ddlAuto) { + if (ddlAuto == null) { + return NONE; + } + // none, validate, update, create, and create-drop + return switch (ddlAuto) { + case "none" -> NONE; + case "validate" -> VALIDATE; + case "create" -> CREATE; + case "create_drop" -> CREATE_DROP; + case "update" -> UPDATE; + default -> throw new IllegalStateException("Unexpected value: " + ddlAuto); + }; + } +} diff --git a/database/starters/oracle-spring-boot-json-relational-duality-views/src/main/java/com/oracle/spring/json/duality/builder/ViewEntity.java b/database/starters/oracle-spring-boot-json-relational-duality-views/src/main/java/com/oracle/spring/json/duality/builder/ViewEntity.java new file mode 100644 index 00000000..98ac8656 --- /dev/null +++ b/database/starters/oracle-spring-boot-json-relational-duality-views/src/main/java/com/oracle/spring/json/duality/builder/ViewEntity.java @@ -0,0 +1,152 @@ +package com.oracle.spring.json.duality.builder; + +import java.lang.reflect.Field; + +import jakarta.json.bind.annotation.JsonbProperty; +import jakarta.persistence.Column; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import org.springframework.util.StringUtils; + +final class ViewEntity { + private static final String _ID_FIELD = "_id"; + private static final String SEPARATOR = " : "; + private static final String TRAILER = "}"; + private static final int TAB_WIDTH = 2; + + private final Class javaType; + private final StringBuilder sb; + private final RootSnippet rootSnippet; + private int nesting; + + ViewEntity(Class javaType, StringBuilder sb, RootSnippet rootSnippet, int nesting) { + this.javaType = javaType; + this.sb = sb; + this.rootSnippet = rootSnippet; + this.nesting = nesting; + } + + ViewEntity build() { + if (rootSnippet != null) { + JsonRelationalDualityView dvAnnotation = javaType.getAnnotation(JsonRelationalDualityView.class); + Table tableAnnotation = javaType.getAnnotation(Table.class); + sb.append(getStatementPrefix(javaType, dvAnnotation, tableAnnotation)); + } + + incNesting(); + for (Field f : javaType.getDeclaredFields()) { + parseField(f); + } + decNesting(); + addTrailer(); + return this; + } + + private String getStatementPrefix(Class javaType, + JsonRelationalDualityView dvAnnotation, + Table tableAnnotation) { + String viewName = getViewName(javaType, dvAnnotation, tableAnnotation); + String tableName = getTableName(javaType, tableAnnotation); + return "%s %s as %s @insert @update @delete {\n".formatted( + rootSnippet.getSnippet(), viewName, tableName + ); + } + + private String getViewName(Class javaType, + JsonRelationalDualityView dvAnnotation, + Table tableAnnotation) { + final String suffix = "_dv"; + if (dvAnnotation != null && StringUtils.hasText(dvAnnotation.name())) { + return dvAnnotation.name(); + } + if (tableAnnotation != null && StringUtils.hasText(tableAnnotation.name())) { + return tableAnnotation.name() + suffix; + } + return javaType.getName() + suffix; + } + + private String getTableName(Class javaType, Table tableAnnotation) { + if (tableAnnotation != null && StringUtils.hasText(tableAnnotation.name())) { + return tableAnnotation.name(); + } + return javaType.getName(); + } + + private void parseField(Field f) { + Id id = f.getAnnotation(Id.class); + if (id != null && rootSnippet != null) { + parseId(f); + } else { + parseColumn(f); + } + } + + private void parseId(Field f) { + String jsonbPropertyName = getJsonbPropertyName(f); + if (!jsonbPropertyName.equals(_ID_FIELD)) { + throw new IllegalArgumentException("@Id Field %s must be named \"%s\" or annotated with @%s(\"%s\")".formatted( + f.getName(), + _ID_FIELD, + JsonbProperty.class.getSimpleName(), + _ID_FIELD + )); + } + addProperty(_ID_FIELD, getDatabaseColumnName(f)); + } + + private void parseColumn(Field f) { + addProperty(getJsonbPropertyName(f), getDatabaseColumnName(f)); + } + + private String getJsonbPropertyName(Field f) { + JsonbProperty jsonbProperty = f.getAnnotation(JsonbProperty.class); + if (jsonbProperty == null || !StringUtils.hasText(jsonbProperty.value())) { + return f.getName(); + } + return jsonbProperty.value(); + } + + private String getDatabaseColumnName(Field f) { + Column column = f.getAnnotation(Column.class); + if (column != null && StringUtils.hasText(column.name())) { + return column.name(); + } + return f.getName(); + } + + private void addProperty(String jsonbPropertyName, String databaseColumnName) { + sb.append(getPadding()); + if (jsonbPropertyName.equals(databaseColumnName)) { + sb.append(jsonbPropertyName); + } else { + sb.append(jsonbPropertyName) + .append(SEPARATOR) + .append(databaseColumnName); + } + sb.append("\n"); + } + + private void addTrailer() { + if (nesting > 0) { + sb.append(getPadding()); + } + sb.append(TRAILER); + } + + private String getPadding() { + return String.format("%" + nesting + "s", " "); + } + + private void incNesting() { + nesting += TAB_WIDTH; + } + + private void decNesting() { + nesting -= TAB_WIDTH; + } + + @Override + public String toString() { + return sb.toString(); + } +} diff --git a/database/starters/oracle-spring-boot-json-relational-duality-views/src/main/java/com/oracle/spring/json/duality/repository/JsonRelationalDualityViewRepository.java b/database/starters/oracle-spring-boot-json-relational-duality-views/src/main/java/com/oracle/spring/json/duality/repository/JsonRelationalDualityViewRepository.java new file mode 100644 index 00000000..d805e668 --- /dev/null +++ b/database/starters/oracle-spring-boot-json-relational-duality-views/src/main/java/com/oracle/spring/json/duality/repository/JsonRelationalDualityViewRepository.java @@ -0,0 +1,7 @@ +package com.oracle.spring.json.duality.repository; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.repository.ListCrudRepository; + +public interface JsonRelationalDualityViewRepository extends ListCrudRepository { +} diff --git a/database/starters/oracle-spring-boot-json-relational-duality-views/src/main/java/com/oracle/spring/json/duality/repository/SimpleJsonRelationalDualityViewRepository.java b/database/starters/oracle-spring-boot-json-relational-duality-views/src/main/java/com/oracle/spring/json/duality/repository/SimpleJsonRelationalDualityViewRepository.java new file mode 100644 index 00000000..680d4f5d --- /dev/null +++ b/database/starters/oracle-spring-boot-json-relational-duality-views/src/main/java/com/oracle/spring/json/duality/repository/SimpleJsonRelationalDualityViewRepository.java @@ -0,0 +1,73 @@ +package com.oracle.spring.json.duality.repository; + +import java.util.List; +import java.util.Optional; + +import org.springframework.stereotype.Repository; +import org.springframework.transaction.annotation.Transactional; + +@Repository +@Transactional( + readOnly = true +) +public class SimpleJsonRelationalDualityViewRepository implements JsonRelationalDualityViewRepository { + @Override + public S save(S entity) { + return null; + } + + @Override + public List saveAll(Iterable entities) { + return List.of(); + } + + @Override + public Optional findById(ID id) { + return Optional.empty(); + } + + @Override + public boolean existsById(ID id) { + return false; + } + + @Override + public List findAll() { + return List.of(); + } + + @Override + public List findAllById(Iterable ids) { + return List.of(); + } + + @Override + public long count() { + return 0; + } + + @Override + public void deleteById(ID id) { + + } + + @Override + public void delete(T entity) { + + } + + @Override + public void deleteAllById(Iterable ids) { + + } + + @Override + public void deleteAll(Iterable entities) { + + } + + @Override + public void deleteAll() { + + } +} diff --git a/database/starters/oracle-spring-boot-json-relational-duality-views/src/test/java/com/oracle/spring/json/duality/Application.java b/database/starters/oracle-spring-boot-json-relational-duality-views/src/test/java/com/oracle/spring/json/duality/Application.java new file mode 100644 index 00000000..de624c20 --- /dev/null +++ b/database/starters/oracle-spring-boot-json-relational-duality-views/src/test/java/com/oracle/spring/json/duality/Application.java @@ -0,0 +1,15 @@ +package com.oracle.spring.json.duality; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.autoconfigure.domain.EntityScan; + +@SpringBootApplication +@EntityScan(basePackages = { + "com.oracle.spring.json.duality.model" +}) +public class Application { + public static void main(String[] args) { + SpringApplication.run(Application.class, args); + } +} diff --git a/database/starters/oracle-spring-boot-json-relational-duality-views/src/test/java/com/oracle/spring/json/duality/SpringBootDualityTest.java b/database/starters/oracle-spring-boot-json-relational-duality-views/src/test/java/com/oracle/spring/json/duality/SpringBootDualityTest.java new file mode 100644 index 00000000..7b259425 --- /dev/null +++ b/database/starters/oracle-spring-boot-json-relational-duality-views/src/test/java/com/oracle/spring/json/duality/SpringBootDualityTest.java @@ -0,0 +1,27 @@ +package com.oracle.spring.json.duality; + +import java.time.Duration; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.testcontainers.service.connection.ServiceConnection; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; +import org.testcontainers.oracle.OracleContainer; + +@SpringBootTest +@Testcontainers +public class SpringBootDualityTest { + /** + * Use a containerized Oracle Database instance for testing. + */ + @Container + @ServiceConnection + static OracleContainer oracleContainer = new OracleContainer("gvenzl/oracle-free:23.6-slim-faststart") + .withStartupTimeout(Duration.ofMinutes(5)) + .withUsername("testuser") + .withPassword("testpwd"); + + @Test + void contextLoads() {} +} diff --git a/database/starters/oracle-spring-boot-json-relational-duality-views/src/test/java/com/oracle/spring/json/duality/builder/ViewEntityTest.java b/database/starters/oracle-spring-boot-json-relational-duality-views/src/test/java/com/oracle/spring/json/duality/builder/ViewEntityTest.java new file mode 100644 index 00000000..c53d1c67 --- /dev/null +++ b/database/starters/oracle-spring-boot-json-relational-duality-views/src/test/java/com/oracle/spring/json/duality/builder/ViewEntityTest.java @@ -0,0 +1,18 @@ +package com.oracle.spring.json.duality.builder; + +import com.oracle.spring.json.duality.model.Student; +import org.junit.jupiter.api.Test; + +public class ViewEntityTest { + @Test + public void studentView() { + ViewEntity ve = testVE(Student.class); + String sql = ve.build().toString(); + + System.out.println(sql); + } + + private ViewEntity testVE(Class javaType) { + return new ViewEntity(javaType, new StringBuilder(), RootSnippet.CREATE, 0); + } +} diff --git a/database/starters/oracle-spring-boot-json-relational-duality-views/src/test/java/com/oracle/spring/json/duality/model/Student.java b/database/starters/oracle-spring-boot-json-relational-duality-views/src/test/java/com/oracle/spring/json/duality/model/Student.java new file mode 100644 index 00000000..ec759166 --- /dev/null +++ b/database/starters/oracle-spring-boot-json-relational-duality-views/src/test/java/com/oracle/spring/json/duality/model/Student.java @@ -0,0 +1,97 @@ +// Copyright (c) 2024, Oracle and/or its affiliates. +// Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl. +package com.oracle.spring.json.duality.model; + +import com.oracle.spring.json.duality.builder.JsonRelationalDualityView; +import jakarta.json.bind.annotation.JsonbProperty; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Table; + +@Entity +@Table(name = "STUDENT") +@JsonRelationalDualityView +public class Student { + @JsonbProperty("_id") + @Id + @GeneratedValue(strategy = GenerationType.UUID) + private String id; + @Column(name = "first_name") + private String firstName; + @Column(name = "last_name") + private String lastName; + private String email; + private String major; + private double credits; + private double gpa; + + public Student() {} + + public Student(String firstName, String lastName, String email, String major, double credits, double gpa) { + this.firstName = firstName; + this.lastName = lastName; + this.email = email; + this.major = major; + this.credits = credits; + this.gpa = gpa; + } + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getFirstName() { + return firstName; + } + + public void setFirstName(String firstName) { + this.firstName = firstName; + } + + public String getLastName() { + return lastName; + } + + public void setLastName(String lastName) { + this.lastName = lastName; + } + + public String getEmail() { + return email; + } + + public void setEmail(String email) { + this.email = email; + } + + public String getMajor() { + return major; + } + + public void setMajor(String major) { + this.major = major; + } + + public double getCredits() { + return credits; + } + + public void setCredits(double credits) { + this.credits = credits; + } + + public double getGpa() { + return gpa; + } + + public void setGpa(double gpa) { + this.gpa = gpa; + } +} diff --git a/database/starters/oracle-spring-boot-json-relational-duality-views/src/test/resources/application.yaml b/database/starters/oracle-spring-boot-json-relational-duality-views/src/test/resources/application.yaml new file mode 100644 index 00000000..39a461f1 --- /dev/null +++ b/database/starters/oracle-spring-boot-json-relational-duality-views/src/test/resources/application.yaml @@ -0,0 +1,5 @@ +spring: + jpa: + hibernate: + ddl-auto: create + show-sql: true diff --git a/database/starters/pom.xml b/database/starters/pom.xml index 0d7e0911..edb709c5 100644 --- a/database/starters/pom.xml +++ b/database/starters/pom.xml @@ -53,6 +53,7 @@ + oracle-spring-boot-json-relational-duality-views oracle-spring-boot-starter-ucp oracle-spring-boot-starter-wallet oracle-spring-boot-starter-aqjms From 976b8018b5a5ed4fdde89118ef69c74b8c30b592 Mon Sep 17 00:00:00 2001 From: Anders Swanson Date: Tue, 11 Mar 2025 13:38:36 -0700 Subject: [PATCH 02/13] refactor modules Signed-off-by: Anders Swanson --- .../pom.xml | 124 ++++++++++++++++++ .../JsonCollectionsAutoConfiguration.java | 0 .../com/oracle/spring/json/jsonb/JSONB.java | 0 .../spring/json/jsonb/JSONBRowMapper.java | 0 .../main/resources/META-INF/spring.factories | 0 ...ot.autoconfigure.AutoConfiguration.imports | 0 .../oracle/spring/json/JsonCollectionsIT.java | 2 +- .../oracle/spring/json/jsonb/JSONBTest.java | 0 .../com/oracle/spring/json/test/Student.java | 0 .../spring/json/test/StudentDetails.java | 0 .../pom.xml | 28 +--- .../pom.xml | 63 +-------- database/starters/pom.xml | 1 + 13 files changed, 128 insertions(+), 90 deletions(-) create mode 100644 database/starters/oracle-spring-boot-json-data-tools/pom.xml rename database/starters/{oracle-spring-boot-starter-json-collections => oracle-spring-boot-json-data-tools}/src/main/java/com/oracle/spring/json/JsonCollectionsAutoConfiguration.java (100%) rename database/starters/{oracle-spring-boot-starter-json-collections => oracle-spring-boot-json-data-tools}/src/main/java/com/oracle/spring/json/jsonb/JSONB.java (100%) rename database/starters/{oracle-spring-boot-starter-json-collections => oracle-spring-boot-json-data-tools}/src/main/java/com/oracle/spring/json/jsonb/JSONBRowMapper.java (100%) rename database/starters/{oracle-spring-boot-starter-json-collections => oracle-spring-boot-json-data-tools}/src/main/resources/META-INF/spring.factories (100%) rename database/starters/{oracle-spring-boot-starter-json-collections => oracle-spring-boot-json-data-tools}/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports (100%) rename database/starters/{oracle-spring-boot-starter-json-collections => oracle-spring-boot-json-data-tools}/src/test/java/com/oracle/spring/json/JsonCollectionsIT.java (100%) rename database/starters/{oracle-spring-boot-starter-json-collections => oracle-spring-boot-json-data-tools}/src/test/java/com/oracle/spring/json/jsonb/JSONBTest.java (100%) rename database/starters/{oracle-spring-boot-starter-json-collections => oracle-spring-boot-json-data-tools}/src/test/java/com/oracle/spring/json/test/Student.java (100%) rename database/starters/{oracle-spring-boot-starter-json-collections => oracle-spring-boot-json-data-tools}/src/test/java/com/oracle/spring/json/test/StudentDetails.java (100%) diff --git a/database/starters/oracle-spring-boot-json-data-tools/pom.xml b/database/starters/oracle-spring-boot-json-data-tools/pom.xml new file mode 100644 index 00000000..f2348536 --- /dev/null +++ b/database/starters/oracle-spring-boot-json-data-tools/pom.xml @@ -0,0 +1,124 @@ + + + + + 4.0.0 + + oracle-spring-boot-starters + com.oracle.database.spring + 25.2.0 + ../pom.xml + + + oracle-spring-boot-json-data-tools + 25.2.0 + + Oracle Spring Boot - JSON Data Tools + Spring Boot for Oracle Database JSON Data Tools + https://github.com/oracle/spring-cloud-oracle/tree/main/database/starters/oracle-spring-boot-json-data-tools + + + Oracle America, Inc. + https://www.oracle.com + + + + + Oracle + obaas_ww at oracle.com + Oracle America, Inc. + https://www.oracle.com + + + + + + The Universal Permissive License (UPL), Version 1.0 + https://oss.oracle.com/licenses/upl/ + repo + + + + + https://github.com/oracle/spring-cloud-oracle + scm:git:https://github.com/oracle/spring-cloud-oracle.git + scm:git:git@github.com:oracle/spring-cloud-oracle.git + + + + + com.oracle.database.spring + oracle-spring-boot-starter-ucp + ${project.version} + + + + org.springframework.boot + spring-boot-starter-jdbc + + + + jakarta.json + jakarta.json-api + + + + org.eclipse.parsson + parsson + + + + jakarta.json.bind + jakarta.json.bind-api + + + + org.eclipse + yasson + + + + org.testcontainers + junit-jupiter + test + + + + org.testcontainers + testcontainers + test + + + + org.testcontainers + oracle-free + test + + + + org.springframework.boot + spring-boot-starter-test + test + + + + org.springframework.boot + spring-boot-testcontainers + test + + + + org.springframework.boot + spring-boot-starter-data-jdbc + test + + + + org.projectlombok + lombok + test + + + diff --git a/database/starters/oracle-spring-boot-starter-json-collections/src/main/java/com/oracle/spring/json/JsonCollectionsAutoConfiguration.java b/database/starters/oracle-spring-boot-json-data-tools/src/main/java/com/oracle/spring/json/JsonCollectionsAutoConfiguration.java similarity index 100% rename from database/starters/oracle-spring-boot-starter-json-collections/src/main/java/com/oracle/spring/json/JsonCollectionsAutoConfiguration.java rename to database/starters/oracle-spring-boot-json-data-tools/src/main/java/com/oracle/spring/json/JsonCollectionsAutoConfiguration.java diff --git a/database/starters/oracle-spring-boot-starter-json-collections/src/main/java/com/oracle/spring/json/jsonb/JSONB.java b/database/starters/oracle-spring-boot-json-data-tools/src/main/java/com/oracle/spring/json/jsonb/JSONB.java similarity index 100% rename from database/starters/oracle-spring-boot-starter-json-collections/src/main/java/com/oracle/spring/json/jsonb/JSONB.java rename to database/starters/oracle-spring-boot-json-data-tools/src/main/java/com/oracle/spring/json/jsonb/JSONB.java diff --git a/database/starters/oracle-spring-boot-starter-json-collections/src/main/java/com/oracle/spring/json/jsonb/JSONBRowMapper.java b/database/starters/oracle-spring-boot-json-data-tools/src/main/java/com/oracle/spring/json/jsonb/JSONBRowMapper.java similarity index 100% rename from database/starters/oracle-spring-boot-starter-json-collections/src/main/java/com/oracle/spring/json/jsonb/JSONBRowMapper.java rename to database/starters/oracle-spring-boot-json-data-tools/src/main/java/com/oracle/spring/json/jsonb/JSONBRowMapper.java diff --git a/database/starters/oracle-spring-boot-starter-json-collections/src/main/resources/META-INF/spring.factories b/database/starters/oracle-spring-boot-json-data-tools/src/main/resources/META-INF/spring.factories similarity index 100% rename from database/starters/oracle-spring-boot-starter-json-collections/src/main/resources/META-INF/spring.factories rename to database/starters/oracle-spring-boot-json-data-tools/src/main/resources/META-INF/spring.factories diff --git a/database/starters/oracle-spring-boot-starter-json-collections/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/database/starters/oracle-spring-boot-json-data-tools/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports similarity index 100% rename from database/starters/oracle-spring-boot-starter-json-collections/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports rename to database/starters/oracle-spring-boot-json-data-tools/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports diff --git a/database/starters/oracle-spring-boot-starter-json-collections/src/test/java/com/oracle/spring/json/JsonCollectionsIT.java b/database/starters/oracle-spring-boot-json-data-tools/src/test/java/com/oracle/spring/json/JsonCollectionsIT.java similarity index 100% rename from database/starters/oracle-spring-boot-starter-json-collections/src/test/java/com/oracle/spring/json/JsonCollectionsIT.java rename to database/starters/oracle-spring-boot-json-data-tools/src/test/java/com/oracle/spring/json/JsonCollectionsIT.java index b085973d..ccd6e5c3 100644 --- a/database/starters/oracle-spring-boot-starter-json-collections/src/test/java/com/oracle/spring/json/JsonCollectionsIT.java +++ b/database/starters/oracle-spring-boot-json-data-tools/src/test/java/com/oracle/spring/json/JsonCollectionsIT.java @@ -2,7 +2,6 @@ // Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl. package com.oracle.spring.json; -import javax.sql.DataSource; import java.sql.PreparedStatement; import java.time.Duration; import java.util.List; @@ -12,6 +11,7 @@ import com.oracle.spring.json.jsonb.JSONBRowMapper; import com.oracle.spring.json.test.Student; import com.oracle.spring.json.test.StudentDetails; +import javax.sql.DataSource; import oracle.jdbc.OracleTypes; import oracle.ucp.jdbc.PoolDataSource; import oracle.ucp.jdbc.PoolDataSourceFactory; diff --git a/database/starters/oracle-spring-boot-starter-json-collections/src/test/java/com/oracle/spring/json/jsonb/JSONBTest.java b/database/starters/oracle-spring-boot-json-data-tools/src/test/java/com/oracle/spring/json/jsonb/JSONBTest.java similarity index 100% rename from database/starters/oracle-spring-boot-starter-json-collections/src/test/java/com/oracle/spring/json/jsonb/JSONBTest.java rename to database/starters/oracle-spring-boot-json-data-tools/src/test/java/com/oracle/spring/json/jsonb/JSONBTest.java diff --git a/database/starters/oracle-spring-boot-starter-json-collections/src/test/java/com/oracle/spring/json/test/Student.java b/database/starters/oracle-spring-boot-json-data-tools/src/test/java/com/oracle/spring/json/test/Student.java similarity index 100% rename from database/starters/oracle-spring-boot-starter-json-collections/src/test/java/com/oracle/spring/json/test/Student.java rename to database/starters/oracle-spring-boot-json-data-tools/src/test/java/com/oracle/spring/json/test/Student.java diff --git a/database/starters/oracle-spring-boot-starter-json-collections/src/test/java/com/oracle/spring/json/test/StudentDetails.java b/database/starters/oracle-spring-boot-json-data-tools/src/test/java/com/oracle/spring/json/test/StudentDetails.java similarity index 100% rename from database/starters/oracle-spring-boot-starter-json-collections/src/test/java/com/oracle/spring/json/test/StudentDetails.java rename to database/starters/oracle-spring-boot-json-data-tools/src/test/java/com/oracle/spring/json/test/StudentDetails.java diff --git a/database/starters/oracle-spring-boot-json-relational-duality-views/pom.xml b/database/starters/oracle-spring-boot-json-relational-duality-views/pom.xml index f77bbbab..c4ba1e78 100644 --- a/database/starters/oracle-spring-boot-json-relational-duality-views/pom.xml +++ b/database/starters/oracle-spring-boot-json-relational-duality-views/pom.xml @@ -50,41 +50,15 @@ com.oracle.database.spring - oracle-spring-boot-starter-ucp + oracle-spring-boot-json-data-tools ${project.version} - - org.springframework.boot - spring-boot-starter-jdbc - - org.springframework.boot spring-boot-starter-data-jpa - - - jakarta.json - jakarta.json-api - - - - org.eclipse.parsson - parsson - - - - jakarta.json.bind - jakarta.json.bind-api - - - - org.eclipse - yasson - - org.testcontainers junit-jupiter diff --git a/database/starters/oracle-spring-boot-starter-json-collections/pom.xml b/database/starters/oracle-spring-boot-starter-json-collections/pom.xml index 4cb6043e..23d0a480 100644 --- a/database/starters/oracle-spring-boot-starter-json-collections/pom.xml +++ b/database/starters/oracle-spring-boot-starter-json-collections/pom.xml @@ -50,69 +50,8 @@ com.oracle.database.spring - oracle-spring-boot-starter-ucp + oracle-spring-boot-json-data-tools ${project.version} - - - org.springframework.boot - spring-boot-starter-jdbc - - - - jakarta.json - jakarta.json-api - - - - org.eclipse.parsson - parsson - - - - jakarta.json.bind - jakarta.json.bind-api - - - - org.eclipse - yasson - - - - org.testcontainers - junit-jupiter - test - - - - org.testcontainers - testcontainers - test - - - - org.testcontainers - oracle-free - test - - - - org.springframework.boot - spring-boot-starter-test - test - - - - org.springframework.boot - spring-boot-starter-data-jdbc - test - - - - org.projectlombok - lombok - test - diff --git a/database/starters/pom.xml b/database/starters/pom.xml index edb709c5..4d9e0533 100644 --- a/database/starters/pom.xml +++ b/database/starters/pom.xml @@ -54,6 +54,7 @@ oracle-spring-boot-json-relational-duality-views + oracle-spring-boot-json-data-tools oracle-spring-boot-starter-ucp oracle-spring-boot-starter-wallet oracle-spring-boot-starter-aqjms From 793fffa51bb189fd6b1bd654cf96547ef65fc4fc Mon Sep 17 00:00:00 2001 From: Anders Swanson Date: Thu, 13 Mar 2025 15:00:07 -0700 Subject: [PATCH 03/13] Nested views Signed-off-by: Anders Swanson --- .../pom.xml | 6 + .../json/duality/annotation/AccessMode.java | 13 ++ .../JsonRelationalDualityView.java | 4 +- .../JsonRelationalDualityViewEntity.java | 17 ++ .../json/duality/builder/Annotations.java | 129 ++++++++++++++ .../duality/builder/DualityViewBuilder.java | 35 +++- .../duality/builder/DualityViewScanner.java | 8 +- .../json/duality/builder/ViewEntity.java | 161 +++++++++++++----- .../JsonRelationalDualityViewRepository.java | 7 - ...leJsonRelationalDualityViewRepository.java | 73 -------- .../duality/JsonRelationalDualityClient.java | 27 +++ .../json/duality/SpringBootDualityTest.java | 19 +++ .../builder/DualityViewBuilderTest.java | 45 +++++ .../json/duality/builder/ViewEntityTest.java | 18 -- .../json/duality/model/movie/Actor.java | 69 ++++++++ .../json/duality/model/movie/Director.java | 68 ++++++++ .../json/duality/model/movie/DirectorBio.java | 45 +++++ .../json/duality/model/movie/Movie.java | 66 +++++++ .../duality/model/{ => student}/Student.java | 17 +- .../src/test/resources/views/actor-create.sql | 24 +++ .../test/resources/views/student-create.sql | 9 + .../test/resources/views/student-update.sql | 9 + 22 files changed, 711 insertions(+), 158 deletions(-) create mode 100644 database/starters/oracle-spring-boot-json-relational-duality-views/src/main/java/com/oracle/spring/json/duality/annotation/AccessMode.java rename database/starters/oracle-spring-boot-json-relational-duality-views/src/main/java/com/oracle/spring/json/duality/{builder => annotation}/JsonRelationalDualityView.java (77%) create mode 100644 database/starters/oracle-spring-boot-json-relational-duality-views/src/main/java/com/oracle/spring/json/duality/annotation/JsonRelationalDualityViewEntity.java create mode 100644 database/starters/oracle-spring-boot-json-relational-duality-views/src/main/java/com/oracle/spring/json/duality/builder/Annotations.java delete mode 100644 database/starters/oracle-spring-boot-json-relational-duality-views/src/main/java/com/oracle/spring/json/duality/repository/JsonRelationalDualityViewRepository.java delete mode 100644 database/starters/oracle-spring-boot-json-relational-duality-views/src/main/java/com/oracle/spring/json/duality/repository/SimpleJsonRelationalDualityViewRepository.java create mode 100644 database/starters/oracle-spring-boot-json-relational-duality-views/src/test/java/com/oracle/spring/json/duality/JsonRelationalDualityClient.java create mode 100644 database/starters/oracle-spring-boot-json-relational-duality-views/src/test/java/com/oracle/spring/json/duality/builder/DualityViewBuilderTest.java delete mode 100644 database/starters/oracle-spring-boot-json-relational-duality-views/src/test/java/com/oracle/spring/json/duality/builder/ViewEntityTest.java create mode 100644 database/starters/oracle-spring-boot-json-relational-duality-views/src/test/java/com/oracle/spring/json/duality/model/movie/Actor.java create mode 100644 database/starters/oracle-spring-boot-json-relational-duality-views/src/test/java/com/oracle/spring/json/duality/model/movie/Director.java create mode 100644 database/starters/oracle-spring-boot-json-relational-duality-views/src/test/java/com/oracle/spring/json/duality/model/movie/DirectorBio.java create mode 100644 database/starters/oracle-spring-boot-json-relational-duality-views/src/test/java/com/oracle/spring/json/duality/model/movie/Movie.java rename database/starters/oracle-spring-boot-json-relational-duality-views/src/test/java/com/oracle/spring/json/duality/model/{ => student}/Student.java (81%) create mode 100644 database/starters/oracle-spring-boot-json-relational-duality-views/src/test/resources/views/actor-create.sql create mode 100644 database/starters/oracle-spring-boot-json-relational-duality-views/src/test/resources/views/student-create.sql create mode 100644 database/starters/oracle-spring-boot-json-relational-duality-views/src/test/resources/views/student-update.sql diff --git a/database/starters/oracle-spring-boot-json-relational-duality-views/pom.xml b/database/starters/oracle-spring-boot-json-relational-duality-views/pom.xml index c4ba1e78..3cf0fb6e 100644 --- a/database/starters/oracle-spring-boot-json-relational-duality-views/pom.xml +++ b/database/starters/oracle-spring-boot-json-relational-duality-views/pom.xml @@ -88,5 +88,11 @@ spring-boot-testcontainers test + + + org.projectlombok + lombok + test + diff --git a/database/starters/oracle-spring-boot-json-relational-duality-views/src/main/java/com/oracle/spring/json/duality/annotation/AccessMode.java b/database/starters/oracle-spring-boot-json-relational-duality-views/src/main/java/com/oracle/spring/json/duality/annotation/AccessMode.java new file mode 100644 index 00000000..e54277c7 --- /dev/null +++ b/database/starters/oracle-spring-boot-json-relational-duality-views/src/main/java/com/oracle/spring/json/duality/annotation/AccessMode.java @@ -0,0 +1,13 @@ +package com.oracle.spring.json.duality.annotation; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target({}) +@Retention(RetentionPolicy.RUNTIME) +public @interface AccessMode { + boolean insert() default false; + boolean update() default false; + boolean delete() default false; +} diff --git a/database/starters/oracle-spring-boot-json-relational-duality-views/src/main/java/com/oracle/spring/json/duality/builder/JsonRelationalDualityView.java b/database/starters/oracle-spring-boot-json-relational-duality-views/src/main/java/com/oracle/spring/json/duality/annotation/JsonRelationalDualityView.java similarity index 77% rename from database/starters/oracle-spring-boot-json-relational-duality-views/src/main/java/com/oracle/spring/json/duality/builder/JsonRelationalDualityView.java rename to database/starters/oracle-spring-boot-json-relational-duality-views/src/main/java/com/oracle/spring/json/duality/annotation/JsonRelationalDualityView.java index eeef9217..d8d83776 100644 --- a/database/starters/oracle-spring-boot-json-relational-duality-views/src/main/java/com/oracle/spring/json/duality/builder/JsonRelationalDualityView.java +++ b/database/starters/oracle-spring-boot-json-relational-duality-views/src/main/java/com/oracle/spring/json/duality/annotation/JsonRelationalDualityView.java @@ -1,4 +1,4 @@ -package com.oracle.spring.json.duality.builder; +package com.oracle.spring.json.duality.annotation; import java.lang.annotation.Documented; import java.lang.annotation.ElementType; @@ -11,4 +11,6 @@ @Retention(RetentionPolicy.RUNTIME) public @interface JsonRelationalDualityView { String name() default ""; + + AccessMode accessMode() default @AccessMode(); } diff --git a/database/starters/oracle-spring-boot-json-relational-duality-views/src/main/java/com/oracle/spring/json/duality/annotation/JsonRelationalDualityViewEntity.java b/database/starters/oracle-spring-boot-json-relational-duality-views/src/main/java/com/oracle/spring/json/duality/annotation/JsonRelationalDualityViewEntity.java new file mode 100644 index 00000000..62225d1b --- /dev/null +++ b/database/starters/oracle-spring-boot-json-relational-duality-views/src/main/java/com/oracle/spring/json/duality/annotation/JsonRelationalDualityViewEntity.java @@ -0,0 +1,17 @@ +package com.oracle.spring.json.duality.annotation; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Documented +@Target({ElementType.FIELD}) +@Retention(RetentionPolicy.RUNTIME) +public @interface JsonRelationalDualityViewEntity { + String name() default ""; + Class entity(); + + AccessMode accessMode() default @AccessMode(); +} diff --git a/database/starters/oracle-spring-boot-json-relational-duality-views/src/main/java/com/oracle/spring/json/duality/builder/Annotations.java b/database/starters/oracle-spring-boot-json-relational-duality-views/src/main/java/com/oracle/spring/json/duality/builder/Annotations.java new file mode 100644 index 00000000..7f9f67eb --- /dev/null +++ b/database/starters/oracle-spring-boot-json-relational-duality-views/src/main/java/com/oracle/spring/json/duality/builder/Annotations.java @@ -0,0 +1,129 @@ +package com.oracle.spring.json.duality.builder; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Field; +import java.util.Set; + +import com.oracle.spring.json.duality.annotation.AccessMode; +import com.oracle.spring.json.duality.annotation.JsonRelationalDualityView; +import com.oracle.spring.json.duality.annotation.JsonRelationalDualityViewEntity; +import jakarta.json.bind.annotation.JsonbProperty; +import jakarta.persistence.Column; +import jakarta.persistence.JoinTable; +import jakarta.persistence.ManyToMany; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.OneToMany; +import jakarta.persistence.OneToOne; +import jakarta.persistence.Table; +import org.hibernate.mapping.Join; +import org.springframework.util.StringUtils; + +public final class Annotations { + public static final String _ID_FIELD = "_id"; + + static final Set> RELATIONAL_ANNOTATIONS = Set.of( + OneToMany.class, + ManyToOne.class, + OneToOne.class, + ManyToMany.class + ); + + static JoinTable getJoinTableAnnotation( Field f, ManyToMany manyToMany, Class mappedType) { + JoinTable annotation = f.getAnnotation(JoinTable.class); + if (annotation != null) { + return annotation; + } + + String mappedFieldName = manyToMany.mappedBy(); + if (!StringUtils.hasText(mappedFieldName)) { + throw new IllegalArgumentException("Mapped field name is required for inverse join on field " + f.getName()); + } + + for (Field field : mappedType.getDeclaredFields()) { + if (field.getName().equals(mappedFieldName)) { + JoinTable mappedJoinTable = field.getAnnotation(JoinTable.class); + if (mappedJoinTable == null) { + throw new IllegalArgumentException("Mapped field %s does has no JoinTable annotation".formatted( + field.getName() + )); + } + return mappedJoinTable; + } + } + throw new IllegalArgumentException("No JoinTable found for field " + f.getName()); + } + + static String getViewEntityName(Class javaType, + JsonRelationalDualityViewEntity viewEntityAnnotation, + Table tableAnnotation) { + if (viewEntityAnnotation != null && StringUtils.hasText(viewEntityAnnotation.name())) { + return viewEntityAnnotation.name().toLowerCase(); + } + return getTableName(javaType, tableAnnotation).toLowerCase(); + } + + static String getViewName(Class javaType, JsonRelationalDualityView dvAnnotation) { + Table tableAnnotation = javaType.getAnnotation(Table.class); + final String suffix = "_dv"; + if (dvAnnotation != null && StringUtils.hasText(dvAnnotation.name())) { + return dvAnnotation.name().toLowerCase(); + } + if (tableAnnotation != null && StringUtils.hasText(tableAnnotation.name())) { + return tableAnnotation.name().toLowerCase() + suffix; + } + return javaType.getName().toLowerCase() + suffix; + } + + static String getTableName(Class javaType, Table tableAnnotation) { + if (tableAnnotation != null && StringUtils.hasText(tableAnnotation.name())) { + return tableAnnotation.name().toLowerCase(); + } + return javaType.getName().toLowerCase(); + } + + static boolean isRelationalEntity(Field f) { + Annotation[] annotations = f.getAnnotations(); + for (Annotation annotation : annotations) { + if (RELATIONAL_ANNOTATIONS.contains(annotation.annotationType())) { + return true; + } + } + + return false; + } + + + static String getJsonbPropertyName(Field f) { + JsonbProperty jsonbProperty = f.getAnnotation(JsonbProperty.class); + if (jsonbProperty == null || !StringUtils.hasText(jsonbProperty.value())) { + return f.getName(); + } + return jsonbProperty.value(); + } + + static String getDatabaseColumnName(Field f) { + Column column = f.getAnnotation(Column.class); + if (column != null && StringUtils.hasText(column.name())) { + return column.name(); + } + return f.getName(); + } + + static String getAccessModeStr(AccessMode accessMode) { + if (accessMode == null) { + return ""; + } + + StringBuilder sb = new StringBuilder(); + if (accessMode.insert()) { + sb.append("@insert "); + } + if (accessMode.update()) { + sb.append("@update "); + } + if (accessMode.delete()) { + sb.append("@delete "); + } + return sb.toString(); + } +} diff --git a/database/starters/oracle-spring-boot-json-relational-duality-views/src/main/java/com/oracle/spring/json/duality/builder/DualityViewBuilder.java b/database/starters/oracle-spring-boot-json-relational-duality-views/src/main/java/com/oracle/spring/json/duality/builder/DualityViewBuilder.java index 7c586c8d..69d86dd6 100644 --- a/database/starters/oracle-spring-boot-json-relational-duality-views/src/main/java/com/oracle/spring/json/duality/builder/DualityViewBuilder.java +++ b/database/starters/oracle-spring-boot-json-relational-duality-views/src/main/java/com/oracle/spring/json/duality/builder/DualityViewBuilder.java @@ -5,15 +5,22 @@ import java.sql.Statement; import java.util.ArrayList; import java.util.List; +import java.util.Optional; +import com.oracle.spring.json.duality.annotation.JsonRelationalDualityView; import javax.sql.DataSource; import org.springframework.beans.factory.DisposableBean; import org.springframework.boot.autoconfigure.orm.jpa.HibernateProperties; import org.springframework.boot.autoconfigure.orm.jpa.JpaProperties; import org.springframework.stereotype.Component; +import static com.oracle.spring.json.duality.builder.Annotations.getAccessModeStr; +import static com.oracle.spring.json.duality.builder.Annotations.getViewName; + @Component public final class DualityViewBuilder implements DisposableBean { + private static final String PREFIX = "JSON Relational Duality Views: "; + private final DataSource dataSource; private final boolean isShowSql; private final RootSnippet rootSnippet; @@ -29,22 +36,40 @@ public DualityViewBuilder(DataSource dataSource, ); } - void build(Class javaType, JsonRelationalDualityView dvAnnotation) { + void apply(Class javaType) { if (rootSnippet.equals(RootSnippet.NONE)) { return; } - ViewEntity ve = new ViewEntity(javaType, new StringBuilder(), rootSnippet, 0); - String ddl = ve.build().toString(); + String ddl = build(javaType); if (isShowSql) { - // TODO: log sql statement + System.out.println(PREFIX + ddl); } if (rootSnippet.equals(RootSnippet.VALIDATE)) { - // TODO: handle duality view validation + // TODO: Handle view validation. return; } + runDDL(ddl); } + String build(Class javaType) { + JsonRelationalDualityView dvAnnotation = javaType.getAnnotation(JsonRelationalDualityView.class); + if (dvAnnotation == null) { + throw new IllegalArgumentException("%s not found for type %s".formatted( + JsonRelationalDualityView.class.getSimpleName(), javaType.getName()) + ); + } + String viewName = getViewName(javaType, dvAnnotation); + String accessMode = getAccessModeStr(dvAnnotation.accessMode()); + ViewEntity ve = new ViewEntity(javaType, + new StringBuilder(), + rootSnippet, + accessMode, + viewName, + 0); + return ve.build().toString(); + } + private void runDDL(String ddl) { try (Connection conn = dataSource.getConnection(); Statement stmt = conn.createStatement()) { diff --git a/database/starters/oracle-spring-boot-json-relational-duality-views/src/main/java/com/oracle/spring/json/duality/builder/DualityViewScanner.java b/database/starters/oracle-spring-boot-json-relational-duality-views/src/main/java/com/oracle/spring/json/duality/builder/DualityViewScanner.java index 053f219a..8e97d428 100644 --- a/database/starters/oracle-spring-boot-json-relational-duality-views/src/main/java/com/oracle/spring/json/duality/builder/DualityViewScanner.java +++ b/database/starters/oracle-spring-boot-json-relational-duality-views/src/main/java/com/oracle/spring/json/duality/builder/DualityViewScanner.java @@ -2,9 +2,12 @@ import java.util.Set; +import com.oracle.spring.json.duality.annotation.JsonRelationalDualityView; import jakarta.annotation.PostConstruct; import jakarta.persistence.EntityManager; import jakarta.persistence.metamodel.EntityType; +import org.springframework.boot.context.event.ApplicationReadyEvent; +import org.springframework.context.event.EventListener; import org.springframework.stereotype.Component; @Component @@ -17,16 +20,15 @@ public DualityViewScanner(DualityViewBuilder dualityViewBuilder, EntityManager e this.entityManager = entityManager; } - @PostConstruct + @EventListener(ApplicationReadyEvent.class) public void scan() { Set> entities = entityManager.getMetamodel().getEntities(); for (EntityType entityType : entities) { Class javaType = entityType.getJavaType(); JsonRelationalDualityView dvAnnotation = javaType.getAnnotation(JsonRelationalDualityView.class); if (dvAnnotation != null) { - dualityViewBuilder.build(javaType, dvAnnotation); + dualityViewBuilder.apply(javaType); } } - } } diff --git a/database/starters/oracle-spring-boot-json-relational-duality-views/src/main/java/com/oracle/spring/json/duality/builder/ViewEntity.java b/database/starters/oracle-spring-boot-json-relational-duality-views/src/main/java/com/oracle/spring/json/duality/builder/ViewEntity.java index 98ac8656..4f835933 100644 --- a/database/starters/oracle-spring-boot-json-relational-duality-views/src/main/java/com/oracle/spring/json/duality/builder/ViewEntity.java +++ b/database/starters/oracle-spring-boot-json-relational-duality-views/src/main/java/com/oracle/spring/json/duality/builder/ViewEntity.java @@ -1,81 +1,105 @@ package com.oracle.spring.json.duality.builder; import java.lang.reflect.Field; +import java.util.HashSet; +import java.util.Set; +import com.oracle.spring.json.duality.annotation.JsonRelationalDualityViewEntity; import jakarta.json.bind.annotation.JsonbProperty; -import jakarta.persistence.Column; import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.JoinTable; +import jakarta.persistence.ManyToMany; import jakarta.persistence.Table; -import org.springframework.util.StringUtils; + +import static com.oracle.spring.json.duality.builder.Annotations._ID_FIELD; +import static com.oracle.spring.json.duality.builder.Annotations.getAccessModeStr; +import static com.oracle.spring.json.duality.builder.Annotations.getDatabaseColumnName; +import static com.oracle.spring.json.duality.builder.Annotations.getJoinTableAnnotation; +import static com.oracle.spring.json.duality.builder.Annotations.getJsonbPropertyName; +import static com.oracle.spring.json.duality.builder.Annotations.getTableName; +import static com.oracle.spring.json.duality.builder.Annotations.getViewEntityName; +import static com.oracle.spring.json.duality.builder.Annotations.isRelationalEntity; final class ViewEntity { - private static final String _ID_FIELD = "_id"; + private static final String SEPARATOR = " : "; - private static final String TRAILER = "}"; + private static final String END_ENTITY = "}"; + private static final String BEGIN_ENTITY = " {\n"; private static final int TAB_WIDTH = 2; private final Class javaType; private final StringBuilder sb; private final RootSnippet rootSnippet; + private final String accessMode; + private final String viewName; private int nesting; - ViewEntity(Class javaType, StringBuilder sb, RootSnippet rootSnippet, int nesting) { + // Track parent types to prevent stacking of nested types + private final Set> parentTypes = new HashSet<>(); + + ViewEntity(Class javaType, StringBuilder sb, String accessMode, String viewName, int nesting) { + this(javaType, sb, null, accessMode, viewName, nesting); + } + + ViewEntity(Class javaType, StringBuilder sb, RootSnippet rootSnippet, String accessMode, String viewName, int nesting) { this.javaType = javaType; this.sb = sb; this.rootSnippet = rootSnippet; + this.accessMode = accessMode; + this.viewName = viewName; this.nesting = nesting; + parentTypes.add(javaType); + } + + void addParentTypes(Set> parentTypes) { + this.parentTypes.addAll(parentTypes); } ViewEntity build() { + Table tableAnnotation = javaType.getAnnotation(Table.class); + if (rootSnippet != null) { - JsonRelationalDualityView dvAnnotation = javaType.getAnnotation(JsonRelationalDualityView.class); - Table tableAnnotation = javaType.getAnnotation(Table.class); - sb.append(getStatementPrefix(javaType, dvAnnotation, tableAnnotation)); + // Root duality view statement + sb.append(getStatementPrefix(tableAnnotation)); + } else { + sb.append(getPadding()); + sb.append(getNestedEntityPrefix(tableAnnotation)); } incNesting(); for (Field f : javaType.getDeclaredFields()) { parseField(f); } - decNesting(); - addTrailer(); + addTrailer(rootSnippet == null); return this; } - private String getStatementPrefix(Class javaType, - JsonRelationalDualityView dvAnnotation, - Table tableAnnotation) { - String viewName = getViewName(javaType, dvAnnotation, tableAnnotation); + private String getStatementPrefix(Table tableAnnotation) { String tableName = getTableName(javaType, tableAnnotation); - return "%s %s as %s @insert @update @delete {\n".formatted( - rootSnippet.getSnippet(), viewName, tableName + return "%s %s as %s %s{\n".formatted( + rootSnippet.getSnippet(), viewName, tableName, accessMode ); } - private String getViewName(Class javaType, - JsonRelationalDualityView dvAnnotation, - Table tableAnnotation) { - final String suffix = "_dv"; - if (dvAnnotation != null && StringUtils.hasText(dvAnnotation.name())) { - return dvAnnotation.name(); - } - if (tableAnnotation != null && StringUtils.hasText(tableAnnotation.name())) { - return tableAnnotation.name() + suffix; - } - return javaType.getName() + suffix; - } - - private String getTableName(Class javaType, Table tableAnnotation) { - if (tableAnnotation != null && StringUtils.hasText(tableAnnotation.name())) { - return tableAnnotation.name(); - } - return javaType.getName(); + private String getNestedEntityPrefix(Table tableAnnotation) { + String tableName = getTableName(javaType, tableAnnotation); + return "%s : %s %s{\n".formatted( + viewName, tableName, accessMode + ); } private void parseField(Field f) { Id id = f.getAnnotation(Id.class); if (id != null && rootSnippet != null) { parseId(f); + } else if (isRelationalEntity(f)) { + JsonRelationalDualityViewEntity viewEntityAnnotation = f.getAnnotation(JsonRelationalDualityViewEntity.class); + // The entity should not be included in the view. + if (viewEntityAnnotation == null) { + return; + } + parseRelationalEntity(f, viewEntityAnnotation); } else { parseColumn(f); } @@ -94,24 +118,63 @@ private void parseId(Field f) { addProperty(_ID_FIELD, getDatabaseColumnName(f)); } + private void parseRelationalEntity(Field f, JsonRelationalDualityViewEntity viewEntityAnnotation) { + Class entityJavaType = viewEntityAnnotation.entity(); + if (entityJavaType == null) { + throw new IllegalArgumentException("%s %s annotation must include the entity class".formatted( + f.getName(), JsonRelationalDualityViewEntity.class.getSimpleName() + )); + } + + // Prevent stack overflow of circular references. + if (parentTypes.contains(entityJavaType)) { + return; + } + // Add join table if present. + ManyToMany manyToMany = f.getAnnotation(ManyToMany.class); + if (manyToMany != null) { + parseManyToMany(manyToMany, f, entityJavaType); + } + // Add nested entity. + parseNestedEntity(entityJavaType, viewEntityAnnotation); + // Additional trailer for join table if present. + if (manyToMany != null) { + addTrailer(true); + } + } + + private void parseNestedEntity(Class entityJavaType, JsonRelationalDualityViewEntity viewEntityAnnotation) { + Table tableAnnotation = entityJavaType.getAnnotation(Table.class); + String viewEntityName = getViewEntityName(entityJavaType, viewEntityAnnotation, tableAnnotation); + String accessMode = getAccessModeStr(viewEntityAnnotation.accessMode()); + ViewEntity ve = new ViewEntity(entityJavaType, + new StringBuilder(), + accessMode, + viewEntityName, + nesting + ); + ve.addParentTypes(parentTypes); + sb.append(ve.build()); + } + private void parseColumn(Field f) { addProperty(getJsonbPropertyName(f), getDatabaseColumnName(f)); } - private String getJsonbPropertyName(Field f) { - JsonbProperty jsonbProperty = f.getAnnotation(JsonbProperty.class); - if (jsonbProperty == null || !StringUtils.hasText(jsonbProperty.value())) { - return f.getName(); - } - return jsonbProperty.value(); + private void parseManyToMany(ManyToMany manyToMany, Field f, Class entityJavaType) { + JoinTable joinTable = getJoinTableAnnotation(f, manyToMany, entityJavaType); + sb.append(getPadding()); + sb.append(joinTable.name()); + sb.append(BEGIN_ENTITY); + incNesting(); + addJoinColumns(joinTable.joinColumns()); + addJoinColumns(joinTable.inverseJoinColumns()); } - private String getDatabaseColumnName(Field f) { - Column column = f.getAnnotation(Column.class); - if (column != null && StringUtils.hasText(column.name())) { - return column.name(); + private void addJoinColumns(JoinColumn[] joinColumns) { + for (JoinColumn joinColumn : joinColumns) { + addProperty(joinColumn.name(), joinColumn.name()); } - return f.getName(); } private void addProperty(String jsonbPropertyName, String databaseColumnName) { @@ -126,11 +189,15 @@ private void addProperty(String jsonbPropertyName, String databaseColumnName) { sb.append("\n"); } - private void addTrailer() { + private void addTrailer(boolean addNewline) { + decNesting(); if (nesting > 0) { sb.append(getPadding()); } - sb.append(TRAILER); + sb.append(END_ENTITY); + if (addNewline) { + sb.append("\n"); + } } private String getPadding() { diff --git a/database/starters/oracle-spring-boot-json-relational-duality-views/src/main/java/com/oracle/spring/json/duality/repository/JsonRelationalDualityViewRepository.java b/database/starters/oracle-spring-boot-json-relational-duality-views/src/main/java/com/oracle/spring/json/duality/repository/JsonRelationalDualityViewRepository.java deleted file mode 100644 index d805e668..00000000 --- a/database/starters/oracle-spring-boot-json-relational-duality-views/src/main/java/com/oracle/spring/json/duality/repository/JsonRelationalDualityViewRepository.java +++ /dev/null @@ -1,7 +0,0 @@ -package com.oracle.spring.json.duality.repository; - -import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.data.repository.ListCrudRepository; - -public interface JsonRelationalDualityViewRepository extends ListCrudRepository { -} diff --git a/database/starters/oracle-spring-boot-json-relational-duality-views/src/main/java/com/oracle/spring/json/duality/repository/SimpleJsonRelationalDualityViewRepository.java b/database/starters/oracle-spring-boot-json-relational-duality-views/src/main/java/com/oracle/spring/json/duality/repository/SimpleJsonRelationalDualityViewRepository.java deleted file mode 100644 index 680d4f5d..00000000 --- a/database/starters/oracle-spring-boot-json-relational-duality-views/src/main/java/com/oracle/spring/json/duality/repository/SimpleJsonRelationalDualityViewRepository.java +++ /dev/null @@ -1,73 +0,0 @@ -package com.oracle.spring.json.duality.repository; - -import java.util.List; -import java.util.Optional; - -import org.springframework.stereotype.Repository; -import org.springframework.transaction.annotation.Transactional; - -@Repository -@Transactional( - readOnly = true -) -public class SimpleJsonRelationalDualityViewRepository implements JsonRelationalDualityViewRepository { - @Override - public S save(S entity) { - return null; - } - - @Override - public List saveAll(Iterable entities) { - return List.of(); - } - - @Override - public Optional findById(ID id) { - return Optional.empty(); - } - - @Override - public boolean existsById(ID id) { - return false; - } - - @Override - public List findAll() { - return List.of(); - } - - @Override - public List findAllById(Iterable ids) { - return List.of(); - } - - @Override - public long count() { - return 0; - } - - @Override - public void deleteById(ID id) { - - } - - @Override - public void delete(T entity) { - - } - - @Override - public void deleteAllById(Iterable ids) { - - } - - @Override - public void deleteAll(Iterable entities) { - - } - - @Override - public void deleteAll() { - - } -} diff --git a/database/starters/oracle-spring-boot-json-relational-duality-views/src/test/java/com/oracle/spring/json/duality/JsonRelationalDualityClient.java b/database/starters/oracle-spring-boot-json-relational-duality-views/src/test/java/com/oracle/spring/json/duality/JsonRelationalDualityClient.java new file mode 100644 index 00000000..a6f6d792 --- /dev/null +++ b/database/starters/oracle-spring-boot-json-relational-duality-views/src/test/java/com/oracle/spring/json/duality/JsonRelationalDualityClient.java @@ -0,0 +1,27 @@ +package com.oracle.spring.json.duality; + +import com.oracle.spring.json.jsonb.JSONB; +import org.springframework.jdbc.core.simple.JdbcClient; + +public class JsonRelationalDualityClient { + private final JdbcClient jdbcClient; + private final JSONB jsonb; + + public JsonRelationalDualityClient(JdbcClient jdbcClient, JSONB jsonb) { + this.jdbcClient = jdbcClient; + this.jsonb = jsonb; + } + + public T save(T entity) { + byte[] oson = jsonb.toOSON(entity); + + final String sql = """ + + """; + return null; + } + + public T findById(Class entityJavaType, ID id) { + return null; + } +} diff --git a/database/starters/oracle-spring-boot-json-relational-duality-views/src/test/java/com/oracle/spring/json/duality/SpringBootDualityTest.java b/database/starters/oracle-spring-boot-json-relational-duality-views/src/test/java/com/oracle/spring/json/duality/SpringBootDualityTest.java index 7b259425..f2b532f9 100644 --- a/database/starters/oracle-spring-boot-json-relational-duality-views/src/test/java/com/oracle/spring/json/duality/SpringBootDualityTest.java +++ b/database/starters/oracle-spring-boot-json-relational-duality-views/src/test/java/com/oracle/spring/json/duality/SpringBootDualityTest.java @@ -1,10 +1,17 @@ package com.oracle.spring.json.duality; +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; import java.time.Duration; +import com.oracle.spring.json.duality.builder.DualityViewScanner; import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.testcontainers.service.connection.ServiceConnection; +import org.springframework.core.io.ClassPathResource; import org.testcontainers.junit.jupiter.Container; import org.testcontainers.junit.jupiter.Testcontainers; import org.testcontainers.oracle.OracleContainer; @@ -12,6 +19,15 @@ @SpringBootTest @Testcontainers public class SpringBootDualityTest { + public static String readViewFile(String fileName) { + try { + File file = new ClassPathResource(Path.of("views", fileName).toString()).getFile(); + return new String(Files.readAllBytes(file.toPath())); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + /** * Use a containerized Oracle Database instance for testing. */ @@ -22,6 +38,9 @@ public class SpringBootDualityTest { .withUsername("testuser") .withPassword("testpwd"); + @Autowired + private DualityViewScanner dualityViewScanner; + @Test void contextLoads() {} } diff --git a/database/starters/oracle-spring-boot-json-relational-duality-views/src/test/java/com/oracle/spring/json/duality/builder/DualityViewBuilderTest.java b/database/starters/oracle-spring-boot-json-relational-duality-views/src/test/java/com/oracle/spring/json/duality/builder/DualityViewBuilderTest.java new file mode 100644 index 00000000..995d520d --- /dev/null +++ b/database/starters/oracle-spring-boot-json-relational-duality-views/src/test/java/com/oracle/spring/json/duality/builder/DualityViewBuilderTest.java @@ -0,0 +1,45 @@ +package com.oracle.spring.json.duality.builder; + +import java.util.stream.Stream; + +import com.oracle.spring.json.duality.model.movie.Actor; +import com.oracle.spring.json.duality.model.student.Student; +import org.jetbrains.annotations.NotNull; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.springframework.boot.autoconfigure.orm.jpa.HibernateProperties; +import org.springframework.boot.autoconfigure.orm.jpa.JpaProperties; + +import static com.oracle.spring.json.duality.SpringBootDualityTest.readViewFile; +import static org.assertj.core.api.Assertions.assertThat; + +public class DualityViewBuilderTest { + public static @NotNull Stream entityClasses() { + return Stream.of( + Arguments.of(Student.class, "student-update.sql", "update"), + Arguments.of(Student.class, "student-create.sql", "create"), + Arguments.of(Actor.class, "actor-create.sql", "create") + ); + } + + @ParameterizedTest(name = "{1} - {0}") + @MethodSource("entityClasses") + public void buildViews(Class entity, String viewFile, String ddlAuto) { + String expectedView = readViewFile(viewFile); + DualityViewBuilder dualityViewBuilder = getDualityViewBuilder(ddlAuto); + String actualView = dualityViewBuilder.build(entity); + System.out.println(actualView); + assertThat(expectedView).isEqualTo(actualView); + } + + private DualityViewBuilder getDualityViewBuilder(String ddlAuto) { + HibernateProperties hibernateProperties = new HibernateProperties(); + hibernateProperties.setDdlAuto(ddlAuto); + return new DualityViewBuilder( + null, + new JpaProperties(), + hibernateProperties + ); + } +} diff --git a/database/starters/oracle-spring-boot-json-relational-duality-views/src/test/java/com/oracle/spring/json/duality/builder/ViewEntityTest.java b/database/starters/oracle-spring-boot-json-relational-duality-views/src/test/java/com/oracle/spring/json/duality/builder/ViewEntityTest.java deleted file mode 100644 index c53d1c67..00000000 --- a/database/starters/oracle-spring-boot-json-relational-duality-views/src/test/java/com/oracle/spring/json/duality/builder/ViewEntityTest.java +++ /dev/null @@ -1,18 +0,0 @@ -package com.oracle.spring.json.duality.builder; - -import com.oracle.spring.json.duality.model.Student; -import org.junit.jupiter.api.Test; - -public class ViewEntityTest { - @Test - public void studentView() { - ViewEntity ve = testVE(Student.class); - String sql = ve.build().toString(); - - System.out.println(sql); - } - - private ViewEntity testVE(Class javaType) { - return new ViewEntity(javaType, new StringBuilder(), RootSnippet.CREATE, 0); - } -} diff --git a/database/starters/oracle-spring-boot-json-relational-duality-views/src/test/java/com/oracle/spring/json/duality/model/movie/Actor.java b/database/starters/oracle-spring-boot-json-relational-duality-views/src/test/java/com/oracle/spring/json/duality/model/movie/Actor.java new file mode 100644 index 00000000..d24e5e89 --- /dev/null +++ b/database/starters/oracle-spring-boot-json-relational-duality-views/src/test/java/com/oracle/spring/json/duality/model/movie/Actor.java @@ -0,0 +1,69 @@ +package com.oracle.spring.json.duality.model.movie; + +import java.util.HashSet; +import java.util.Objects; +import java.util.Set; + +import com.oracle.spring.json.duality.annotation.JsonRelationalDualityView; +import com.oracle.spring.json.duality.annotation.JsonRelationalDualityViewEntity; +import jakarta.json.bind.annotation.JsonbProperty; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.ManyToMany; +import jakarta.persistence.Table; +import lombok.Getter; +import lombok.Setter; + +import static com.oracle.spring.json.duality.builder.Annotations._ID_FIELD; + +@Entity +@Table(name = "actor") +@Getter +@Setter +@JsonRelationalDualityView +public class Actor { + @JsonbProperty(_ID_FIELD) + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "actor_id") + private Long actorId; + + @Column(name = "first_name", nullable = false, length = 50) + private String firstName; + + @Column(name = "last_name", nullable = false, length = 50) + private String lastName; + + @ManyToMany(mappedBy = "actors") + @JsonRelationalDualityViewEntity( + entity = Movie.class + ) + private Set movies; + + /** + * Adds an Actor to a movie, maintaining bidirectional integrity. + * @param movie to add the Actor into. + */ + public void addMovie(Movie movie) { + if (movies == null) { + movies = new HashSet<>(); + } + movies.add(movie); + movie.getActors().add(this); + } + + @Override + public final boolean equals(Object o) { + if (!(o instanceof Actor actor)) return false; + + return Objects.equals(getActorId(), actor.getActorId()); + } + + @Override + public int hashCode() { + return Objects.hashCode(getActorId()); + } +} diff --git a/database/starters/oracle-spring-boot-json-relational-duality-views/src/test/java/com/oracle/spring/json/duality/model/movie/Director.java b/database/starters/oracle-spring-boot-json-relational-duality-views/src/test/java/com/oracle/spring/json/duality/model/movie/Director.java new file mode 100644 index 00000000..432c469a --- /dev/null +++ b/database/starters/oracle-spring-boot-json-relational-duality-views/src/test/java/com/oracle/spring/json/duality/model/movie/Director.java @@ -0,0 +1,68 @@ +package com.oracle.spring.json.duality.model.movie; + +import java.util.Objects; +import java.util.Set; + +import com.oracle.spring.json.duality.annotation.JsonRelationalDualityViewEntity; +import jakarta.persistence.CascadeType; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.OneToMany; +import jakarta.persistence.OneToOne; +import jakarta.persistence.PrimaryKeyJoinColumn; +import jakarta.persistence.Table; +import lombok.Getter; +import lombok.Setter; + +@Entity +@Table(name = "director") +@Getter +@Setter +public class Director { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "director_id") + private Long directorId; + + @Column(name = "first_name", nullable = false, length = 50) + private String firstName; + + @Column(name = "last_name", nullable = false, length = 50) + private String lastName; + + @JsonRelationalDualityViewEntity(entity = Movie.class) + @OneToMany(mappedBy = "director") // Reference related entity's associated field + private Set movies; + + @OneToOne( + mappedBy = "director", // Reference related entity's associated field + cascade = CascadeType.ALL, // Cascade persistence to the mapped entity + orphanRemoval = true // Remove director bio from director if deleted + ) + // The primary key of the Director entity is used as the foreign key of the DirectorBio entity. + @PrimaryKeyJoinColumn + @JsonRelationalDualityViewEntity(entity = DirectorBio.class) + private DirectorBio directorBio; + + public void setDirectorBio(DirectorBio directorBio) { + this.directorBio = directorBio; + if (directorBio != null) { + directorBio.setDirector(this); + } + } + + @Override + public final boolean equals(Object o) { + if (!(o instanceof Director director)) return false; + + return Objects.equals(getDirectorId(), director.getDirectorId()); + } + + @Override + public int hashCode() { + return Objects.hashCode(getDirectorId()); + } +} diff --git a/database/starters/oracle-spring-boot-json-relational-duality-views/src/test/java/com/oracle/spring/json/duality/model/movie/DirectorBio.java b/database/starters/oracle-spring-boot-json-relational-duality-views/src/test/java/com/oracle/spring/json/duality/model/movie/DirectorBio.java new file mode 100644 index 00000000..f2f31d59 --- /dev/null +++ b/database/starters/oracle-spring-boot-json-relational-duality-views/src/test/java/com/oracle/spring/json/duality/model/movie/DirectorBio.java @@ -0,0 +1,45 @@ +package com.oracle.spring.json.duality.model.movie; + +import java.util.Objects; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.MapsId; +import jakarta.persistence.OneToOne; +import jakarta.persistence.Table; +import lombok.Getter; +import lombok.Setter; + +@Entity +@Table(name = "director_bio") +@Getter +@Setter +public class DirectorBio { + + @Id + @Column(name = "director_id") + private Long directorId; + + @OneToOne(fetch = FetchType.LAZY) + // The primary key will be copied from the director entity + @MapsId + @JoinColumn(name = "director_id") + private Director director; + + @Column(name = "biography", columnDefinition = "CLOB") + private String biography; + + @Override + public final boolean equals(Object o) { + if (!(o instanceof DirectorBio directorBio)) return false; + return Objects.equals(getDirectorId(), directorBio.getDirectorId()); + } + + @Override + public int hashCode() { + return Objects.hashCode(getDirectorId()); + } +} diff --git a/database/starters/oracle-spring-boot-json-relational-duality-views/src/test/java/com/oracle/spring/json/duality/model/movie/Movie.java b/database/starters/oracle-spring-boot-json-relational-duality-views/src/test/java/com/oracle/spring/json/duality/model/movie/Movie.java new file mode 100644 index 00000000..146600aa --- /dev/null +++ b/database/starters/oracle-spring-boot-json-relational-duality-views/src/test/java/com/oracle/spring/json/duality/model/movie/Movie.java @@ -0,0 +1,66 @@ +package com.oracle.spring.json.duality.model.movie; + +import java.util.Objects; +import java.util.Set; + +import com.oracle.spring.json.duality.annotation.JsonRelationalDualityViewEntity; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.JoinTable; +import jakarta.persistence.ManyToMany; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; +import lombok.Getter; +import lombok.Setter; + +@Entity +@Table(name = "movie") +@Getter +@Setter +public class Movie { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "movie_id") + private Long movieId; + + @Column(name = "title", nullable = false, length = 100) + private String title; + + @Column(name = "release_year") + private Integer releaseYear; + + @Column(name = "genre", length = 50) + private String genre; + + @ManyToOne + @JoinColumn(name = "director_id") + @JsonRelationalDualityViewEntity(entity = Director.class) + private Director director; + + @ManyToMany + @JsonRelationalDualityViewEntity( + entity = Actor.class + ) + @JoinTable( + name = "movie_actor", + joinColumns = @JoinColumn(name = "movie_id"), + inverseJoinColumns = @JoinColumn(name = "actor_id") + ) + private Set actors; + + @Override + public final boolean equals(Object o) { + if (!(o instanceof Movie movie)) return false; + + return Objects.equals(movieId, movie.movieId); + } + + @Override + public int hashCode() { + return Objects.hashCode(movieId); + } +} diff --git a/database/starters/oracle-spring-boot-json-relational-duality-views/src/test/java/com/oracle/spring/json/duality/model/Student.java b/database/starters/oracle-spring-boot-json-relational-duality-views/src/test/java/com/oracle/spring/json/duality/model/student/Student.java similarity index 81% rename from database/starters/oracle-spring-boot-json-relational-duality-views/src/test/java/com/oracle/spring/json/duality/model/Student.java rename to database/starters/oracle-spring-boot-json-relational-duality-views/src/test/java/com/oracle/spring/json/duality/model/student/Student.java index ec759166..6754a9f8 100644 --- a/database/starters/oracle-spring-boot-json-relational-duality-views/src/test/java/com/oracle/spring/json/duality/model/Student.java +++ b/database/starters/oracle-spring-boot-json-relational-duality-views/src/test/java/com/oracle/spring/json/duality/model/student/Student.java @@ -1,8 +1,9 @@ // Copyright (c) 2024, Oracle and/or its affiliates. // Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl. -package com.oracle.spring.json.duality.model; +package com.oracle.spring.json.duality.model.student; -import com.oracle.spring.json.duality.builder.JsonRelationalDualityView; +import com.oracle.spring.json.duality.annotation.JsonRelationalDualityView; +import com.oracle.spring.json.duality.annotation.AccessMode; import jakarta.json.bind.annotation.JsonbProperty; import jakarta.persistence.Column; import jakarta.persistence.Entity; @@ -11,11 +12,19 @@ import jakarta.persistence.Id; import jakarta.persistence.Table; +import static com.oracle.spring.json.duality.builder.Annotations._ID_FIELD; + @Entity @Table(name = "STUDENT") -@JsonRelationalDualityView +@JsonRelationalDualityView( + accessMode = @AccessMode( + insert = true, + update = true, + delete = true + ) +) public class Student { - @JsonbProperty("_id") + @JsonbProperty(_ID_FIELD) @Id @GeneratedValue(strategy = GenerationType.UUID) private String id; diff --git a/database/starters/oracle-spring-boot-json-relational-duality-views/src/test/resources/views/actor-create.sql b/database/starters/oracle-spring-boot-json-relational-duality-views/src/test/resources/views/actor-create.sql new file mode 100644 index 00000000..197132e4 --- /dev/null +++ b/database/starters/oracle-spring-boot-json-relational-duality-views/src/test/resources/views/actor-create.sql @@ -0,0 +1,24 @@ +create force editionable json relational duality view actor_dv as actor { + _id : actor_id + firstName : first_name + lastName : last_name + movie_actor { + movie_id + actor_id + movie : movie { + movieId : movie_id + title + releaseYear : release_year + genre + director : director { + directorId : director_id + firstName : first_name + lastName : last_name + director_bio : director_bio { + directorId : director_id + biography + } + } + } + } +} \ No newline at end of file diff --git a/database/starters/oracle-spring-boot-json-relational-duality-views/src/test/resources/views/student-create.sql b/database/starters/oracle-spring-boot-json-relational-duality-views/src/test/resources/views/student-create.sql new file mode 100644 index 00000000..4a683759 --- /dev/null +++ b/database/starters/oracle-spring-boot-json-relational-duality-views/src/test/resources/views/student-create.sql @@ -0,0 +1,9 @@ +create force editionable json relational duality view student_dv as student @insert @update @delete { + _id : id + firstName : first_name + lastName : last_name + email + major + credits + gpa +} \ No newline at end of file diff --git a/database/starters/oracle-spring-boot-json-relational-duality-views/src/test/resources/views/student-update.sql b/database/starters/oracle-spring-boot-json-relational-duality-views/src/test/resources/views/student-update.sql new file mode 100644 index 00000000..6a004f44 --- /dev/null +++ b/database/starters/oracle-spring-boot-json-relational-duality-views/src/test/resources/views/student-update.sql @@ -0,0 +1,9 @@ +create or replace force editionable json relational duality view student_dv as student @insert @update @delete { + _id : id + firstName : first_name + lastName : last_name + email + major + credits + gpa +} \ No newline at end of file From 47899ecee294c09655a195d0752fe2da1c7eae96 Mon Sep 17 00:00:00 2001 From: Anders Swanson Date: Thu, 13 Mar 2025 15:13:26 -0700 Subject: [PATCH 04/13] Generic type reflection Signed-off-by: Anders Swanson --- .../annotation/JsonRelationalDualityView.java | 2 +- .../JsonRelationalDualityViewEntity.java | 17 --------- .../json/duality/builder/Annotations.java | 12 +++--- .../json/duality/builder/ViewEntity.java | 38 +++++++++++++------ .../builder/DualityViewBuilderTest.java | 11 ++++++ .../json/duality/model/movie/Actor.java | 5 +-- .../json/duality/model/movie/Director.java | 6 +-- .../json/duality/model/movie/Movie.java | 8 ++-- 8 files changed, 50 insertions(+), 49 deletions(-) delete mode 100644 database/starters/oracle-spring-boot-json-relational-duality-views/src/main/java/com/oracle/spring/json/duality/annotation/JsonRelationalDualityViewEntity.java diff --git a/database/starters/oracle-spring-boot-json-relational-duality-views/src/main/java/com/oracle/spring/json/duality/annotation/JsonRelationalDualityView.java b/database/starters/oracle-spring-boot-json-relational-duality-views/src/main/java/com/oracle/spring/json/duality/annotation/JsonRelationalDualityView.java index d8d83776..70450352 100644 --- a/database/starters/oracle-spring-boot-json-relational-duality-views/src/main/java/com/oracle/spring/json/duality/annotation/JsonRelationalDualityView.java +++ b/database/starters/oracle-spring-boot-json-relational-duality-views/src/main/java/com/oracle/spring/json/duality/annotation/JsonRelationalDualityView.java @@ -7,7 +7,7 @@ import java.lang.annotation.Target; @Documented -@Target({ElementType.TYPE}) +@Target({ElementType.TYPE, ElementType.FIELD}) @Retention(RetentionPolicy.RUNTIME) public @interface JsonRelationalDualityView { String name() default ""; diff --git a/database/starters/oracle-spring-boot-json-relational-duality-views/src/main/java/com/oracle/spring/json/duality/annotation/JsonRelationalDualityViewEntity.java b/database/starters/oracle-spring-boot-json-relational-duality-views/src/main/java/com/oracle/spring/json/duality/annotation/JsonRelationalDualityViewEntity.java deleted file mode 100644 index 62225d1b..00000000 --- a/database/starters/oracle-spring-boot-json-relational-duality-views/src/main/java/com/oracle/spring/json/duality/annotation/JsonRelationalDualityViewEntity.java +++ /dev/null @@ -1,17 +0,0 @@ -package com.oracle.spring.json.duality.annotation; - -import java.lang.annotation.Documented; -import java.lang.annotation.ElementType; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; - -@Documented -@Target({ElementType.FIELD}) -@Retention(RetentionPolicy.RUNTIME) -public @interface JsonRelationalDualityViewEntity { - String name() default ""; - Class entity(); - - AccessMode accessMode() default @AccessMode(); -} diff --git a/database/starters/oracle-spring-boot-json-relational-duality-views/src/main/java/com/oracle/spring/json/duality/builder/Annotations.java b/database/starters/oracle-spring-boot-json-relational-duality-views/src/main/java/com/oracle/spring/json/duality/builder/Annotations.java index 7f9f67eb..dbafe199 100644 --- a/database/starters/oracle-spring-boot-json-relational-duality-views/src/main/java/com/oracle/spring/json/duality/builder/Annotations.java +++ b/database/starters/oracle-spring-boot-json-relational-duality-views/src/main/java/com/oracle/spring/json/duality/builder/Annotations.java @@ -6,7 +6,6 @@ import com.oracle.spring.json.duality.annotation.AccessMode; import com.oracle.spring.json.duality.annotation.JsonRelationalDualityView; -import com.oracle.spring.json.duality.annotation.JsonRelationalDualityViewEntity; import jakarta.json.bind.annotation.JsonbProperty; import jakarta.persistence.Column; import jakarta.persistence.JoinTable; @@ -15,7 +14,6 @@ import jakarta.persistence.OneToMany; import jakarta.persistence.OneToOne; import jakarta.persistence.Table; -import org.hibernate.mapping.Join; import org.springframework.util.StringUtils; public final class Annotations { @@ -53,11 +51,11 @@ static JoinTable getJoinTableAnnotation( Field f, ManyToMany manyToMany, Class javaType, - JsonRelationalDualityViewEntity viewEntityAnnotation, - Table tableAnnotation) { - if (viewEntityAnnotation != null && StringUtils.hasText(viewEntityAnnotation.name())) { - return viewEntityAnnotation.name().toLowerCase(); + static String getNestedViewName(Class javaType, + JsonRelationalDualityView dvAnnotation, + Table tableAnnotation) { + if (dvAnnotation != null && StringUtils.hasText(dvAnnotation.name())) { + return dvAnnotation.name().toLowerCase(); } return getTableName(javaType, tableAnnotation).toLowerCase(); } diff --git a/database/starters/oracle-spring-boot-json-relational-duality-views/src/main/java/com/oracle/spring/json/duality/builder/ViewEntity.java b/database/starters/oracle-spring-boot-json-relational-duality-views/src/main/java/com/oracle/spring/json/duality/builder/ViewEntity.java index 4f835933..84fec8c3 100644 --- a/database/starters/oracle-spring-boot-json-relational-duality-views/src/main/java/com/oracle/spring/json/duality/builder/ViewEntity.java +++ b/database/starters/oracle-spring-boot-json-relational-duality-views/src/main/java/com/oracle/spring/json/duality/builder/ViewEntity.java @@ -1,10 +1,12 @@ package com.oracle.spring.json.duality.builder; import java.lang.reflect.Field; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; import java.util.HashSet; import java.util.Set; -import com.oracle.spring.json.duality.annotation.JsonRelationalDualityViewEntity; +import com.oracle.spring.json.duality.annotation.JsonRelationalDualityView; import jakarta.json.bind.annotation.JsonbProperty; import jakarta.persistence.Id; import jakarta.persistence.JoinColumn; @@ -18,7 +20,7 @@ import static com.oracle.spring.json.duality.builder.Annotations.getJoinTableAnnotation; import static com.oracle.spring.json.duality.builder.Annotations.getJsonbPropertyName; import static com.oracle.spring.json.duality.builder.Annotations.getTableName; -import static com.oracle.spring.json.duality.builder.Annotations.getViewEntityName; +import static com.oracle.spring.json.duality.builder.Annotations.getNestedViewName; import static com.oracle.spring.json.duality.builder.Annotations.isRelationalEntity; final class ViewEntity { @@ -94,12 +96,12 @@ private void parseField(Field f) { if (id != null && rootSnippet != null) { parseId(f); } else if (isRelationalEntity(f)) { - JsonRelationalDualityViewEntity viewEntityAnnotation = f.getAnnotation(JsonRelationalDualityViewEntity.class); + JsonRelationalDualityView dvAnnotation = f.getAnnotation(JsonRelationalDualityView.class); // The entity should not be included in the view. - if (viewEntityAnnotation == null) { + if (dvAnnotation == null) { return; } - parseRelationalEntity(f, viewEntityAnnotation); + parseRelationalEntity(f, dvAnnotation); } else { parseColumn(f); } @@ -118,11 +120,11 @@ private void parseId(Field f) { addProperty(_ID_FIELD, getDatabaseColumnName(f)); } - private void parseRelationalEntity(Field f, JsonRelationalDualityViewEntity viewEntityAnnotation) { - Class entityJavaType = viewEntityAnnotation.entity(); + private void parseRelationalEntity(Field f, JsonRelationalDualityView dvAnnotation) { + Class entityJavaType = getGenericFieldType(f); if (entityJavaType == null) { throw new IllegalArgumentException("%s %s annotation must include the entity class".formatted( - f.getName(), JsonRelationalDualityViewEntity.class.getSimpleName() + f.getName(), JsonRelationalDualityView.class.getSimpleName() )); } @@ -136,17 +138,29 @@ private void parseRelationalEntity(Field f, JsonRelationalDualityViewEntity view parseManyToMany(manyToMany, f, entityJavaType); } // Add nested entity. - parseNestedEntity(entityJavaType, viewEntityAnnotation); + parseNestedEntity(entityJavaType, dvAnnotation); // Additional trailer for join table if present. if (manyToMany != null) { addTrailer(true); } } - private void parseNestedEntity(Class entityJavaType, JsonRelationalDualityViewEntity viewEntityAnnotation) { + private Class getGenericFieldType(Field f) { + Type genericType = f.getGenericType(); + if (genericType instanceof ParameterizedType p) { + Type type = p.getActualTypeArguments()[0]; + if (type instanceof Class c) { + return c; + } + throw new IllegalStateException("failed to process type: " + type); + } + return f.getType(); + } + + private void parseNestedEntity(Class entityJavaType, JsonRelationalDualityView dvAnnotation) { Table tableAnnotation = entityJavaType.getAnnotation(Table.class); - String viewEntityName = getViewEntityName(entityJavaType, viewEntityAnnotation, tableAnnotation); - String accessMode = getAccessModeStr(viewEntityAnnotation.accessMode()); + String viewEntityName = getNestedViewName(entityJavaType, dvAnnotation, tableAnnotation); + String accessMode = getAccessModeStr(dvAnnotation.accessMode()); ViewEntity ve = new ViewEntity(entityJavaType, new StringBuilder(), accessMode, diff --git a/database/starters/oracle-spring-boot-json-relational-duality-views/src/test/java/com/oracle/spring/json/duality/builder/DualityViewBuilderTest.java b/database/starters/oracle-spring-boot-json-relational-duality-views/src/test/java/com/oracle/spring/json/duality/builder/DualityViewBuilderTest.java index 995d520d..28535e92 100644 --- a/database/starters/oracle-spring-boot-json-relational-duality-views/src/test/java/com/oracle/spring/json/duality/builder/DualityViewBuilderTest.java +++ b/database/starters/oracle-spring-boot-json-relational-duality-views/src/test/java/com/oracle/spring/json/duality/builder/DualityViewBuilderTest.java @@ -1,10 +1,14 @@ package com.oracle.spring.json.duality.builder; +import java.lang.reflect.Field; +import java.util.ArrayList; import java.util.stream.Stream; import com.oracle.spring.json.duality.model.movie.Actor; +import com.oracle.spring.json.duality.model.movie.Movie; import com.oracle.spring.json.duality.model.student.Student; import org.jetbrains.annotations.NotNull; +import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; @@ -42,4 +46,11 @@ private DualityViewBuilder getDualityViewBuilder(String ddlAuto) { hibernateProperties ); } + + @Test + void t() throws Exception { + Field m = Actor.class.getDeclaredField("movies"); + + System.out.println(m.getGenericType()); + } } diff --git a/database/starters/oracle-spring-boot-json-relational-duality-views/src/test/java/com/oracle/spring/json/duality/model/movie/Actor.java b/database/starters/oracle-spring-boot-json-relational-duality-views/src/test/java/com/oracle/spring/json/duality/model/movie/Actor.java index d24e5e89..0a8c2c47 100644 --- a/database/starters/oracle-spring-boot-json-relational-duality-views/src/test/java/com/oracle/spring/json/duality/model/movie/Actor.java +++ b/database/starters/oracle-spring-boot-json-relational-duality-views/src/test/java/com/oracle/spring/json/duality/model/movie/Actor.java @@ -5,7 +5,6 @@ import java.util.Set; import com.oracle.spring.json.duality.annotation.JsonRelationalDualityView; -import com.oracle.spring.json.duality.annotation.JsonRelationalDualityViewEntity; import jakarta.json.bind.annotation.JsonbProperty; import jakarta.persistence.Column; import jakarta.persistence.Entity; @@ -38,9 +37,7 @@ public class Actor { private String lastName; @ManyToMany(mappedBy = "actors") - @JsonRelationalDualityViewEntity( - entity = Movie.class - ) + @JsonRelationalDualityView private Set movies; /** diff --git a/database/starters/oracle-spring-boot-json-relational-duality-views/src/test/java/com/oracle/spring/json/duality/model/movie/Director.java b/database/starters/oracle-spring-boot-json-relational-duality-views/src/test/java/com/oracle/spring/json/duality/model/movie/Director.java index 432c469a..09189942 100644 --- a/database/starters/oracle-spring-boot-json-relational-duality-views/src/test/java/com/oracle/spring/json/duality/model/movie/Director.java +++ b/database/starters/oracle-spring-boot-json-relational-duality-views/src/test/java/com/oracle/spring/json/duality/model/movie/Director.java @@ -3,7 +3,7 @@ import java.util.Objects; import java.util.Set; -import com.oracle.spring.json.duality.annotation.JsonRelationalDualityViewEntity; +import com.oracle.spring.json.duality.annotation.JsonRelationalDualityView; import jakarta.persistence.CascadeType; import jakarta.persistence.Column; import jakarta.persistence.Entity; @@ -33,7 +33,7 @@ public class Director { @Column(name = "last_name", nullable = false, length = 50) private String lastName; - @JsonRelationalDualityViewEntity(entity = Movie.class) + @JsonRelationalDualityView @OneToMany(mappedBy = "director") // Reference related entity's associated field private Set movies; @@ -44,7 +44,7 @@ public class Director { ) // The primary key of the Director entity is used as the foreign key of the DirectorBio entity. @PrimaryKeyJoinColumn - @JsonRelationalDualityViewEntity(entity = DirectorBio.class) + @JsonRelationalDualityView private DirectorBio directorBio; public void setDirectorBio(DirectorBio directorBio) { diff --git a/database/starters/oracle-spring-boot-json-relational-duality-views/src/test/java/com/oracle/spring/json/duality/model/movie/Movie.java b/database/starters/oracle-spring-boot-json-relational-duality-views/src/test/java/com/oracle/spring/json/duality/model/movie/Movie.java index 146600aa..0ab66ab7 100644 --- a/database/starters/oracle-spring-boot-json-relational-duality-views/src/test/java/com/oracle/spring/json/duality/model/movie/Movie.java +++ b/database/starters/oracle-spring-boot-json-relational-duality-views/src/test/java/com/oracle/spring/json/duality/model/movie/Movie.java @@ -3,7 +3,7 @@ import java.util.Objects; import java.util.Set; -import com.oracle.spring.json.duality.annotation.JsonRelationalDualityViewEntity; +import com.oracle.spring.json.duality.annotation.JsonRelationalDualityView; import jakarta.persistence.Column; import jakarta.persistence.Entity; import jakarta.persistence.GeneratedValue; @@ -38,13 +38,11 @@ public class Movie { @ManyToOne @JoinColumn(name = "director_id") - @JsonRelationalDualityViewEntity(entity = Director.class) + @JsonRelationalDualityView private Director director; @ManyToMany - @JsonRelationalDualityViewEntity( - entity = Actor.class - ) + @JsonRelationalDualityView @JoinTable( name = "movie_actor", joinColumns = @JoinColumn(name = "movie_id"), From c889ffe6f479f5b57716dceaffa984b55d9c7e0b Mon Sep 17 00:00:00 2001 From: Anders Swanson Date: Thu, 13 Mar 2025 15:34:50 -0700 Subject: [PATCH 05/13] Fixes Signed-off-by: Anders Swanson --- .../spring/json/duality/annotation/AccessMode.java | 3 +++ .../annotation/JsonRelationalDualityView.java | 3 +++ .../spring/json/duality/builder/Annotations.java | 8 ++++++++ .../json/duality/builder/DualityViewBuilder.java | 4 +++- .../json/duality/builder/DualityViewScanner.java | 4 +++- .../spring/json/duality/builder/RootSnippet.java | 3 +++ .../spring/json/duality/builder/ViewEntity.java | 10 ++++++++-- .../oracle/spring/json/duality/Application.java | 3 +++ .../json/duality/JsonRelationalDualityClient.java | 3 +++ .../spring/json/duality/SpringBootDualityTest.java | 3 +++ .../duality/builder/DualityViewBuilderTest.java | 14 +++----------- .../spring/json/duality/model/movie/Actor.java | 3 +++ .../spring/json/duality/model/movie/Director.java | 3 +++ .../json/duality/model/movie/DirectorBio.java | 3 +++ .../spring/json/duality/model/movie/Movie.java | 3 +++ .../spring/json/duality/model/student/Student.java | 3 ++- 16 files changed, 57 insertions(+), 16 deletions(-) diff --git a/database/starters/oracle-spring-boot-json-relational-duality-views/src/main/java/com/oracle/spring/json/duality/annotation/AccessMode.java b/database/starters/oracle-spring-boot-json-relational-duality-views/src/main/java/com/oracle/spring/json/duality/annotation/AccessMode.java index e54277c7..00df54f9 100644 --- a/database/starters/oracle-spring-boot-json-relational-duality-views/src/main/java/com/oracle/spring/json/duality/annotation/AccessMode.java +++ b/database/starters/oracle-spring-boot-json-relational-duality-views/src/main/java/com/oracle/spring/json/duality/annotation/AccessMode.java @@ -1,3 +1,6 @@ +// Copyright (c) 2025, Oracle and/or its affiliates. +// Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl. + package com.oracle.spring.json.duality.annotation; import java.lang.annotation.Retention; diff --git a/database/starters/oracle-spring-boot-json-relational-duality-views/src/main/java/com/oracle/spring/json/duality/annotation/JsonRelationalDualityView.java b/database/starters/oracle-spring-boot-json-relational-duality-views/src/main/java/com/oracle/spring/json/duality/annotation/JsonRelationalDualityView.java index 70450352..a7227491 100644 --- a/database/starters/oracle-spring-boot-json-relational-duality-views/src/main/java/com/oracle/spring/json/duality/annotation/JsonRelationalDualityView.java +++ b/database/starters/oracle-spring-boot-json-relational-duality-views/src/main/java/com/oracle/spring/json/duality/annotation/JsonRelationalDualityView.java @@ -1,3 +1,6 @@ +// Copyright (c) 2025, Oracle and/or its affiliates. +// Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl. + package com.oracle.spring.json.duality.annotation; import java.lang.annotation.Documented; diff --git a/database/starters/oracle-spring-boot-json-relational-duality-views/src/main/java/com/oracle/spring/json/duality/builder/Annotations.java b/database/starters/oracle-spring-boot-json-relational-duality-views/src/main/java/com/oracle/spring/json/duality/builder/Annotations.java index dbafe199..295b5565 100644 --- a/database/starters/oracle-spring-boot-json-relational-duality-views/src/main/java/com/oracle/spring/json/duality/builder/Annotations.java +++ b/database/starters/oracle-spring-boot-json-relational-duality-views/src/main/java/com/oracle/spring/json/duality/builder/Annotations.java @@ -1,3 +1,6 @@ +// Copyright (c) 2025, Oracle and/or its affiliates. +// Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl. + package com.oracle.spring.json.duality.builder; import java.lang.annotation.Annotation; @@ -7,6 +10,7 @@ import com.oracle.spring.json.duality.annotation.AccessMode; import com.oracle.spring.json.duality.annotation.JsonRelationalDualityView; import jakarta.json.bind.annotation.JsonbProperty; +import jakarta.json.bind.annotation.JsonbTransient; import jakarta.persistence.Column; import jakarta.persistence.JoinTable; import jakarta.persistence.ManyToMany; @@ -90,6 +94,10 @@ static boolean isRelationalEntity(Field f) { return false; } + static boolean isFieldIncluded(Field f) { + return f.getAnnotation(JsonbTransient.class) == null; + } + static String getJsonbPropertyName(Field f) { JsonbProperty jsonbProperty = f.getAnnotation(JsonbProperty.class); diff --git a/database/starters/oracle-spring-boot-json-relational-duality-views/src/main/java/com/oracle/spring/json/duality/builder/DualityViewBuilder.java b/database/starters/oracle-spring-boot-json-relational-duality-views/src/main/java/com/oracle/spring/json/duality/builder/DualityViewBuilder.java index 69d86dd6..2f3635de 100644 --- a/database/starters/oracle-spring-boot-json-relational-duality-views/src/main/java/com/oracle/spring/json/duality/builder/DualityViewBuilder.java +++ b/database/starters/oracle-spring-boot-json-relational-duality-views/src/main/java/com/oracle/spring/json/duality/builder/DualityViewBuilder.java @@ -1,3 +1,6 @@ +// Copyright (c) 2025, Oracle and/or its affiliates. +// Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl. + package com.oracle.spring.json.duality.builder; import java.sql.Connection; @@ -5,7 +8,6 @@ import java.sql.Statement; import java.util.ArrayList; import java.util.List; -import java.util.Optional; import com.oracle.spring.json.duality.annotation.JsonRelationalDualityView; import javax.sql.DataSource; diff --git a/database/starters/oracle-spring-boot-json-relational-duality-views/src/main/java/com/oracle/spring/json/duality/builder/DualityViewScanner.java b/database/starters/oracle-spring-boot-json-relational-duality-views/src/main/java/com/oracle/spring/json/duality/builder/DualityViewScanner.java index 8e97d428..aec5f420 100644 --- a/database/starters/oracle-spring-boot-json-relational-duality-views/src/main/java/com/oracle/spring/json/duality/builder/DualityViewScanner.java +++ b/database/starters/oracle-spring-boot-json-relational-duality-views/src/main/java/com/oracle/spring/json/duality/builder/DualityViewScanner.java @@ -1,9 +1,11 @@ +// Copyright (c) 2025, Oracle and/or its affiliates. +// Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl. + package com.oracle.spring.json.duality.builder; import java.util.Set; import com.oracle.spring.json.duality.annotation.JsonRelationalDualityView; -import jakarta.annotation.PostConstruct; import jakarta.persistence.EntityManager; import jakarta.persistence.metamodel.EntityType; import org.springframework.boot.context.event.ApplicationReadyEvent; diff --git a/database/starters/oracle-spring-boot-json-relational-duality-views/src/main/java/com/oracle/spring/json/duality/builder/RootSnippet.java b/database/starters/oracle-spring-boot-json-relational-duality-views/src/main/java/com/oracle/spring/json/duality/builder/RootSnippet.java index e97b96ec..528ab36b 100644 --- a/database/starters/oracle-spring-boot-json-relational-duality-views/src/main/java/com/oracle/spring/json/duality/builder/RootSnippet.java +++ b/database/starters/oracle-spring-boot-json-relational-duality-views/src/main/java/com/oracle/spring/json/duality/builder/RootSnippet.java @@ -1,3 +1,6 @@ +// Copyright (c) 2025, Oracle and/or its affiliates. +// Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl. + package com.oracle.spring.json.duality.builder; public enum RootSnippet { diff --git a/database/starters/oracle-spring-boot-json-relational-duality-views/src/main/java/com/oracle/spring/json/duality/builder/ViewEntity.java b/database/starters/oracle-spring-boot-json-relational-duality-views/src/main/java/com/oracle/spring/json/duality/builder/ViewEntity.java index 84fec8c3..d83f0bbb 100644 --- a/database/starters/oracle-spring-boot-json-relational-duality-views/src/main/java/com/oracle/spring/json/duality/builder/ViewEntity.java +++ b/database/starters/oracle-spring-boot-json-relational-duality-views/src/main/java/com/oracle/spring/json/duality/builder/ViewEntity.java @@ -1,3 +1,6 @@ +// Copyright (c) 2025, Oracle and/or its affiliates. +// Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl. + package com.oracle.spring.json.duality.builder; import java.lang.reflect.Field; @@ -21,6 +24,7 @@ import static com.oracle.spring.json.duality.builder.Annotations.getJsonbPropertyName; import static com.oracle.spring.json.duality.builder.Annotations.getTableName; import static com.oracle.spring.json.duality.builder.Annotations.getNestedViewName; +import static com.oracle.spring.json.duality.builder.Annotations.isFieldIncluded; import static com.oracle.spring.json.duality.builder.Annotations.isRelationalEntity; final class ViewEntity { @@ -71,7 +75,9 @@ ViewEntity build() { incNesting(); for (Field f : javaType.getDeclaredFields()) { - parseField(f); + if (isFieldIncluded(f)) { + parseField(f); + } } addTrailer(rootSnippet == null); return this; @@ -215,7 +221,7 @@ private void addTrailer(boolean addNewline) { } private String getPadding() { - return String.format("%" + nesting + "s", " "); + return " ".repeat(nesting); } private void incNesting() { diff --git a/database/starters/oracle-spring-boot-json-relational-duality-views/src/test/java/com/oracle/spring/json/duality/Application.java b/database/starters/oracle-spring-boot-json-relational-duality-views/src/test/java/com/oracle/spring/json/duality/Application.java index de624c20..1a13a905 100644 --- a/database/starters/oracle-spring-boot-json-relational-duality-views/src/test/java/com/oracle/spring/json/duality/Application.java +++ b/database/starters/oracle-spring-boot-json-relational-duality-views/src/test/java/com/oracle/spring/json/duality/Application.java @@ -1,3 +1,6 @@ +// Copyright (c) 2025, Oracle and/or its affiliates. +// Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl. + package com.oracle.spring.json.duality; import org.springframework.boot.SpringApplication; diff --git a/database/starters/oracle-spring-boot-json-relational-duality-views/src/test/java/com/oracle/spring/json/duality/JsonRelationalDualityClient.java b/database/starters/oracle-spring-boot-json-relational-duality-views/src/test/java/com/oracle/spring/json/duality/JsonRelationalDualityClient.java index a6f6d792..4a0b77bc 100644 --- a/database/starters/oracle-spring-boot-json-relational-duality-views/src/test/java/com/oracle/spring/json/duality/JsonRelationalDualityClient.java +++ b/database/starters/oracle-spring-boot-json-relational-duality-views/src/test/java/com/oracle/spring/json/duality/JsonRelationalDualityClient.java @@ -1,3 +1,6 @@ +// Copyright (c) 2025, Oracle and/or its affiliates. +// Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl. + package com.oracle.spring.json.duality; import com.oracle.spring.json.jsonb.JSONB; diff --git a/database/starters/oracle-spring-boot-json-relational-duality-views/src/test/java/com/oracle/spring/json/duality/SpringBootDualityTest.java b/database/starters/oracle-spring-boot-json-relational-duality-views/src/test/java/com/oracle/spring/json/duality/SpringBootDualityTest.java index f2b532f9..12fad7d1 100644 --- a/database/starters/oracle-spring-boot-json-relational-duality-views/src/test/java/com/oracle/spring/json/duality/SpringBootDualityTest.java +++ b/database/starters/oracle-spring-boot-json-relational-duality-views/src/test/java/com/oracle/spring/json/duality/SpringBootDualityTest.java @@ -1,3 +1,6 @@ +// Copyright (c) 2025, Oracle and/or its affiliates. +// Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl. + package com.oracle.spring.json.duality; import java.io.File; diff --git a/database/starters/oracle-spring-boot-json-relational-duality-views/src/test/java/com/oracle/spring/json/duality/builder/DualityViewBuilderTest.java b/database/starters/oracle-spring-boot-json-relational-duality-views/src/test/java/com/oracle/spring/json/duality/builder/DualityViewBuilderTest.java index 28535e92..601e6790 100644 --- a/database/starters/oracle-spring-boot-json-relational-duality-views/src/test/java/com/oracle/spring/json/duality/builder/DualityViewBuilderTest.java +++ b/database/starters/oracle-spring-boot-json-relational-duality-views/src/test/java/com/oracle/spring/json/duality/builder/DualityViewBuilderTest.java @@ -1,14 +1,13 @@ +// Copyright (c) 2025, Oracle and/or its affiliates. +// Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl. + package com.oracle.spring.json.duality.builder; -import java.lang.reflect.Field; -import java.util.ArrayList; import java.util.stream.Stream; import com.oracle.spring.json.duality.model.movie.Actor; -import com.oracle.spring.json.duality.model.movie.Movie; import com.oracle.spring.json.duality.model.student.Student; import org.jetbrains.annotations.NotNull; -import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; @@ -46,11 +45,4 @@ private DualityViewBuilder getDualityViewBuilder(String ddlAuto) { hibernateProperties ); } - - @Test - void t() throws Exception { - Field m = Actor.class.getDeclaredField("movies"); - - System.out.println(m.getGenericType()); - } } diff --git a/database/starters/oracle-spring-boot-json-relational-duality-views/src/test/java/com/oracle/spring/json/duality/model/movie/Actor.java b/database/starters/oracle-spring-boot-json-relational-duality-views/src/test/java/com/oracle/spring/json/duality/model/movie/Actor.java index 0a8c2c47..0ad13d85 100644 --- a/database/starters/oracle-spring-boot-json-relational-duality-views/src/test/java/com/oracle/spring/json/duality/model/movie/Actor.java +++ b/database/starters/oracle-spring-boot-json-relational-duality-views/src/test/java/com/oracle/spring/json/duality/model/movie/Actor.java @@ -1,3 +1,6 @@ +// Copyright (c) 2025, Oracle and/or its affiliates. +// Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl. + package com.oracle.spring.json.duality.model.movie; import java.util.HashSet; diff --git a/database/starters/oracle-spring-boot-json-relational-duality-views/src/test/java/com/oracle/spring/json/duality/model/movie/Director.java b/database/starters/oracle-spring-boot-json-relational-duality-views/src/test/java/com/oracle/spring/json/duality/model/movie/Director.java index 09189942..73f75278 100644 --- a/database/starters/oracle-spring-boot-json-relational-duality-views/src/test/java/com/oracle/spring/json/duality/model/movie/Director.java +++ b/database/starters/oracle-spring-boot-json-relational-duality-views/src/test/java/com/oracle/spring/json/duality/model/movie/Director.java @@ -1,3 +1,6 @@ +// Copyright (c) 2025, Oracle and/or its affiliates. +// Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl. + package com.oracle.spring.json.duality.model.movie; import java.util.Objects; diff --git a/database/starters/oracle-spring-boot-json-relational-duality-views/src/test/java/com/oracle/spring/json/duality/model/movie/DirectorBio.java b/database/starters/oracle-spring-boot-json-relational-duality-views/src/test/java/com/oracle/spring/json/duality/model/movie/DirectorBio.java index f2f31d59..c91062a8 100644 --- a/database/starters/oracle-spring-boot-json-relational-duality-views/src/test/java/com/oracle/spring/json/duality/model/movie/DirectorBio.java +++ b/database/starters/oracle-spring-boot-json-relational-duality-views/src/test/java/com/oracle/spring/json/duality/model/movie/DirectorBio.java @@ -1,3 +1,6 @@ +// Copyright (c) 2025, Oracle and/or its affiliates. +// Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl. + package com.oracle.spring.json.duality.model.movie; import java.util.Objects; diff --git a/database/starters/oracle-spring-boot-json-relational-duality-views/src/test/java/com/oracle/spring/json/duality/model/movie/Movie.java b/database/starters/oracle-spring-boot-json-relational-duality-views/src/test/java/com/oracle/spring/json/duality/model/movie/Movie.java index 0ab66ab7..c6880f91 100644 --- a/database/starters/oracle-spring-boot-json-relational-duality-views/src/test/java/com/oracle/spring/json/duality/model/movie/Movie.java +++ b/database/starters/oracle-spring-boot-json-relational-duality-views/src/test/java/com/oracle/spring/json/duality/model/movie/Movie.java @@ -1,3 +1,6 @@ +// Copyright (c) 2025, Oracle and/or its affiliates. +// Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl. + package com.oracle.spring.json.duality.model.movie; import java.util.Objects; diff --git a/database/starters/oracle-spring-boot-json-relational-duality-views/src/test/java/com/oracle/spring/json/duality/model/student/Student.java b/database/starters/oracle-spring-boot-json-relational-duality-views/src/test/java/com/oracle/spring/json/duality/model/student/Student.java index 6754a9f8..c4468dd5 100644 --- a/database/starters/oracle-spring-boot-json-relational-duality-views/src/test/java/com/oracle/spring/json/duality/model/student/Student.java +++ b/database/starters/oracle-spring-boot-json-relational-duality-views/src/test/java/com/oracle/spring/json/duality/model/student/Student.java @@ -1,5 +1,6 @@ -// Copyright (c) 2024, Oracle and/or its affiliates. +// Copyright (c) 2025, Oracle and/or its affiliates. // Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl. + package com.oracle.spring.json.duality.model.student; import com.oracle.spring.json.duality.annotation.JsonRelationalDualityView; From 25fc3ba06b7ca616e33b8b5c24b491cc75245d47 Mon Sep 17 00:00:00 2001 From: Anders Swanson Date: Fri, 14 Mar 2025 12:41:16 -0700 Subject: [PATCH 06/13] JsonbTransient Signed-off-by: Anders Swanson --- .../json/duality/builder/Annotations.java | 2 +- .../duality/JsonRelationalDualityClient.java | 36 ++++++++-- .../json/duality/SpringBootDualityTest.java | 56 ++++++++++++++- .../builder/DualityViewBuilderTest.java | 3 +- .../json/duality/model/movie/Director.java | 7 +- .../json/duality/model/movie/DirectorBio.java | 7 +- .../json/duality/model/movie/Movie.java | 7 +- .../json/duality/model/student/Student.java | 71 ++----------------- .../src/test/resources/views/actor-create.sql | 6 +- 9 files changed, 114 insertions(+), 81 deletions(-) diff --git a/database/starters/oracle-spring-boot-json-relational-duality-views/src/main/java/com/oracle/spring/json/duality/builder/Annotations.java b/database/starters/oracle-spring-boot-json-relational-duality-views/src/main/java/com/oracle/spring/json/duality/builder/Annotations.java index 295b5565..0da8a1dc 100644 --- a/database/starters/oracle-spring-boot-json-relational-duality-views/src/main/java/com/oracle/spring/json/duality/builder/Annotations.java +++ b/database/starters/oracle-spring-boot-json-relational-duality-views/src/main/java/com/oracle/spring/json/duality/builder/Annotations.java @@ -64,7 +64,7 @@ static String getNestedViewName(Class javaType, return getTableName(javaType, tableAnnotation).toLowerCase(); } - static String getViewName(Class javaType, JsonRelationalDualityView dvAnnotation) { + public static String getViewName(Class javaType, JsonRelationalDualityView dvAnnotation) { Table tableAnnotation = javaType.getAnnotation(Table.class); final String suffix = "_dv"; if (dvAnnotation != null && StringUtils.hasText(dvAnnotation.name())) { diff --git a/database/starters/oracle-spring-boot-json-relational-duality-views/src/test/java/com/oracle/spring/json/duality/JsonRelationalDualityClient.java b/database/starters/oracle-spring-boot-json-relational-duality-views/src/test/java/com/oracle/spring/json/duality/JsonRelationalDualityClient.java index 4a0b77bc..cd7af213 100644 --- a/database/starters/oracle-spring-boot-json-relational-duality-views/src/test/java/com/oracle/spring/json/duality/JsonRelationalDualityClient.java +++ b/database/starters/oracle-spring-boot-json-relational-duality-views/src/test/java/com/oracle/spring/json/duality/JsonRelationalDualityClient.java @@ -3,9 +3,18 @@ package com.oracle.spring.json.duality; +import java.util.Optional; + +import com.oracle.spring.json.duality.annotation.JsonRelationalDualityView; import com.oracle.spring.json.jsonb.JSONB; +import com.oracle.spring.json.jsonb.JSONBRowMapper; +import oracle.jdbc.OracleTypes; import org.springframework.jdbc.core.simple.JdbcClient; +import org.springframework.stereotype.Component; + +import static com.oracle.spring.json.duality.builder.Annotations.getViewName; +@Component public class JsonRelationalDualityClient { private final JdbcClient jdbcClient; private final JSONB jsonb; @@ -15,16 +24,29 @@ public JsonRelationalDualityClient(JdbcClient jdbcClient, JSONB jsonb) { this.jsonb = jsonb; } - public T save(T entity) { + public int save(T entity, Class entityJavaType) { + String viewName = getViewName(entityJavaType, entityJavaType.getAnnotation(JsonRelationalDualityView.class)); + final String sql = """ + insert into %s (data) values(?) + """.formatted(viewName); + byte[] oson = jsonb.toOSON(entity); + return jdbcClient.sql(sql) + .param(1, oson, OracleTypes.JSON) + .update(); + } + public Optional findById(Class entityJavaType, ID id) { + String viewName = getViewName(entityJavaType, entityJavaType.getAnnotation(JsonRelationalDualityView.class)); final String sql = """ - - """; - return null; - } + select * from %s dv + where dv.data."_id" = ? + """.formatted(viewName); - public T findById(Class entityJavaType, ID id) { - return null; + JSONBRowMapper rowMapper = new JSONBRowMapper<>(jsonb, entityJavaType); + return jdbcClient.sql(sql) + .param(1, id) + .query(rowMapper) + .optional(); } } diff --git a/database/starters/oracle-spring-boot-json-relational-duality-views/src/test/java/com/oracle/spring/json/duality/SpringBootDualityTest.java b/database/starters/oracle-spring-boot-json-relational-duality-views/src/test/java/com/oracle/spring/json/duality/SpringBootDualityTest.java index 12fad7d1..f2d6d198 100644 --- a/database/starters/oracle-spring-boot-json-relational-duality-views/src/test/java/com/oracle/spring/json/duality/SpringBootDualityTest.java +++ b/database/starters/oracle-spring-boot-json-relational-duality-views/src/test/java/com/oracle/spring/json/duality/SpringBootDualityTest.java @@ -8,8 +8,16 @@ import java.nio.file.Files; import java.nio.file.Path; import java.time.Duration; +import java.util.Optional; +import java.util.Set; +import java.util.UUID; import com.oracle.spring.json.duality.builder.DualityViewScanner; +import com.oracle.spring.json.duality.model.movie.Actor; +import com.oracle.spring.json.duality.model.movie.Director; +import com.oracle.spring.json.duality.model.movie.DirectorBio; +import com.oracle.spring.json.duality.model.movie.Movie; +import com.oracle.spring.json.duality.model.student.Student; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; @@ -19,6 +27,8 @@ import org.testcontainers.junit.jupiter.Testcontainers; import org.testcontainers.oracle.OracleContainer; +import static org.assertj.core.api.Assertions.assertThat; + @SpringBootTest @Testcontainers public class SpringBootDualityTest { @@ -44,6 +54,50 @@ public static String readViewFile(String fileName) { @Autowired private DualityViewScanner dualityViewScanner; + @Autowired + JsonRelationalDualityClient dvClient; + + @Test + void student() { + Student s = new Student(); + s.setId(UUID.randomUUID().toString()); + s.setFirstName("John"); + s.setLastName("Doe"); + s.setCredits(87); + s.setEmail("johndoe@example.com"); + s.setGpa(3.77); + s.setMajor("Computer Science"); + + int save = dvClient.save(s, Student.class); + assertThat(save).isEqualTo(1); + Optional byId = dvClient.findById(Student.class, s.getId()); + assertThat(byId.isPresent()).isTrue(); + assertThat(byId.get()).isEqualTo(s); + } + @Test - void contextLoads() {} + void actor() { + DirectorBio directorBio = new DirectorBio(); + directorBio.setBiography("biography"); + + Director director = new Director(); + director.setDirectorBio(directorBio); + director.setFirstName("John"); + director.setLastName("Doe"); + + Movie m = new Movie(); + m.setDirector(director); + m.setTitle("my movie"); + m.setGenre("action"); + m.setReleaseYear(1993); + + Actor actor = new Actor(); + actor.setFirstName("John"); + actor.setLastName("Doe"); + actor.setMovies(Set.of(m)); + + dvClient.save(actor, Actor.class); + Optional actorById = dvClient.findById(Actor.class, 1); + assertThat(actorById.isPresent()).isTrue(); + } } diff --git a/database/starters/oracle-spring-boot-json-relational-duality-views/src/test/java/com/oracle/spring/json/duality/builder/DualityViewBuilderTest.java b/database/starters/oracle-spring-boot-json-relational-duality-views/src/test/java/com/oracle/spring/json/duality/builder/DualityViewBuilderTest.java index 601e6790..0239bc98 100644 --- a/database/starters/oracle-spring-boot-json-relational-duality-views/src/test/java/com/oracle/spring/json/duality/builder/DualityViewBuilderTest.java +++ b/database/starters/oracle-spring-boot-json-relational-duality-views/src/test/java/com/oracle/spring/json/duality/builder/DualityViewBuilderTest.java @@ -6,6 +6,8 @@ import java.util.stream.Stream; import com.oracle.spring.json.duality.model.movie.Actor; +import com.oracle.spring.json.duality.model.movie.Director; +import com.oracle.spring.json.duality.model.movie.Movie; import com.oracle.spring.json.duality.model.student.Student; import org.jetbrains.annotations.NotNull; import org.junit.jupiter.params.ParameterizedTest; @@ -32,7 +34,6 @@ public void buildViews(Class entity, String viewFile, String ddlAuto) { String expectedView = readViewFile(viewFile); DualityViewBuilder dualityViewBuilder = getDualityViewBuilder(ddlAuto); String actualView = dualityViewBuilder.build(entity); - System.out.println(actualView); assertThat(expectedView).isEqualTo(actualView); } diff --git a/database/starters/oracle-spring-boot-json-relational-duality-views/src/test/java/com/oracle/spring/json/duality/model/movie/Director.java b/database/starters/oracle-spring-boot-json-relational-duality-views/src/test/java/com/oracle/spring/json/duality/model/movie/Director.java index 73f75278..201d22bb 100644 --- a/database/starters/oracle-spring-boot-json-relational-duality-views/src/test/java/com/oracle/spring/json/duality/model/movie/Director.java +++ b/database/starters/oracle-spring-boot-json-relational-duality-views/src/test/java/com/oracle/spring/json/duality/model/movie/Director.java @@ -7,6 +7,8 @@ import java.util.Set; import com.oracle.spring.json.duality.annotation.JsonRelationalDualityView; +import jakarta.json.bind.annotation.JsonbProperty; +import jakarta.json.bind.annotation.JsonbTransient; import jakarta.persistence.CascadeType; import jakarta.persistence.Column; import jakarta.persistence.Entity; @@ -20,11 +22,14 @@ import lombok.Getter; import lombok.Setter; +import static com.oracle.spring.json.duality.builder.Annotations._ID_FIELD; + @Entity @Table(name = "director") @Getter @Setter public class Director { + @JsonbProperty(_ID_FIELD) @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @Column(name = "director_id") @@ -36,7 +41,7 @@ public class Director { @Column(name = "last_name", nullable = false, length = 50) private String lastName; - @JsonRelationalDualityView + @JsonbTransient @OneToMany(mappedBy = "director") // Reference related entity's associated field private Set movies; diff --git a/database/starters/oracle-spring-boot-json-relational-duality-views/src/test/java/com/oracle/spring/json/duality/model/movie/DirectorBio.java b/database/starters/oracle-spring-boot-json-relational-duality-views/src/test/java/com/oracle/spring/json/duality/model/movie/DirectorBio.java index c91062a8..c7e14482 100644 --- a/database/starters/oracle-spring-boot-json-relational-duality-views/src/test/java/com/oracle/spring/json/duality/model/movie/DirectorBio.java +++ b/database/starters/oracle-spring-boot-json-relational-duality-views/src/test/java/com/oracle/spring/json/duality/model/movie/DirectorBio.java @@ -5,6 +5,8 @@ import java.util.Objects; +import jakarta.json.bind.annotation.JsonbProperty; +import jakarta.json.bind.annotation.JsonbTransient; import jakarta.persistence.Column; import jakarta.persistence.Entity; import jakarta.persistence.FetchType; @@ -16,12 +18,14 @@ import lombok.Getter; import lombok.Setter; +import static com.oracle.spring.json.duality.builder.Annotations._ID_FIELD; + @Entity @Table(name = "director_bio") @Getter @Setter public class DirectorBio { - + @JsonbProperty(_ID_FIELD) @Id @Column(name = "director_id") private Long directorId; @@ -30,6 +34,7 @@ public class DirectorBio { // The primary key will be copied from the director entity @MapsId @JoinColumn(name = "director_id") + @JsonbTransient private Director director; @Column(name = "biography", columnDefinition = "CLOB") diff --git a/database/starters/oracle-spring-boot-json-relational-duality-views/src/test/java/com/oracle/spring/json/duality/model/movie/Movie.java b/database/starters/oracle-spring-boot-json-relational-duality-views/src/test/java/com/oracle/spring/json/duality/model/movie/Movie.java index c6880f91..11e59018 100644 --- a/database/starters/oracle-spring-boot-json-relational-duality-views/src/test/java/com/oracle/spring/json/duality/model/movie/Movie.java +++ b/database/starters/oracle-spring-boot-json-relational-duality-views/src/test/java/com/oracle/spring/json/duality/model/movie/Movie.java @@ -7,6 +7,8 @@ import java.util.Set; import com.oracle.spring.json.duality.annotation.JsonRelationalDualityView; +import jakarta.json.bind.annotation.JsonbProperty; +import jakarta.json.bind.annotation.JsonbTransient; import jakarta.persistence.Column; import jakarta.persistence.Entity; import jakarta.persistence.GeneratedValue; @@ -20,6 +22,8 @@ import lombok.Getter; import lombok.Setter; +import static com.oracle.spring.json.duality.builder.Annotations._ID_FIELD; + @Entity @Table(name = "movie") @Getter @@ -28,6 +32,7 @@ public class Movie { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @Column(name = "movie_id") + @JsonbProperty(_ID_FIELD) private Long movieId; @Column(name = "title", nullable = false, length = 100) @@ -45,7 +50,7 @@ public class Movie { private Director director; @ManyToMany - @JsonRelationalDualityView + @JsonbTransient @JoinTable( name = "movie_actor", joinColumns = @JoinColumn(name = "movie_id"), diff --git a/database/starters/oracle-spring-boot-json-relational-duality-views/src/test/java/com/oracle/spring/json/duality/model/student/Student.java b/database/starters/oracle-spring-boot-json-relational-duality-views/src/test/java/com/oracle/spring/json/duality/model/student/Student.java index c4468dd5..20f235f6 100644 --- a/database/starters/oracle-spring-boot-json-relational-duality-views/src/test/java/com/oracle/spring/json/duality/model/student/Student.java +++ b/database/starters/oracle-spring-boot-json-relational-duality-views/src/test/java/com/oracle/spring/json/duality/model/student/Student.java @@ -12,6 +12,9 @@ import jakarta.persistence.GenerationType; import jakarta.persistence.Id; import jakarta.persistence.Table; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.Setter; import static com.oracle.spring.json.duality.builder.Annotations._ID_FIELD; @@ -24,6 +27,9 @@ delete = true ) ) +@EqualsAndHashCode +@Getter +@Setter public class Student { @JsonbProperty(_ID_FIELD) @Id @@ -39,69 +45,4 @@ public class Student { private double gpa; public Student() {} - - public Student(String firstName, String lastName, String email, String major, double credits, double gpa) { - this.firstName = firstName; - this.lastName = lastName; - this.email = email; - this.major = major; - this.credits = credits; - this.gpa = gpa; - } - - public String getId() { - return id; - } - - public void setId(String id) { - this.id = id; - } - - public String getFirstName() { - return firstName; - } - - public void setFirstName(String firstName) { - this.firstName = firstName; - } - - public String getLastName() { - return lastName; - } - - public void setLastName(String lastName) { - this.lastName = lastName; - } - - public String getEmail() { - return email; - } - - public void setEmail(String email) { - this.email = email; - } - - public String getMajor() { - return major; - } - - public void setMajor(String major) { - this.major = major; - } - - public double getCredits() { - return credits; - } - - public void setCredits(double credits) { - this.credits = credits; - } - - public double getGpa() { - return gpa; - } - - public void setGpa(double gpa) { - this.gpa = gpa; - } } diff --git a/database/starters/oracle-spring-boot-json-relational-duality-views/src/test/resources/views/actor-create.sql b/database/starters/oracle-spring-boot-json-relational-duality-views/src/test/resources/views/actor-create.sql index 197132e4..99378d6a 100644 --- a/database/starters/oracle-spring-boot-json-relational-duality-views/src/test/resources/views/actor-create.sql +++ b/database/starters/oracle-spring-boot-json-relational-duality-views/src/test/resources/views/actor-create.sql @@ -6,16 +6,16 @@ create force editionable json relational duality view actor_dv as actor { movie_id actor_id movie : movie { - movieId : movie_id + _id : movie_id title releaseYear : release_year genre director : director { - directorId : director_id + _id : director_id firstName : first_name lastName : last_name director_bio : director_bio { - directorId : director_id + _id : director_id biography } } From 5bff8f361a5f336504f9875d82f5e8af328b159d Mon Sep 17 00:00:00 2001 From: Anders Swanson Date: Wed, 19 Mar 2025 09:54:33 -0700 Subject: [PATCH 07/13] View Builder Signed-off-by: Anders Swanson --- .../JsonRelationalDualityViewScan.java | 20 +++++++ .../json/duality/builder/Annotations.java | 23 -------- .../duality/builder/DualityViewScanner.java | 58 +++++++++++++++---- .../json/duality/builder/JoinTableSerde.java | 25 ++++++++ .../duality/builder/ScannerConfiguration.java | 25 ++++++++ .../json/duality/builder/ViewEntity.java | 9 +-- .../spring/json/duality/Application.java | 9 ++- .../json/duality/SpringBootDualityTest.java | 25 +++++++- .../builder/DualityViewBuilderTest.java | 5 +- .../json/duality/model/products/Order.java | 33 +++++++++++ .../json/duality/model/products/Product.java | 31 ++++++++++ .../src/test/resources/products.sql | 13 +++++ .../src/test/resources/views/order-create.sql | 10 ++++ 13 files changed, 242 insertions(+), 44 deletions(-) create mode 100644 database/starters/oracle-spring-boot-json-relational-duality-views/src/main/java/com/oracle/spring/json/duality/annotation/JsonRelationalDualityViewScan.java create mode 100644 database/starters/oracle-spring-boot-json-relational-duality-views/src/main/java/com/oracle/spring/json/duality/builder/JoinTableSerde.java create mode 100644 database/starters/oracle-spring-boot-json-relational-duality-views/src/main/java/com/oracle/spring/json/duality/builder/ScannerConfiguration.java create mode 100644 database/starters/oracle-spring-boot-json-relational-duality-views/src/test/java/com/oracle/spring/json/duality/model/products/Order.java create mode 100644 database/starters/oracle-spring-boot-json-relational-duality-views/src/test/java/com/oracle/spring/json/duality/model/products/Product.java create mode 100644 database/starters/oracle-spring-boot-json-relational-duality-views/src/test/resources/products.sql create mode 100644 database/starters/oracle-spring-boot-json-relational-duality-views/src/test/resources/views/order-create.sql diff --git a/database/starters/oracle-spring-boot-json-relational-duality-views/src/main/java/com/oracle/spring/json/duality/annotation/JsonRelationalDualityViewScan.java b/database/starters/oracle-spring-boot-json-relational-duality-views/src/main/java/com/oracle/spring/json/duality/annotation/JsonRelationalDualityViewScan.java new file mode 100644 index 00000000..285abba9 --- /dev/null +++ b/database/starters/oracle-spring-boot-json-relational-duality-views/src/main/java/com/oracle/spring/json/duality/annotation/JsonRelationalDualityViewScan.java @@ -0,0 +1,20 @@ +// Copyright (c) 2025, Oracle and/or its affiliates. +// Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl. + +package com.oracle.spring.json.duality.annotation; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + + +@Documented +@Target({ElementType.TYPE}) +@Retention(RetentionPolicy.RUNTIME) +public @interface JsonRelationalDualityViewScan { + String[] basePackages() default {}; + + Class[] basePackageClasses() default {}; +} diff --git a/database/starters/oracle-spring-boot-json-relational-duality-views/src/main/java/com/oracle/spring/json/duality/builder/Annotations.java b/database/starters/oracle-spring-boot-json-relational-duality-views/src/main/java/com/oracle/spring/json/duality/builder/Annotations.java index 0da8a1dc..11024790 100644 --- a/database/starters/oracle-spring-boot-json-relational-duality-views/src/main/java/com/oracle/spring/json/duality/builder/Annotations.java +++ b/database/starters/oracle-spring-boot-json-relational-duality-views/src/main/java/com/oracle/spring/json/duality/builder/Annotations.java @@ -3,9 +3,7 @@ package com.oracle.spring.json.duality.builder; -import java.lang.annotation.Annotation; import java.lang.reflect.Field; -import java.util.Set; import com.oracle.spring.json.duality.annotation.AccessMode; import com.oracle.spring.json.duality.annotation.JsonRelationalDualityView; @@ -14,22 +12,12 @@ import jakarta.persistence.Column; import jakarta.persistence.JoinTable; import jakarta.persistence.ManyToMany; -import jakarta.persistence.ManyToOne; -import jakarta.persistence.OneToMany; -import jakarta.persistence.OneToOne; import jakarta.persistence.Table; import org.springframework.util.StringUtils; public final class Annotations { public static final String _ID_FIELD = "_id"; - static final Set> RELATIONAL_ANNOTATIONS = Set.of( - OneToMany.class, - ManyToOne.class, - OneToOne.class, - ManyToMany.class - ); - static JoinTable getJoinTableAnnotation( Field f, ManyToMany manyToMany, Class mappedType) { JoinTable annotation = f.getAnnotation(JoinTable.class); if (annotation != null) { @@ -83,17 +71,6 @@ static String getTableName(Class javaType, Table tableAnnotation) { return javaType.getName().toLowerCase(); } - static boolean isRelationalEntity(Field f) { - Annotation[] annotations = f.getAnnotations(); - for (Annotation annotation : annotations) { - if (RELATIONAL_ANNOTATIONS.contains(annotation.annotationType())) { - return true; - } - } - - return false; - } - static boolean isFieldIncluded(Field f) { return f.getAnnotation(JsonbTransient.class) == null; } diff --git a/database/starters/oracle-spring-boot-json-relational-duality-views/src/main/java/com/oracle/spring/json/duality/builder/DualityViewScanner.java b/database/starters/oracle-spring-boot-json-relational-duality-views/src/main/java/com/oracle/spring/json/duality/builder/DualityViewScanner.java index aec5f420..704bbbb1 100644 --- a/database/starters/oracle-spring-boot-json-relational-duality-views/src/main/java/com/oracle/spring/json/duality/builder/DualityViewScanner.java +++ b/database/starters/oracle-spring-boot-json-relational-duality-views/src/main/java/com/oracle/spring/json/duality/builder/DualityViewScanner.java @@ -3,34 +3,70 @@ package com.oracle.spring.json.duality.builder; +import java.util.Map; import java.util.Set; import com.oracle.spring.json.duality.annotation.JsonRelationalDualityView; -import jakarta.persistence.EntityManager; -import jakarta.persistence.metamodel.EntityType; +import com.oracle.spring.json.duality.annotation.JsonRelationalDualityViewScan; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.context.event.ApplicationReadyEvent; +import org.springframework.context.ApplicationContext; import org.springframework.context.event.EventListener; +import org.springframework.data.util.AnnotatedTypeScanner; import org.springframework.stereotype.Component; +import org.springframework.util.ClassUtils; @Component final public class DualityViewScanner { private final DualityViewBuilder dualityViewBuilder; - private final EntityManager entityManager; + private final ApplicationContext applicationContext; + private final AnnotatedTypeScanner scanner; - public DualityViewScanner(DualityViewBuilder dualityViewBuilder, EntityManager entityManager) { + public DualityViewScanner(DualityViewBuilder dualityViewBuilder, + ApplicationContext applicationContext, + @Qualifier("jsonRelationalDualityViewScanner") AnnotatedTypeScanner scanner) { this.dualityViewBuilder = dualityViewBuilder; - this.entityManager = entityManager; + this.applicationContext = applicationContext; + this.scanner = scanner; } @EventListener(ApplicationReadyEvent.class) public void scan() { - Set> entities = entityManager.getMetamodel().getEntities(); - for (EntityType entityType : entities) { - Class javaType = entityType.getJavaType(); - JsonRelationalDualityView dvAnnotation = javaType.getAnnotation(JsonRelationalDualityView.class); - if (dvAnnotation != null) { - dualityViewBuilder.apply(javaType); + Map springBootApplications = applicationContext.getBeansWithAnnotation(SpringBootApplication.class); + for (Map.Entry entry : springBootApplications.entrySet()) { + Class mainClass = ClassUtils.getUserClass(entry.getValue().getClass()); + JsonRelationalDualityViewScan dvScan = mainClass.getAnnotation(JsonRelationalDualityViewScan.class); + if (dvScan == null) { + scanPackage(mainClass.getPackageName()); + } else { + applyDvScan(dvScan); } } } + + private void applyDvScan(JsonRelationalDualityViewScan dvScan) { + if (dvScan.basePackages() != null) { + for (String pkg : dvScan.basePackages()) { + scanPackage(pkg); + } + } + for (Class javaType : dvScan.basePackageClasses()) { + applyClass(javaType); + } + } + + private void scanPackage(String packageName) { + Set> types = scanner.findTypes(packageName); + for (Class type : types) { + applyClass(type); + } + } + + private void applyClass(Class javaType) { + JsonRelationalDualityView dvAnnotation = javaType.getAnnotation(JsonRelationalDualityView.class); + if (dvAnnotation != null) { + dualityViewBuilder.apply(javaType); + } + } } diff --git a/database/starters/oracle-spring-boot-json-relational-duality-views/src/main/java/com/oracle/spring/json/duality/builder/JoinTableSerde.java b/database/starters/oracle-spring-boot-json-relational-duality-views/src/main/java/com/oracle/spring/json/duality/builder/JoinTableSerde.java new file mode 100644 index 00000000..b4206e84 --- /dev/null +++ b/database/starters/oracle-spring-boot-json-relational-duality-views/src/main/java/com/oracle/spring/json/duality/builder/JoinTableSerde.java @@ -0,0 +1,25 @@ +// Copyright (c) 2025, Oracle and/or its affiliates. +// Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl. + +package com.oracle.spring.json.duality.builder; + +import java.lang.reflect.Type; + +import jakarta.json.bind.serializer.DeserializationContext; +import jakarta.json.bind.serializer.JsonbDeserializer; +import jakarta.json.bind.serializer.JsonbSerializer; +import jakarta.json.bind.serializer.SerializationContext; +import jakarta.json.stream.JsonGenerator; +import jakarta.json.stream.JsonParser; + +public class JoinTableSerde implements JsonbSerializer>, JsonbDeserializer> { + @Override + public void serialize(Class aClass, JsonGenerator jsonGenerator, SerializationContext serializationContext) { + + } + + @Override + public Class deserialize(JsonParser jsonParser, DeserializationContext deserializationContext, Type type) { + return null; + } +} diff --git a/database/starters/oracle-spring-boot-json-relational-duality-views/src/main/java/com/oracle/spring/json/duality/builder/ScannerConfiguration.java b/database/starters/oracle-spring-boot-json-relational-duality-views/src/main/java/com/oracle/spring/json/duality/builder/ScannerConfiguration.java new file mode 100644 index 00000000..ab12886a --- /dev/null +++ b/database/starters/oracle-spring-boot-json-relational-duality-views/src/main/java/com/oracle/spring/json/duality/builder/ScannerConfiguration.java @@ -0,0 +1,25 @@ +// Copyright (c) 2025, Oracle and/or its affiliates. +// Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl. + +package com.oracle.spring.json.duality.builder; + +import com.oracle.spring.json.duality.annotation.JsonRelationalDualityView; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.env.Environment; +import org.springframework.core.io.ResourceLoader; +import org.springframework.data.util.AnnotatedTypeScanner; + +@Configuration +public class ScannerConfiguration { + @Bean + @Qualifier("jsonRelationalDualityViewScanner") + public AnnotatedTypeScanner scanner(ResourceLoader resourceLoader, Environment environment) { + AnnotatedTypeScanner dvScanner = new AnnotatedTypeScanner(JsonRelationalDualityView.class); + + dvScanner.setResourceLoader(resourceLoader); + dvScanner.setEnvironment(environment); + return dvScanner; + } +} diff --git a/database/starters/oracle-spring-boot-json-relational-duality-views/src/main/java/com/oracle/spring/json/duality/builder/ViewEntity.java b/database/starters/oracle-spring-boot-json-relational-duality-views/src/main/java/com/oracle/spring/json/duality/builder/ViewEntity.java index d83f0bbb..03119437 100644 --- a/database/starters/oracle-spring-boot-json-relational-duality-views/src/main/java/com/oracle/spring/json/duality/builder/ViewEntity.java +++ b/database/starters/oracle-spring-boot-json-relational-duality-views/src/main/java/com/oracle/spring/json/duality/builder/ViewEntity.java @@ -25,7 +25,6 @@ import static com.oracle.spring.json.duality.builder.Annotations.getTableName; import static com.oracle.spring.json.duality.builder.Annotations.getNestedViewName; import static com.oracle.spring.json.duality.builder.Annotations.isFieldIncluded; -import static com.oracle.spring.json.duality.builder.Annotations.isRelationalEntity; final class ViewEntity { @@ -98,15 +97,11 @@ private String getNestedEntityPrefix(Table tableAnnotation) { } private void parseField(Field f) { + JsonRelationalDualityView dvAnnotation; Id id = f.getAnnotation(Id.class); if (id != null && rootSnippet != null) { parseId(f); - } else if (isRelationalEntity(f)) { - JsonRelationalDualityView dvAnnotation = f.getAnnotation(JsonRelationalDualityView.class); - // The entity should not be included in the view. - if (dvAnnotation == null) { - return; - } + } else if ((dvAnnotation = f.getAnnotation(JsonRelationalDualityView.class)) != null) { parseRelationalEntity(f, dvAnnotation); } else { parseColumn(f); diff --git a/database/starters/oracle-spring-boot-json-relational-duality-views/src/test/java/com/oracle/spring/json/duality/Application.java b/database/starters/oracle-spring-boot-json-relational-duality-views/src/test/java/com/oracle/spring/json/duality/Application.java index 1a13a905..e000e3b8 100644 --- a/database/starters/oracle-spring-boot-json-relational-duality-views/src/test/java/com/oracle/spring/json/duality/Application.java +++ b/database/starters/oracle-spring-boot-json-relational-duality-views/src/test/java/com/oracle/spring/json/duality/Application.java @@ -3,6 +3,7 @@ package com.oracle.spring.json.duality; +import com.oracle.spring.json.duality.annotation.JsonRelationalDualityViewScan; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.autoconfigure.domain.EntityScan; @@ -10,7 +11,13 @@ @SpringBootApplication @EntityScan(basePackages = { "com.oracle.spring.json.duality.model" -}) + } +) +@JsonRelationalDualityViewScan( + basePackages = { + "com.oracle.spring.json.duality.model" + } +) public class Application { public static void main(String[] args) { SpringApplication.run(Application.class, args); diff --git a/database/starters/oracle-spring-boot-json-relational-duality-views/src/test/java/com/oracle/spring/json/duality/SpringBootDualityTest.java b/database/starters/oracle-spring-boot-json-relational-duality-views/src/test/java/com/oracle/spring/json/duality/SpringBootDualityTest.java index f2d6d198..277aa70c 100644 --- a/database/starters/oracle-spring-boot-json-relational-duality-views/src/test/java/com/oracle/spring/json/duality/SpringBootDualityTest.java +++ b/database/starters/oracle-spring-boot-json-relational-duality-views/src/test/java/com/oracle/spring/json/duality/SpringBootDualityTest.java @@ -8,6 +8,7 @@ import java.nio.file.Files; import java.nio.file.Path; import java.time.Duration; +import java.util.Date; import java.util.Optional; import java.util.Set; import java.util.UUID; @@ -17,6 +18,8 @@ import com.oracle.spring.json.duality.model.movie.Director; import com.oracle.spring.json.duality.model.movie.DirectorBio; import com.oracle.spring.json.duality.model.movie.Movie; +import com.oracle.spring.json.duality.model.products.Order; +import com.oracle.spring.json.duality.model.products.Product; import com.oracle.spring.json.duality.model.student.Student; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; @@ -46,8 +49,9 @@ public static String readViewFile(String fileName) { */ @Container @ServiceConnection - static OracleContainer oracleContainer = new OracleContainer("gvenzl/oracle-free:23.6-slim-faststart") + static OracleContainer oracleContainer = new OracleContainer("gvenzl/oracle-free:23.7-slim-faststart") .withStartupTimeout(Duration.ofMinutes(5)) + .withInitScript("products.sql") .withUsername("testuser") .withPassword("testpwd"); @@ -100,4 +104,23 @@ void actor() { Optional actorById = dvClient.findById(Actor.class, 1); assertThat(actorById.isPresent()).isTrue(); } + + @Test + void orders() { + Product product = new Product(); + product.setName("my product"); + product.setPrice(100.00); + + dvClient.save(product, Product.class); + Optional productById = dvClient.findById(Product.class, 1); + assertThat(productById.isPresent()).isTrue(); + + Order order = new Order(); + order.setProduct(productById.get()); + order.setQuantity(10); + + dvClient.save(order, Order.class); + Optional OrderById = dvClient.findById(Order.class, 1); + assertThat(OrderById.isPresent()).isTrue(); + } } diff --git a/database/starters/oracle-spring-boot-json-relational-duality-views/src/test/java/com/oracle/spring/json/duality/builder/DualityViewBuilderTest.java b/database/starters/oracle-spring-boot-json-relational-duality-views/src/test/java/com/oracle/spring/json/duality/builder/DualityViewBuilderTest.java index 0239bc98..215aba1d 100644 --- a/database/starters/oracle-spring-boot-json-relational-duality-views/src/test/java/com/oracle/spring/json/duality/builder/DualityViewBuilderTest.java +++ b/database/starters/oracle-spring-boot-json-relational-duality-views/src/test/java/com/oracle/spring/json/duality/builder/DualityViewBuilderTest.java @@ -8,6 +8,7 @@ import com.oracle.spring.json.duality.model.movie.Actor; import com.oracle.spring.json.duality.model.movie.Director; import com.oracle.spring.json.duality.model.movie.Movie; +import com.oracle.spring.json.duality.model.products.Order; import com.oracle.spring.json.duality.model.student.Student; import org.jetbrains.annotations.NotNull; import org.junit.jupiter.params.ParameterizedTest; @@ -24,7 +25,8 @@ public class DualityViewBuilderTest { return Stream.of( Arguments.of(Student.class, "student-update.sql", "update"), Arguments.of(Student.class, "student-create.sql", "create"), - Arguments.of(Actor.class, "actor-create.sql", "create") + Arguments.of(Actor.class, "actor-create.sql", "create"), + Arguments.of(Order.class, "order-create.sql", "create") ); } @@ -34,6 +36,7 @@ public void buildViews(Class entity, String viewFile, String ddlAuto) { String expectedView = readViewFile(viewFile); DualityViewBuilder dualityViewBuilder = getDualityViewBuilder(ddlAuto); String actualView = dualityViewBuilder.build(entity); + System.out.println(actualView); assertThat(expectedView).isEqualTo(actualView); } diff --git a/database/starters/oracle-spring-boot-json-relational-duality-views/src/test/java/com/oracle/spring/json/duality/model/products/Order.java b/database/starters/oracle-spring-boot-json-relational-duality-views/src/test/java/com/oracle/spring/json/duality/model/products/Order.java new file mode 100644 index 00000000..fca66933 --- /dev/null +++ b/database/starters/oracle-spring-boot-json-relational-duality-views/src/test/java/com/oracle/spring/json/duality/model/products/Order.java @@ -0,0 +1,33 @@ +// Copyright (c) 2025, Oracle and/or its affiliates. +// Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl. + +package com.oracle.spring.json.duality.model.products; + +import com.oracle.spring.json.duality.annotation.AccessMode; +import com.oracle.spring.json.duality.annotation.JsonRelationalDualityView; +import jakarta.json.bind.annotation.JsonbProperty; +import jakarta.persistence.Column; +import jakarta.persistence.Table; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.AllArgsConstructor; +import java.util.Date; + +import static com.oracle.spring.json.duality.builder.Annotations._ID_FIELD; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@JsonRelationalDualityView(accessMode = @AccessMode(insert = true)) +@Table(name = "orders") +public class Order { + @JsonbProperty(_ID_FIELD) + @Column(name = "order_id") + private Long id; + @JsonRelationalDualityView(name = "product") + @Column(name = "products") + private Product product; + private Integer quantity; + @Column(name = "order_date") + private Date orderDate; +} \ No newline at end of file diff --git a/database/starters/oracle-spring-boot-json-relational-duality-views/src/test/java/com/oracle/spring/json/duality/model/products/Product.java b/database/starters/oracle-spring-boot-json-relational-duality-views/src/test/java/com/oracle/spring/json/duality/model/products/Product.java new file mode 100644 index 00000000..e8d135ec --- /dev/null +++ b/database/starters/oracle-spring-boot-json-relational-duality-views/src/test/java/com/oracle/spring/json/duality/model/products/Product.java @@ -0,0 +1,31 @@ +// Copyright (c) 2025, Oracle and/or its affiliates. +// Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl. + +package com.oracle.spring.json.duality.model.products; + +import com.oracle.spring.json.duality.annotation.AccessMode; +import com.oracle.spring.json.duality.annotation.JsonRelationalDualityView; +import jakarta.json.bind.annotation.JsonbProperty; +import jakarta.persistence.Column; +import jakarta.persistence.Table; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.AllArgsConstructor; + +import static com.oracle.spring.json.duality.builder.Annotations._ID_FIELD; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Table(name = "products") +@JsonRelationalDualityView( + name = "product_dv", + accessMode = @AccessMode(insert = true) +) +public class Product { + @JsonbProperty(_ID_FIELD) + @Column(name = "product_id") + private Long id; + private String name; + private Double price; +} diff --git a/database/starters/oracle-spring-boot-json-relational-duality-views/src/test/resources/products.sql b/database/starters/oracle-spring-boot-json-relational-duality-views/src/test/resources/products.sql new file mode 100644 index 00000000..d1378acc --- /dev/null +++ b/database/starters/oracle-spring-boot-json-relational-duality-views/src/test/resources/products.sql @@ -0,0 +1,13 @@ +create table products ( + product_id number generated always as identity primary key, + name varchar2(255) not null, + price number(10,2) not null +); + +create table orders ( + order_id number generated always as identity primary key, + product_id number not null, + quantity number not null, + order_date date default sysdate, + constraint fk_orders_product foreign key (product_id) references products(product_id) on delete cascade +); diff --git a/database/starters/oracle-spring-boot-json-relational-duality-views/src/test/resources/views/order-create.sql b/database/starters/oracle-spring-boot-json-relational-duality-views/src/test/resources/views/order-create.sql new file mode 100644 index 00000000..684057cc --- /dev/null +++ b/database/starters/oracle-spring-boot-json-relational-duality-views/src/test/resources/views/order-create.sql @@ -0,0 +1,10 @@ +create force editionable json relational duality view orders_dv as orders @insert { + _id : order_id + product : products { + _id : product_id + name + price + } + quantity + orderDate : order_date +} \ No newline at end of file From 7a8b840d92112c99f8feffce4fb5f3d9a829cc27 Mon Sep 17 00:00:00 2001 From: Anders Swanson Date: Fri, 18 Apr 2025 14:02:40 -0700 Subject: [PATCH 08/13] Duality View builder Signed-off-by: Anders Swanson --- .../json/duality/builder/Annotations.java | 30 +++---- .../duality/builder/DualityViewBuilder.java | 82 +++++++++++++------ .../duality/builder/DualityViewScanner.java | 9 +- .../json/duality/builder/RootSnippet.java | 2 +- .../json/duality/builder/ViewEntity.java | 65 ++++++++------- .../duality/JsonRelationalDualityClient.java | 2 +- .../json/duality/SpringBootDualityTest.java | 12 ++- .../json/duality/model/movie/Actor.java | 10 +-- .../json/duality/model/movie/Director.java | 6 +- .../json/duality/model/movie/DirectorBio.java | 2 +- .../json/duality/model/movie/Movie.java | 12 ++- .../json/duality/model/products/Order.java | 27 ++++-- .../json/duality/model/products/Product.java | 27 ++++-- .../src/test/resources/application.yaml | 14 +++- .../src/test/resources/products.sql | 4 +- .../src/test/resources/views/actor-create.sql | 12 ++- 16 files changed, 208 insertions(+), 108 deletions(-) diff --git a/database/starters/oracle-spring-boot-json-relational-duality-views/src/main/java/com/oracle/spring/json/duality/builder/Annotations.java b/database/starters/oracle-spring-boot-json-relational-duality-views/src/main/java/com/oracle/spring/json/duality/builder/Annotations.java index 11024790..4de81a54 100644 --- a/database/starters/oracle-spring-boot-json-relational-duality-views/src/main/java/com/oracle/spring/json/duality/builder/Annotations.java +++ b/database/starters/oracle-spring-boot-json-relational-duality-views/src/main/java/com/oracle/spring/json/duality/builder/Annotations.java @@ -47,9 +47,9 @@ static String getNestedViewName(Class javaType, JsonRelationalDualityView dvAnnotation, Table tableAnnotation) { if (dvAnnotation != null && StringUtils.hasText(dvAnnotation.name())) { - return dvAnnotation.name().toLowerCase(); + return dvAnnotation.name(); } - return getTableName(javaType, tableAnnotation).toLowerCase(); + return getTableName(javaType, tableAnnotation); } public static String getViewName(Class javaType, JsonRelationalDualityView dvAnnotation) { @@ -92,21 +92,23 @@ static String getDatabaseColumnName(Field f) { return f.getName(); } - static String getAccessModeStr(AccessMode accessMode) { - if (accessMode == null) { - return ""; - } - + static String getAccessModeStr(AccessMode accessMode, ManyToMany manyToMany) { StringBuilder sb = new StringBuilder(); - if (accessMode.insert()) { - sb.append("@insert "); - } - if (accessMode.update()) { - sb.append("@update "); + if (manyToMany != null) { + sb.append("@unnest "); } - if (accessMode.delete()) { - sb.append("@delete "); + if (accessMode != null) { + if (accessMode.insert()) { + sb.append("@insert "); + } + if (accessMode.update()) { + sb.append("@update "); + } + if (accessMode.delete()) { + sb.append("@delete "); + } } + return sb.toString(); } } diff --git a/database/starters/oracle-spring-boot-json-relational-duality-views/src/main/java/com/oracle/spring/json/duality/builder/DualityViewBuilder.java b/database/starters/oracle-spring-boot-json-relational-duality-views/src/main/java/com/oracle/spring/json/duality/builder/DualityViewBuilder.java index 2f3635de..07e90838 100644 --- a/database/starters/oracle-spring-boot-json-relational-duality-views/src/main/java/com/oracle/spring/json/duality/builder/DualityViewBuilder.java +++ b/database/starters/oracle-spring-boot-json-relational-duality-views/src/main/java/com/oracle/spring/json/duality/builder/DualityViewBuilder.java @@ -6,10 +6,11 @@ import java.sql.Connection; import java.sql.SQLException; import java.sql.Statement; -import java.util.ArrayList; -import java.util.List; +import java.util.HashMap; +import java.util.Map; import com.oracle.spring.json.duality.annotation.JsonRelationalDualityView; +import jakarta.annotation.PostConstruct; import javax.sql.DataSource; import org.springframework.beans.factory.DisposableBean; import org.springframework.boot.autoconfigure.orm.jpa.HibernateProperties; @@ -22,11 +23,12 @@ @Component public final class DualityViewBuilder implements DisposableBean { private static final String PREFIX = "JSON Relational Duality Views: "; + private static final int TABLE_OR_VIEW_DOES_NOT_EXIST = 942; private final DataSource dataSource; private final boolean isShowSql; private final RootSnippet rootSnippet; - private final List dualityViews = new ArrayList<>(); + private final Map dualityViews = new HashMap<>(); public DualityViewBuilder(DataSource dataSource, JpaProperties jpaProperties, @@ -38,23 +40,34 @@ public DualityViewBuilder(DataSource dataSource, ); } - void apply(Class javaType) { - if (rootSnippet.equals(RootSnippet.NONE)) { - return; - } - String ddl = build(javaType); - if (isShowSql) { - System.out.println(PREFIX + ddl); - } - if (rootSnippet.equals(RootSnippet.VALIDATE)) { - // TODO: Handle view validation. - return; + void apply() { + switch (rootSnippet) { + case NONE -> { + return; + } + case CREATE_DROP -> { + try { + createDrop(); + } catch (SQLException e) { + throw new RuntimeException(e); + } + } } - runDDL(ddl); + for (String ddl : dualityViews.values()) { + if (isShowSql) { + System.out.println(PREFIX + ddl); + } + if (rootSnippet.equals(RootSnippet.VALIDATE)) { + // TODO: Handle view validation. + return; + } + + runDDL(ddl); + } } - String build(Class javaType) { + public String build(Class javaType) { JsonRelationalDualityView dvAnnotation = javaType.getAnnotation(JsonRelationalDualityView.class); if (dvAnnotation == null) { throw new IllegalArgumentException("%s not found for type %s".formatted( @@ -62,14 +75,16 @@ String build(Class javaType) { ); } String viewName = getViewName(javaType, dvAnnotation); - String accessMode = getAccessModeStr(dvAnnotation.accessMode()); + String accessMode = getAccessModeStr(dvAnnotation.accessMode(), null); ViewEntity ve = new ViewEntity(javaType, new StringBuilder(), rootSnippet, accessMode, viewName, 0); - return ve.build().toString(); + String ddl = ve.build().toString(); + dualityViews.put(viewName, ddl); + return ddl; } private void runDDL(String ddl) { @@ -81,17 +96,38 @@ private void runDDL(String ddl) { } } + @PostConstruct + public void init() throws SQLException { + createDrop(); + } @Override public void destroy() throws Exception { + createDrop(); + } + + private void createDrop() throws SQLException { if (rootSnippet.equals(RootSnippet.CREATE_DROP) && !dualityViews.isEmpty()) { - final String dropView = """ - drop view %s - """; try (Connection conn = dataSource.getConnection(); Statement stmt = conn.createStatement()) { - for (String view : dualityViews) { - stmt.execute(dropView.formatted(view)); + dropViews(stmt); + } + } + } + + private void dropViews(Statement stmt) { + final String dropView = "drop view %s"; + + for (String view : dualityViews.keySet()) { + String dropStatement = dropView.formatted(view); + if (isShowSql) { + System.out.println(PREFIX + dropStatement); + } + try { + stmt.execute(dropStatement); + } catch (SQLException e) { + if (e.getErrorCode() != TABLE_OR_VIEW_DOES_NOT_EXIST) { + throw new RuntimeException(e); } } } diff --git a/database/starters/oracle-spring-boot-json-relational-duality-views/src/main/java/com/oracle/spring/json/duality/builder/DualityViewScanner.java b/database/starters/oracle-spring-boot-json-relational-duality-views/src/main/java/com/oracle/spring/json/duality/builder/DualityViewScanner.java index 704bbbb1..2c2ac4cf 100644 --- a/database/starters/oracle-spring-boot-json-relational-duality-views/src/main/java/com/oracle/spring/json/duality/builder/DualityViewScanner.java +++ b/database/starters/oracle-spring-boot-json-relational-duality-views/src/main/java/com/oracle/spring/json/duality/builder/DualityViewScanner.java @@ -52,21 +52,22 @@ private void applyDvScan(JsonRelationalDualityViewScan dvScan) { } } for (Class javaType : dvScan.basePackageClasses()) { - applyClass(javaType); + addClass(javaType); } + dualityViewBuilder.apply(); } private void scanPackage(String packageName) { Set> types = scanner.findTypes(packageName); for (Class type : types) { - applyClass(type); + addClass(type); } } - private void applyClass(Class javaType) { + private void addClass(Class javaType) { JsonRelationalDualityView dvAnnotation = javaType.getAnnotation(JsonRelationalDualityView.class); if (dvAnnotation != null) { - dualityViewBuilder.apply(javaType); + dualityViewBuilder.build(javaType); } } } diff --git a/database/starters/oracle-spring-boot-json-relational-duality-views/src/main/java/com/oracle/spring/json/duality/builder/RootSnippet.java b/database/starters/oracle-spring-boot-json-relational-duality-views/src/main/java/com/oracle/spring/json/duality/builder/RootSnippet.java index 528ab36b..63f714ea 100644 --- a/database/starters/oracle-spring-boot-json-relational-duality-views/src/main/java/com/oracle/spring/json/duality/builder/RootSnippet.java +++ b/database/starters/oracle-spring-boot-json-relational-duality-views/src/main/java/com/oracle/spring/json/duality/builder/RootSnippet.java @@ -28,7 +28,7 @@ public static RootSnippet fromDdlAuto(String ddlAuto) { case "none" -> NONE; case "validate" -> VALIDATE; case "create" -> CREATE; - case "create_drop" -> CREATE_DROP; + case "create-drop" -> CREATE_DROP; case "update" -> UPDATE; default -> throw new IllegalStateException("Unexpected value: " + ddlAuto); }; diff --git a/database/starters/oracle-spring-boot-json-relational-duality-views/src/main/java/com/oracle/spring/json/duality/builder/ViewEntity.java b/database/starters/oracle-spring-boot-json-relational-duality-views/src/main/java/com/oracle/spring/json/duality/builder/ViewEntity.java index 03119437..e0aba14c 100644 --- a/database/starters/oracle-spring-boot-json-relational-duality-views/src/main/java/com/oracle/spring/json/duality/builder/ViewEntity.java +++ b/database/starters/oracle-spring-boot-json-relational-duality-views/src/main/java/com/oracle/spring/json/duality/builder/ViewEntity.java @@ -12,10 +12,10 @@ import com.oracle.spring.json.duality.annotation.JsonRelationalDualityView; import jakarta.json.bind.annotation.JsonbProperty; import jakarta.persistence.Id; -import jakarta.persistence.JoinColumn; import jakarta.persistence.JoinTable; import jakarta.persistence.ManyToMany; import jakarta.persistence.Table; +import org.springframework.util.StringUtils; import static com.oracle.spring.json.duality.builder.Annotations._ID_FIELD; import static com.oracle.spring.json.duality.builder.Annotations.getAccessModeStr; @@ -27,10 +27,10 @@ import static com.oracle.spring.json.duality.builder.Annotations.isFieldIncluded; final class ViewEntity { - private static final String SEPARATOR = " : "; private static final String END_ENTITY = "}"; - private static final String BEGIN_ENTITY = " {\n"; + private static final String END_ARRAY_ENTITY = "} ]"; + private static final String BEGIN_ARRAY_ENTITY = "[ {\n"; private static final int TAB_WIDTH = 2; private final Class javaType; @@ -91,8 +91,11 @@ private String getStatementPrefix(Table tableAnnotation) { private String getNestedEntityPrefix(Table tableAnnotation) { String tableName = getTableName(javaType, tableAnnotation); - return "%s : %s %s{\n".formatted( - viewName, tableName, accessMode + if (tableName.equals(viewName)) { + return "%s %s{\n".formatted(tableName, accessMode); + } + return "%s%s%s %s{\n".formatted( + viewName, SEPARATOR, tableName, accessMode ); } @@ -136,13 +139,13 @@ private void parseRelationalEntity(Field f, JsonRelationalDualityView dvAnnotati // Add join table if present. ManyToMany manyToMany = f.getAnnotation(ManyToMany.class); if (manyToMany != null) { - parseManyToMany(manyToMany, f, entityJavaType); + parseManyToMany(manyToMany, dvAnnotation, f, entityJavaType); } // Add nested entity. - parseNestedEntity(entityJavaType, dvAnnotation); + parseNestedEntity(entityJavaType, dvAnnotation, manyToMany); // Additional trailer for join table if present. if (manyToMany != null) { - addTrailer(true); + addTrailer(true, END_ARRAY_ENTITY); } } @@ -158,10 +161,10 @@ private Class getGenericFieldType(Field f) { return f.getType(); } - private void parseNestedEntity(Class entityJavaType, JsonRelationalDualityView dvAnnotation) { + private void parseNestedEntity(Class entityJavaType, JsonRelationalDualityView dvAnnotation, ManyToMany manyToMany) { Table tableAnnotation = entityJavaType.getAnnotation(Table.class); - String viewEntityName = getNestedViewName(entityJavaType, dvAnnotation, tableAnnotation); - String accessMode = getAccessModeStr(dvAnnotation.accessMode()); + String viewEntityName = getNestedViewName(entityJavaType, manyToMany == null ? dvAnnotation : null, tableAnnotation); + String accessMode = getAccessModeStr(dvAnnotation.accessMode(), manyToMany); ViewEntity ve = new ViewEntity(entityJavaType, new StringBuilder(), accessMode, @@ -176,23 +179,19 @@ private void parseColumn(Field f) { addProperty(getJsonbPropertyName(f), getDatabaseColumnName(f)); } - private void parseManyToMany(ManyToMany manyToMany, Field f, Class entityJavaType) { + private void parseManyToMany(ManyToMany manyToMany, JsonRelationalDualityView dvAnnotation, Field f, Class entityJavaType) { JoinTable joinTable = getJoinTableAnnotation(f, manyToMany, entityJavaType); - sb.append(getPadding()); - sb.append(joinTable.name()); - sb.append(BEGIN_ENTITY); - incNesting(); - addJoinColumns(joinTable.joinColumns()); - addJoinColumns(joinTable.inverseJoinColumns()); - } - - private void addJoinColumns(JoinColumn[] joinColumns) { - for (JoinColumn joinColumn : joinColumns) { - addProperty(joinColumn.name(), joinColumn.name()); + String propertyName = dvAnnotation.name(); + if (!StringUtils.hasText(propertyName)) { + propertyName = getJsonbPropertyName(f); } + addProperty(propertyName, joinTable.name(), false); + sb.append(" ").append(getAccessModeStr(dvAnnotation.accessMode(), null)); + sb.append(BEGIN_ARRAY_ENTITY); + incNesting(); } - private void addProperty(String jsonbPropertyName, String databaseColumnName) { + private void addProperty(String jsonbPropertyName, String databaseColumnName, boolean addNewLine) { sb.append(getPadding()); if (jsonbPropertyName.equals(databaseColumnName)) { sb.append(jsonbPropertyName); @@ -201,16 +200,26 @@ private void addProperty(String jsonbPropertyName, String databaseColumnName) { .append(SEPARATOR) .append(databaseColumnName); } - sb.append("\n"); + if (addNewLine) { + sb.append("\n"); + } + } + + private void addProperty(String jsonbPropertyName, String databaseColumnName) { + addProperty(jsonbPropertyName, databaseColumnName, true); + } + + private void addTrailer(boolean addNewLine) { + addTrailer(addNewLine, END_ENTITY); } - private void addTrailer(boolean addNewline) { + private void addTrailer(boolean addNewLine, String terminal) { decNesting(); if (nesting > 0) { sb.append(getPadding()); } - sb.append(END_ENTITY); - if (addNewline) { + sb.append(terminal); + if (addNewLine) { sb.append("\n"); } } diff --git a/database/starters/oracle-spring-boot-json-relational-duality-views/src/test/java/com/oracle/spring/json/duality/JsonRelationalDualityClient.java b/database/starters/oracle-spring-boot-json-relational-duality-views/src/test/java/com/oracle/spring/json/duality/JsonRelationalDualityClient.java index cd7af213..f81c1ab1 100644 --- a/database/starters/oracle-spring-boot-json-relational-duality-views/src/test/java/com/oracle/spring/json/duality/JsonRelationalDualityClient.java +++ b/database/starters/oracle-spring-boot-json-relational-duality-views/src/test/java/com/oracle/spring/json/duality/JsonRelationalDualityClient.java @@ -27,7 +27,7 @@ public JsonRelationalDualityClient(JdbcClient jdbcClient, JSONB jsonb) { public int save(T entity, Class entityJavaType) { String viewName = getViewName(entityJavaType, entityJavaType.getAnnotation(JsonRelationalDualityView.class)); final String sql = """ - insert into %s (data) values(?) + insert into %s (data) values (?) """.formatted(viewName); byte[] oson = jsonb.toOSON(entity); diff --git a/database/starters/oracle-spring-boot-json-relational-duality-views/src/test/java/com/oracle/spring/json/duality/SpringBootDualityTest.java b/database/starters/oracle-spring-boot-json-relational-duality-views/src/test/java/com/oracle/spring/json/duality/SpringBootDualityTest.java index 277aa70c..d5ef9ee8 100644 --- a/database/starters/oracle-spring-boot-json-relational-duality-views/src/test/java/com/oracle/spring/json/duality/SpringBootDualityTest.java +++ b/database/starters/oracle-spring-boot-json-relational-duality-views/src/test/java/com/oracle/spring/json/duality/SpringBootDualityTest.java @@ -81,27 +81,35 @@ void student() { @Test void actor() { + String actorId = UUID.randomUUID().toString(); + String directorId = UUID.randomUUID().toString(); + String movieId = UUID.randomUUID().toString(); + DirectorBio directorBio = new DirectorBio(); + directorBio.setDirectorId(directorId); directorBio.setBiography("biography"); Director director = new Director(); + directorBio.setDirectorId(directorId); director.setDirectorBio(directorBio); director.setFirstName("John"); director.setLastName("Doe"); Movie m = new Movie(); - m.setDirector(director); + m.setMovieId(movieId); m.setTitle("my movie"); m.setGenre("action"); m.setReleaseYear(1993); + dvClient.save(m, Movie.class); Actor actor = new Actor(); + actor.setActorId(actorId); actor.setFirstName("John"); actor.setLastName("Doe"); actor.setMovies(Set.of(m)); dvClient.save(actor, Actor.class); - Optional actorById = dvClient.findById(Actor.class, 1); + Optional actorById = dvClient.findById(Actor.class, actorId); assertThat(actorById.isPresent()).isTrue(); } diff --git a/database/starters/oracle-spring-boot-json-relational-duality-views/src/test/java/com/oracle/spring/json/duality/model/movie/Actor.java b/database/starters/oracle-spring-boot-json-relational-duality-views/src/test/java/com/oracle/spring/json/duality/model/movie/Actor.java index 0ad13d85..33ef62ec 100644 --- a/database/starters/oracle-spring-boot-json-relational-duality-views/src/test/java/com/oracle/spring/json/duality/model/movie/Actor.java +++ b/database/starters/oracle-spring-boot-json-relational-duality-views/src/test/java/com/oracle/spring/json/duality/model/movie/Actor.java @@ -7,12 +7,11 @@ import java.util.Objects; import java.util.Set; +import com.oracle.spring.json.duality.annotation.AccessMode; import com.oracle.spring.json.duality.annotation.JsonRelationalDualityView; import jakarta.json.bind.annotation.JsonbProperty; import jakarta.persistence.Column; import jakarta.persistence.Entity; -import jakarta.persistence.GeneratedValue; -import jakarta.persistence.GenerationType; import jakarta.persistence.Id; import jakarta.persistence.ManyToMany; import jakarta.persistence.Table; @@ -25,13 +24,12 @@ @Table(name = "actor") @Getter @Setter -@JsonRelationalDualityView +@JsonRelationalDualityView(accessMode = @AccessMode(insert = true)) public class Actor { @JsonbProperty(_ID_FIELD) @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) @Column(name = "actor_id") - private Long actorId; + private String actorId; @Column(name = "first_name", nullable = false, length = 50) private String firstName; @@ -40,7 +38,7 @@ public class Actor { private String lastName; @ManyToMany(mappedBy = "actors") - @JsonRelationalDualityView + @JsonRelationalDualityView(name = "movies", accessMode = @AccessMode(insert = true)) private Set movies; /** diff --git a/database/starters/oracle-spring-boot-json-relational-duality-views/src/test/java/com/oracle/spring/json/duality/model/movie/Director.java b/database/starters/oracle-spring-boot-json-relational-duality-views/src/test/java/com/oracle/spring/json/duality/model/movie/Director.java index 201d22bb..d1e82a82 100644 --- a/database/starters/oracle-spring-boot-json-relational-duality-views/src/test/java/com/oracle/spring/json/duality/model/movie/Director.java +++ b/database/starters/oracle-spring-boot-json-relational-duality-views/src/test/java/com/oracle/spring/json/duality/model/movie/Director.java @@ -31,9 +31,8 @@ public class Director { @JsonbProperty(_ID_FIELD) @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) @Column(name = "director_id") - private Long directorId; + private String directorId; @Column(name = "first_name", nullable = false, length = 50) private String firstName; @@ -52,7 +51,8 @@ public class Director { ) // The primary key of the Director entity is used as the foreign key of the DirectorBio entity. @PrimaryKeyJoinColumn - @JsonRelationalDualityView + @JsonbTransient + //@JsonRelationalDualityView(name = "directorBio", accessMode = @AccessMode(insert = true)) private DirectorBio directorBio; public void setDirectorBio(DirectorBio directorBio) { diff --git a/database/starters/oracle-spring-boot-json-relational-duality-views/src/test/java/com/oracle/spring/json/duality/model/movie/DirectorBio.java b/database/starters/oracle-spring-boot-json-relational-duality-views/src/test/java/com/oracle/spring/json/duality/model/movie/DirectorBio.java index c7e14482..38ac1757 100644 --- a/database/starters/oracle-spring-boot-json-relational-duality-views/src/test/java/com/oracle/spring/json/duality/model/movie/DirectorBio.java +++ b/database/starters/oracle-spring-boot-json-relational-duality-views/src/test/java/com/oracle/spring/json/duality/model/movie/DirectorBio.java @@ -28,7 +28,7 @@ public class DirectorBio { @JsonbProperty(_ID_FIELD) @Id @Column(name = "director_id") - private Long directorId; + private String directorId; @OneToOne(fetch = FetchType.LAZY) // The primary key will be copied from the director entity diff --git a/database/starters/oracle-spring-boot-json-relational-duality-views/src/test/java/com/oracle/spring/json/duality/model/movie/Movie.java b/database/starters/oracle-spring-boot-json-relational-duality-views/src/test/java/com/oracle/spring/json/duality/model/movie/Movie.java index 11e59018..35b9eeae 100644 --- a/database/starters/oracle-spring-boot-json-relational-duality-views/src/test/java/com/oracle/spring/json/duality/model/movie/Movie.java +++ b/database/starters/oracle-spring-boot-json-relational-duality-views/src/test/java/com/oracle/spring/json/duality/model/movie/Movie.java @@ -6,6 +6,7 @@ import java.util.Objects; import java.util.Set; +import com.oracle.spring.json.duality.annotation.AccessMode; import com.oracle.spring.json.duality.annotation.JsonRelationalDualityView; import jakarta.json.bind.annotation.JsonbProperty; import jakarta.json.bind.annotation.JsonbTransient; @@ -28,12 +29,14 @@ @Table(name = "movie") @Getter @Setter +@JsonRelationalDualityView(name = "movie_dv", accessMode = @AccessMode( + insert = true +)) public class Movie { @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) @Column(name = "movie_id") - @JsonbProperty(_ID_FIELD) - private Long movieId; + @JsonbProperty("_id") + private String movieId; @Column(name = "title", nullable = false, length = 100) private String title; @@ -46,7 +49,8 @@ public class Movie { @ManyToOne @JoinColumn(name = "director_id") - @JsonRelationalDualityView + //@JsonRelationalDualityView(accessMode = @AccessMode(insert = true)) + @JsonbTransient private Director director; @ManyToMany diff --git a/database/starters/oracle-spring-boot-json-relational-duality-views/src/test/java/com/oracle/spring/json/duality/model/products/Order.java b/database/starters/oracle-spring-boot-json-relational-duality-views/src/test/java/com/oracle/spring/json/duality/model/products/Order.java index fca66933..950082c2 100644 --- a/database/starters/oracle-spring-boot-json-relational-duality-views/src/test/java/com/oracle/spring/json/duality/model/products/Order.java +++ b/database/starters/oracle-spring-boot-json-relational-duality-views/src/test/java/com/oracle/spring/json/duality/model/products/Order.java @@ -8,16 +8,16 @@ import jakarta.json.bind.annotation.JsonbProperty; import jakarta.persistence.Column; import jakarta.persistence.Table; -import lombok.Data; -import lombok.NoArgsConstructor; -import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.Setter; + import java.util.Date; +import java.util.Objects; import static com.oracle.spring.json.duality.builder.Annotations._ID_FIELD; -@Data -@NoArgsConstructor -@AllArgsConstructor +@Getter +@Setter @JsonRelationalDualityView(accessMode = @AccessMode(insert = true)) @Table(name = "orders") public class Order { @@ -30,4 +30,19 @@ public class Order { private Integer quantity; @Column(name = "order_date") private Date orderDate; + + @Override + public final boolean equals(Object o) { + if (!(o instanceof Order order)) return false; + + return Objects.equals(getId(), order.getId()) && Objects.equals(getQuantity(), order.getQuantity()) && Objects.equals(getOrderDate(), order.getOrderDate()); + } + + @Override + public int hashCode() { + int result = Objects.hashCode(getId()); + result = 31 * result + Objects.hashCode(getQuantity()); + result = 31 * result + Objects.hashCode(getOrderDate()); + return result; + } } \ No newline at end of file diff --git a/database/starters/oracle-spring-boot-json-relational-duality-views/src/test/java/com/oracle/spring/json/duality/model/products/Product.java b/database/starters/oracle-spring-boot-json-relational-duality-views/src/test/java/com/oracle/spring/json/duality/model/products/Product.java index e8d135ec..51d3b6e4 100644 --- a/database/starters/oracle-spring-boot-json-relational-duality-views/src/test/java/com/oracle/spring/json/duality/model/products/Product.java +++ b/database/starters/oracle-spring-boot-json-relational-duality-views/src/test/java/com/oracle/spring/json/duality/model/products/Product.java @@ -3,20 +3,20 @@ package com.oracle.spring.json.duality.model.products; +import java.util.Objects; + import com.oracle.spring.json.duality.annotation.AccessMode; import com.oracle.spring.json.duality.annotation.JsonRelationalDualityView; import jakarta.json.bind.annotation.JsonbProperty; import jakarta.persistence.Column; import jakarta.persistence.Table; -import lombok.Data; -import lombok.NoArgsConstructor; -import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.Setter; import static com.oracle.spring.json.duality.builder.Annotations._ID_FIELD; -@Data -@NoArgsConstructor -@AllArgsConstructor +@Getter +@Setter @Table(name = "products") @JsonRelationalDualityView( name = "product_dv", @@ -28,4 +28,19 @@ public class Product { private Long id; private String name; private Double price; + + @Override + public final boolean equals(Object o) { + if (!(o instanceof Product product)) return false; + + return Objects.equals(getId(), product.getId()) && Objects.equals(getName(), product.getName()) && Objects.equals(getPrice(), product.getPrice()); + } + + @Override + public int hashCode() { + int result = Objects.hashCode(getId()); + result = 31 * result + Objects.hashCode(getName()); + result = 31 * result + Objects.hashCode(getPrice()); + return result; + } } diff --git a/database/starters/oracle-spring-boot-json-relational-duality-views/src/test/resources/application.yaml b/database/starters/oracle-spring-boot-json-relational-duality-views/src/test/resources/application.yaml index 39a461f1..64c69a27 100644 --- a/database/starters/oracle-spring-boot-json-relational-duality-views/src/test/resources/application.yaml +++ b/database/starters/oracle-spring-boot-json-relational-duality-views/src/test/resources/application.yaml @@ -1,5 +1,17 @@ spring: + datasource: + url: ${DB_URL} + username: ${DB_USERNAME} + password: ${DB_PASSWORD} + driver-class-name: oracle.jdbc.OracleDriver + type: oracle.ucp.jdbc.PoolDataSource + oracleucp: + connection-factory-class-name: oracle.jdbc.pool.OracleDataSource + connection-pool-name: ConsumerConnectionPool + initial-pool-size: 15 + min-pool-size: 10 + max-pool-size: 30 jpa: hibernate: - ddl-auto: create + ddl-auto: create-drop show-sql: true diff --git a/database/starters/oracle-spring-boot-json-relational-duality-views/src/test/resources/products.sql b/database/starters/oracle-spring-boot-json-relational-duality-views/src/test/resources/products.sql index d1378acc..ced55ae8 100644 --- a/database/starters/oracle-spring-boot-json-relational-duality-views/src/test/resources/products.sql +++ b/database/starters/oracle-spring-boot-json-relational-duality-views/src/test/resources/products.sql @@ -5,9 +5,11 @@ create table products ( ); create table orders ( - order_id number generated always as identity primary key, + order_id number generated always as identity primary key, product_id number not null, quantity number not null, order_date date default sysdate, constraint fk_orders_product foreign key (product_id) references products(product_id) on delete cascade ); + + diff --git a/database/starters/oracle-spring-boot-json-relational-duality-views/src/test/resources/views/actor-create.sql b/database/starters/oracle-spring-boot-json-relational-duality-views/src/test/resources/views/actor-create.sql index 99378d6a..7cb9a108 100644 --- a/database/starters/oracle-spring-boot-json-relational-duality-views/src/test/resources/views/actor-create.sql +++ b/database/starters/oracle-spring-boot-json-relational-duality-views/src/test/resources/views/actor-create.sql @@ -2,23 +2,21 @@ create force editionable json relational duality view actor_dv as actor { _id : actor_id firstName : first_name lastName : last_name - movie_actor { - movie_id - actor_id - movie : movie { + movies : movie_actor [ { + movie @unnest { _id : movie_id title releaseYear : release_year genre - director : director { + director { _id : director_id firstName : first_name lastName : last_name - director_bio : director_bio { + director_bio { _id : director_id biography } } } - } + } ] } \ No newline at end of file From 8bc099a11844aa8394969ce911b2d57ad5937736 Mon Sep 17 00:00:00 2001 From: Anders Swanson Date: Mon, 21 Apr 2025 10:58:15 -0700 Subject: [PATCH 09/13] Duality view builder Signed-off-by: Anders Swanson --- .../json/duality/builder/ViewEntity.java | 47 ++++++++++++-- .../json/duality/SpringBootDualityTest.java | 28 +++++++- .../builder/DualityViewBuilderTest.java | 6 +- .../spring/json/duality/model/book/Book.java | 53 +++++++++++++++ .../spring/json/duality/model/book/Loan.java | 65 +++++++++++++++++++ .../json/duality/model/book/Member.java | 49 ++++++++++++++ .../json/duality/model/movie/Director.java | 3 - .../json/duality/model/movie/Movie.java | 4 -- .../src/test/resources/views/actor-create.sql | 15 +---- .../resources/views/member-create-drop.sql | 11 ++++ 10 files changed, 251 insertions(+), 30 deletions(-) create mode 100644 database/starters/oracle-spring-boot-json-relational-duality-views/src/test/java/com/oracle/spring/json/duality/model/book/Book.java create mode 100644 database/starters/oracle-spring-boot-json-relational-duality-views/src/test/java/com/oracle/spring/json/duality/model/book/Loan.java create mode 100644 database/starters/oracle-spring-boot-json-relational-duality-views/src/test/java/com/oracle/spring/json/duality/model/book/Member.java create mode 100644 database/starters/oracle-spring-boot-json-relational-duality-views/src/test/resources/views/member-create-drop.sql diff --git a/database/starters/oracle-spring-boot-json-relational-duality-views/src/main/java/com/oracle/spring/json/duality/builder/ViewEntity.java b/database/starters/oracle-spring-boot-json-relational-duality-views/src/main/java/com/oracle/spring/json/duality/builder/ViewEntity.java index e0aba14c..c97def1d 100644 --- a/database/starters/oracle-spring-boot-json-relational-duality-views/src/main/java/com/oracle/spring/json/duality/builder/ViewEntity.java +++ b/database/starters/oracle-spring-boot-json-relational-duality-views/src/main/java/com/oracle/spring/json/duality/builder/ViewEntity.java @@ -27,10 +27,15 @@ import static com.oracle.spring.json.duality.builder.Annotations.isFieldIncluded; final class ViewEntity { + // Separates JSON keys from database column names private static final String SEPARATOR = " : "; - private static final String END_ENTITY = "}"; - private static final String END_ARRAY_ENTITY = "} ]"; - private static final String BEGIN_ARRAY_ENTITY = "[ {\n"; + // Terminal for view entity + private static final String OBJECT_TERMINAL = "}"; + // Terminal for view array entity + private static final String ARRAY_TERMINAL = "} ]"; + // Begin array entity + private static final String BEGIN_ARRAY = "[ {\n"; + // Nesting spacing private static final int TAB_WIDTH = 2; private final Class javaType; @@ -38,6 +43,7 @@ final class ViewEntity { private final RootSnippet rootSnippet; private final String accessMode; private final String viewName; + // Tracks number of spaces for key nesting (pretty print) private int nesting; // Track parent types to prevent stacking of nested types @@ -61,27 +67,41 @@ void addParentTypes(Set> parentTypes) { this.parentTypes.addAll(parentTypes); } + /** + * Parse view from javaType. + * @return this + */ ViewEntity build() { Table tableAnnotation = javaType.getAnnotation(Table.class); if (rootSnippet != null) { - // Root duality view statement + // Add create view snippet sb.append(getStatementPrefix(tableAnnotation)); } else { + // Process nested entity sb.append(getPadding()); sb.append(getNestedEntityPrefix(tableAnnotation)); } + // Increment the nesting (left padding) after processing an entity. incNesting(); + // Parse each field of the javaType. for (Field f : javaType.getDeclaredFields()) { if (isFieldIncluded(f)) { parseField(f); } } + // Close the entity after processing fields. addTrailer(rootSnippet == null); return this; } + /** + * Parse the javaType and tableAnnotation to generate the view prefix, e.g., + * 'create force editionable json relational duality view my_view as my table @insert @update @delete {} + * @param tableAnnotation of the javaType. + * @return view prefix String. + */ private String getStatementPrefix(Table tableAnnotation) { String tableName = getTableName(javaType, tableAnnotation); return "%s %s as %s %s{\n".formatted( @@ -103,14 +123,21 @@ private void parseField(Field f) { JsonRelationalDualityView dvAnnotation; Id id = f.getAnnotation(Id.class); if (id != null && rootSnippet != null) { + // Parse the root entity's _id field. parseId(f); } else if ((dvAnnotation = f.getAnnotation(JsonRelationalDualityView.class)) != null) { + // Parse the related sub-entity. parseRelationalEntity(f, dvAnnotation); } else { + // Parse the field as a database column. parseColumn(f); } } + /** + * Parse the view's root _id field. + * @param f The view's root _id field. + */ private void parseId(Field f) { String jsonbPropertyName = getJsonbPropertyName(f); if (!jsonbPropertyName.equals(_ID_FIELD)) { @@ -121,6 +148,7 @@ private void parseId(Field f) { _ID_FIELD )); } + // Add the root _id field to the view. addProperty(_ID_FIELD, getDatabaseColumnName(f)); } @@ -145,10 +173,15 @@ private void parseRelationalEntity(Field f, JsonRelationalDualityView dvAnnotati parseNestedEntity(entityJavaType, dvAnnotation, manyToMany); // Additional trailer for join table if present. if (manyToMany != null) { - addTrailer(true, END_ARRAY_ENTITY); + addTrailer(true, ARRAY_TERMINAL); } } + /** + * Returns the type of f, or parameterized type of f. + * @param f to introspect for type information. + * @return type of f or parameterized type of f. + */ private Class getGenericFieldType(Field f) { Type genericType = f.getGenericType(); if (genericType instanceof ParameterizedType p) { @@ -187,7 +220,7 @@ private void parseManyToMany(ManyToMany manyToMany, JsonRelationalDualityView dv } addProperty(propertyName, joinTable.name(), false); sb.append(" ").append(getAccessModeStr(dvAnnotation.accessMode(), null)); - sb.append(BEGIN_ARRAY_ENTITY); + sb.append(BEGIN_ARRAY); incNesting(); } @@ -210,7 +243,7 @@ private void addProperty(String jsonbPropertyName, String databaseColumnName) { } private void addTrailer(boolean addNewLine) { - addTrailer(addNewLine, END_ENTITY); + addTrailer(addNewLine, OBJECT_TERMINAL); } private void addTrailer(boolean addNewLine, String terminal) { diff --git a/database/starters/oracle-spring-boot-json-relational-duality-views/src/test/java/com/oracle/spring/json/duality/SpringBootDualityTest.java b/database/starters/oracle-spring-boot-json-relational-duality-views/src/test/java/com/oracle/spring/json/duality/SpringBootDualityTest.java index d5ef9ee8..ef890253 100644 --- a/database/starters/oracle-spring-boot-json-relational-duality-views/src/test/java/com/oracle/spring/json/duality/SpringBootDualityTest.java +++ b/database/starters/oracle-spring-boot-json-relational-duality-views/src/test/java/com/oracle/spring/json/duality/SpringBootDualityTest.java @@ -8,12 +8,15 @@ import java.nio.file.Files; import java.nio.file.Path; import java.time.Duration; -import java.util.Date; +import java.util.List; import java.util.Optional; import java.util.Set; import java.util.UUID; import com.oracle.spring.json.duality.builder.DualityViewScanner; +import com.oracle.spring.json.duality.model.book.Book; +import com.oracle.spring.json.duality.model.book.Loan; +import com.oracle.spring.json.duality.model.book.Member; import com.oracle.spring.json.duality.model.movie.Actor; import com.oracle.spring.json.duality.model.movie.Director; import com.oracle.spring.json.duality.model.movie.DirectorBio; @@ -131,4 +134,27 @@ void orders() { Optional OrderById = dvClient.findById(Order.class, 1); assertThat(OrderById.isPresent()).isTrue(); } + + @Test + void books() { + Book book = new Book(); + book.setTitle("my book"); + + dvClient.save(book, Book.class); + + Loan loan = new Loan(); + loan.setBook(book); + + Member member = new Member(); + member.setFullName("member"); + member.setLoans(List.of(loan)); + + dvClient.save(member, Member.class); + + Optional byId = dvClient.findById(Member.class, 1); + assertThat(byId.isPresent()).isTrue(); + assertThat(byId.get().getFullName()).isEqualTo("member"); + assertThat(byId.get().getLoans()).hasSize(1); + assertThat(byId.get().getLoans().get(0).getBook().getTitle()).isEqualTo("my book"); + } } diff --git a/database/starters/oracle-spring-boot-json-relational-duality-views/src/test/java/com/oracle/spring/json/duality/builder/DualityViewBuilderTest.java b/database/starters/oracle-spring-boot-json-relational-duality-views/src/test/java/com/oracle/spring/json/duality/builder/DualityViewBuilderTest.java index 215aba1d..cf4bda7d 100644 --- a/database/starters/oracle-spring-boot-json-relational-duality-views/src/test/java/com/oracle/spring/json/duality/builder/DualityViewBuilderTest.java +++ b/database/starters/oracle-spring-boot-json-relational-duality-views/src/test/java/com/oracle/spring/json/duality/builder/DualityViewBuilderTest.java @@ -5,9 +5,8 @@ import java.util.stream.Stream; +import com.oracle.spring.json.duality.model.book.Member; import com.oracle.spring.json.duality.model.movie.Actor; -import com.oracle.spring.json.duality.model.movie.Director; -import com.oracle.spring.json.duality.model.movie.Movie; import com.oracle.spring.json.duality.model.products.Order; import com.oracle.spring.json.duality.model.student.Student; import org.jetbrains.annotations.NotNull; @@ -26,7 +25,8 @@ public class DualityViewBuilderTest { Arguments.of(Student.class, "student-update.sql", "update"), Arguments.of(Student.class, "student-create.sql", "create"), Arguments.of(Actor.class, "actor-create.sql", "create"), - Arguments.of(Order.class, "order-create.sql", "create") + Arguments.of(Order.class, "order-create.sql", "create"), + Arguments.of(Member.class, "member-create-drop.sql", "create-drop") ); } diff --git a/database/starters/oracle-spring-boot-json-relational-duality-views/src/test/java/com/oracle/spring/json/duality/model/book/Book.java b/database/starters/oracle-spring-boot-json-relational-duality-views/src/test/java/com/oracle/spring/json/duality/model/book/Book.java new file mode 100644 index 00000000..1b3213eb --- /dev/null +++ b/database/starters/oracle-spring-boot-json-relational-duality-views/src/test/java/com/oracle/spring/json/duality/model/book/Book.java @@ -0,0 +1,53 @@ +// Copyright (c) 2025, Oracle and/or its affiliates. +// Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl. +package com.oracle.spring.json.duality.model.book; + +import java.util.Objects; + +import com.oracle.spring.json.duality.annotation.AccessMode; +import com.oracle.spring.json.duality.annotation.JsonRelationalDualityView; +import jakarta.json.bind.annotation.JsonbProperty; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import lombok.Getter; +import lombok.Setter; + +import static com.oracle.spring.json.duality.builder.Annotations._ID_FIELD; + +@Entity +@Table(name = "books") +@JsonRelationalDualityView(name = "book_dv", accessMode = @AccessMode( + insert = true, + update = true +)) +@Getter +@Setter +public class Book { + + @Id + @JsonbProperty(_ID_FIELD) + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "book_id") + private Long bookId; + + @Column(nullable = false) + private String title; + + @Override + public final boolean equals(Object o) { + if (!(o instanceof Book book)) return false; + + return Objects.equals(bookId, book.bookId) && Objects.equals(title, book.title); + } + + @Override + public int hashCode() { + int result = Objects.hashCode(bookId); + result = 31 * result + Objects.hashCode(title); + return result; + } +} \ No newline at end of file diff --git a/database/starters/oracle-spring-boot-json-relational-duality-views/src/test/java/com/oracle/spring/json/duality/model/book/Loan.java b/database/starters/oracle-spring-boot-json-relational-duality-views/src/test/java/com/oracle/spring/json/duality/model/book/Loan.java new file mode 100644 index 00000000..8dca9076 --- /dev/null +++ b/database/starters/oracle-spring-boot-json-relational-duality-views/src/test/java/com/oracle/spring/json/duality/model/book/Loan.java @@ -0,0 +1,65 @@ +// Copyright (c) 2025, Oracle and/or its affiliates. +// Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl. +package com.oracle.spring.json.duality.model.book; + +import java.util.Objects; + +import com.oracle.spring.json.duality.annotation.AccessMode; +import com.oracle.spring.json.duality.annotation.JsonRelationalDualityView; +import jakarta.json.bind.annotation.JsonbProperty; +import jakarta.json.bind.annotation.JsonbTransient; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; +import lombok.Getter; +import lombok.Setter; + +import static com.oracle.spring.json.duality.builder.Annotations._ID_FIELD; + +@Entity +@Table(name = "loans") +@Getter +@Setter +@JsonRelationalDualityView(name = "loan_dv", accessMode = @AccessMode( + insert = true, + update = true +)) +public class Loan { + + @Id + @JsonbProperty(_ID_FIELD) + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "loan_id") + private Long loanId; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "member_id", nullable = false) + @JsonbTransient + private Member member; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "book_id", nullable = false) + @JsonRelationalDualityView(name = "book", accessMode = @AccessMode( + insert = true, + update = true + )) + private Book book; + + @Override + public final boolean equals(Object o) { + if (!(o instanceof Loan loan)) return false; + + return Objects.equals(getLoanId(), loan.getLoanId()); + } + + @Override + public int hashCode() { + return Objects.hashCode(getLoanId()); + } +} diff --git a/database/starters/oracle-spring-boot-json-relational-duality-views/src/test/java/com/oracle/spring/json/duality/model/book/Member.java b/database/starters/oracle-spring-boot-json-relational-duality-views/src/test/java/com/oracle/spring/json/duality/model/book/Member.java new file mode 100644 index 00000000..7ae50eed --- /dev/null +++ b/database/starters/oracle-spring-boot-json-relational-duality-views/src/test/java/com/oracle/spring/json/duality/model/book/Member.java @@ -0,0 +1,49 @@ +// Copyright (c) 2025, Oracle and/or its affiliates. +// Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl. +package com.oracle.spring.json.duality.model.book; + +import java.util.List; + +import com.oracle.spring.json.duality.annotation.AccessMode; +import com.oracle.spring.json.duality.annotation.JsonRelationalDualityView; +import jakarta.json.bind.annotation.JsonbProperty; +import jakarta.persistence.CascadeType; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.OneToMany; +import jakarta.persistence.Table; +import lombok.Getter; +import lombok.Setter; + +import static com.oracle.spring.json.duality.builder.Annotations._ID_FIELD; + +@Entity +@Table(name = "members") +@JsonRelationalDualityView(accessMode = @AccessMode( + insert = true, + update = true, + delete = true +)) +@Getter +@Setter +public class Member { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @JsonbProperty(_ID_FIELD) + @Column(name = "member_id") + private Long memberId; + + @Column(name = "name", nullable = false) + private String fullName; + + @JsonRelationalDualityView(name = "loans", accessMode = @AccessMode( + insert = true, + update = true + )) + @OneToMany(mappedBy = "member", cascade = CascadeType.ALL) + private List loans; +} diff --git a/database/starters/oracle-spring-boot-json-relational-duality-views/src/test/java/com/oracle/spring/json/duality/model/movie/Director.java b/database/starters/oracle-spring-boot-json-relational-duality-views/src/test/java/com/oracle/spring/json/duality/model/movie/Director.java index d1e82a82..9f00169b 100644 --- a/database/starters/oracle-spring-boot-json-relational-duality-views/src/test/java/com/oracle/spring/json/duality/model/movie/Director.java +++ b/database/starters/oracle-spring-boot-json-relational-duality-views/src/test/java/com/oracle/spring/json/duality/model/movie/Director.java @@ -6,14 +6,11 @@ import java.util.Objects; import java.util.Set; -import com.oracle.spring.json.duality.annotation.JsonRelationalDualityView; import jakarta.json.bind.annotation.JsonbProperty; import jakarta.json.bind.annotation.JsonbTransient; import jakarta.persistence.CascadeType; import jakarta.persistence.Column; import jakarta.persistence.Entity; -import jakarta.persistence.GeneratedValue; -import jakarta.persistence.GenerationType; import jakarta.persistence.Id; import jakarta.persistence.OneToMany; import jakarta.persistence.OneToOne; diff --git a/database/starters/oracle-spring-boot-json-relational-duality-views/src/test/java/com/oracle/spring/json/duality/model/movie/Movie.java b/database/starters/oracle-spring-boot-json-relational-duality-views/src/test/java/com/oracle/spring/json/duality/model/movie/Movie.java index 35b9eeae..9148ec1a 100644 --- a/database/starters/oracle-spring-boot-json-relational-duality-views/src/test/java/com/oracle/spring/json/duality/model/movie/Movie.java +++ b/database/starters/oracle-spring-boot-json-relational-duality-views/src/test/java/com/oracle/spring/json/duality/model/movie/Movie.java @@ -12,8 +12,6 @@ import jakarta.json.bind.annotation.JsonbTransient; import jakarta.persistence.Column; import jakarta.persistence.Entity; -import jakarta.persistence.GeneratedValue; -import jakarta.persistence.GenerationType; import jakarta.persistence.Id; import jakarta.persistence.JoinColumn; import jakarta.persistence.JoinTable; @@ -23,8 +21,6 @@ import lombok.Getter; import lombok.Setter; -import static com.oracle.spring.json.duality.builder.Annotations._ID_FIELD; - @Entity @Table(name = "movie") @Getter diff --git a/database/starters/oracle-spring-boot-json-relational-duality-views/src/test/resources/views/actor-create.sql b/database/starters/oracle-spring-boot-json-relational-duality-views/src/test/resources/views/actor-create.sql index 7cb9a108..3fbd8560 100644 --- a/database/starters/oracle-spring-boot-json-relational-duality-views/src/test/resources/views/actor-create.sql +++ b/database/starters/oracle-spring-boot-json-relational-duality-views/src/test/resources/views/actor-create.sql @@ -1,22 +1,13 @@ -create force editionable json relational duality view actor_dv as actor { +create force editionable json relational duality view actor_dv as actor @insert { _id : actor_id firstName : first_name lastName : last_name - movies : movie_actor [ { - movie @unnest { + movies : movie_actor @insert [ { + movie @unnest @insert { _id : movie_id title releaseYear : release_year genre - director { - _id : director_id - firstName : first_name - lastName : last_name - director_bio { - _id : director_id - biography - } - } } } ] } \ No newline at end of file diff --git a/database/starters/oracle-spring-boot-json-relational-duality-views/src/test/resources/views/member-create-drop.sql b/database/starters/oracle-spring-boot-json-relational-duality-views/src/test/resources/views/member-create-drop.sql new file mode 100644 index 00000000..76b86e75 --- /dev/null +++ b/database/starters/oracle-spring-boot-json-relational-duality-views/src/test/resources/views/member-create-drop.sql @@ -0,0 +1,11 @@ +create force editionable json relational duality view members_dv as members @insert @update @delete { + _id : member_id + fullName : name + loans @insert @update { + _id : loan_id + book : books @insert @update { + _id : book_id + title + } + } +} \ No newline at end of file From 6f1f842acbc23467d2abd19f029f3129857aa534 Mon Sep 17 00:00:00 2001 From: Anders Swanson Date: Mon, 21 Apr 2025 14:15:41 -0700 Subject: [PATCH 10/13] Duality view builder Signed-off-by: Anders Swanson --- .../annotation/JsonRelationalDualityView.java | 2 + .../json/duality/builder/Annotations.java | 8 ++- .../duality/builder/DualityViewBuilder.java | 5 +- .../json/duality/builder/ViewEntity.java | 71 +++++++++++++------ .../json/duality/SpringBootDualityTest.java | 16 +++++ .../builder/DualityViewBuilderTest.java | 4 +- .../json/duality/model/employee/Employee.java | 70 ++++++++++++++++++ .../model/employee/ManagerAdapter.java | 23 ++++++ .../model/employee/ReportsAdapter.java | 30 ++++++++ .../model/employee/SimpleEmployee.java | 13 ++++ .../src/test/resources/views/actor-create.sql | 2 +- .../test/resources/views/employee-create.sql | 12 ++++ .../resources/views/member-create-drop.sql | 2 +- .../src/test/resources/views/order-create.sql | 4 +- 14 files changed, 231 insertions(+), 31 deletions(-) create mode 100644 database/starters/oracle-spring-boot-json-relational-duality-views/src/test/java/com/oracle/spring/json/duality/model/employee/Employee.java create mode 100644 database/starters/oracle-spring-boot-json-relational-duality-views/src/test/java/com/oracle/spring/json/duality/model/employee/ManagerAdapter.java create mode 100644 database/starters/oracle-spring-boot-json-relational-duality-views/src/test/java/com/oracle/spring/json/duality/model/employee/ReportsAdapter.java create mode 100644 database/starters/oracle-spring-boot-json-relational-duality-views/src/test/java/com/oracle/spring/json/duality/model/employee/SimpleEmployee.java create mode 100644 database/starters/oracle-spring-boot-json-relational-duality-views/src/test/resources/views/employee-create.sql diff --git a/database/starters/oracle-spring-boot-json-relational-duality-views/src/main/java/com/oracle/spring/json/duality/annotation/JsonRelationalDualityView.java b/database/starters/oracle-spring-boot-json-relational-duality-views/src/main/java/com/oracle/spring/json/duality/annotation/JsonRelationalDualityView.java index a7227491..041343b6 100644 --- a/database/starters/oracle-spring-boot-json-relational-duality-views/src/main/java/com/oracle/spring/json/duality/annotation/JsonRelationalDualityView.java +++ b/database/starters/oracle-spring-boot-json-relational-duality-views/src/main/java/com/oracle/spring/json/duality/annotation/JsonRelationalDualityView.java @@ -15,5 +15,7 @@ public @interface JsonRelationalDualityView { String name() default ""; + boolean selfReferential() default false; + AccessMode accessMode() default @AccessMode(); } diff --git a/database/starters/oracle-spring-boot-json-relational-duality-views/src/main/java/com/oracle/spring/json/duality/builder/Annotations.java b/database/starters/oracle-spring-boot-json-relational-duality-views/src/main/java/com/oracle/spring/json/duality/builder/Annotations.java index 4de81a54..a3e6e03f 100644 --- a/database/starters/oracle-spring-boot-json-relational-duality-views/src/main/java/com/oracle/spring/json/duality/builder/Annotations.java +++ b/database/starters/oracle-spring-boot-json-relational-duality-views/src/main/java/com/oracle/spring/json/duality/builder/Annotations.java @@ -10,6 +10,7 @@ import jakarta.json.bind.annotation.JsonbProperty; import jakarta.json.bind.annotation.JsonbTransient; import jakarta.persistence.Column; +import jakarta.persistence.JoinColumn; import jakarta.persistence.JoinTable; import jakarta.persistence.ManyToMany; import jakarta.persistence.Table; @@ -92,7 +93,7 @@ static String getDatabaseColumnName(Field f) { return f.getName(); } - static String getAccessModeStr(AccessMode accessMode, ManyToMany manyToMany) { + static String getAccessModeStr(AccessMode accessMode, ManyToMany manyToMany, JoinColumn joinColumn) { StringBuilder sb = new StringBuilder(); if (manyToMany != null) { sb.append("@unnest "); @@ -109,6 +110,11 @@ static String getAccessModeStr(AccessMode accessMode, ManyToMany manyToMany) { } } + // Add join from if join column present + if (joinColumn != null && StringUtils.hasText(joinColumn.name())) { + sb.append("@link (from : [%s]) ".formatted(joinColumn.name())); + } + return sb.toString(); } } diff --git a/database/starters/oracle-spring-boot-json-relational-duality-views/src/main/java/com/oracle/spring/json/duality/builder/DualityViewBuilder.java b/database/starters/oracle-spring-boot-json-relational-duality-views/src/main/java/com/oracle/spring/json/duality/builder/DualityViewBuilder.java index 07e90838..5d4e8fb3 100644 --- a/database/starters/oracle-spring-boot-json-relational-duality-views/src/main/java/com/oracle/spring/json/duality/builder/DualityViewBuilder.java +++ b/database/starters/oracle-spring-boot-json-relational-duality-views/src/main/java/com/oracle/spring/json/duality/builder/DualityViewBuilder.java @@ -75,13 +75,14 @@ public String build(Class javaType) { ); } String viewName = getViewName(javaType, dvAnnotation); - String accessMode = getAccessModeStr(dvAnnotation.accessMode(), null); + String accessMode = getAccessModeStr(dvAnnotation.accessMode(), null, null); ViewEntity ve = new ViewEntity(javaType, new StringBuilder(), rootSnippet, accessMode, viewName, - 0); + 0, + false); String ddl = ve.build().toString(); dualityViews.put(viewName, ddl); return ddl; diff --git a/database/starters/oracle-spring-boot-json-relational-duality-views/src/main/java/com/oracle/spring/json/duality/builder/ViewEntity.java b/database/starters/oracle-spring-boot-json-relational-duality-views/src/main/java/com/oracle/spring/json/duality/builder/ViewEntity.java index c97def1d..56a10604 100644 --- a/database/starters/oracle-spring-boot-json-relational-duality-views/src/main/java/com/oracle/spring/json/duality/builder/ViewEntity.java +++ b/database/starters/oracle-spring-boot-json-relational-duality-views/src/main/java/com/oracle/spring/json/duality/builder/ViewEntity.java @@ -6,12 +6,15 @@ import java.lang.reflect.Field; import java.lang.reflect.ParameterizedType; import java.lang.reflect.Type; +import java.util.ArrayList; import java.util.HashSet; +import java.util.List; import java.util.Set; import com.oracle.spring.json.duality.annotation.JsonRelationalDualityView; import jakarta.json.bind.annotation.JsonbProperty; import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; import jakarta.persistence.JoinTable; import jakarta.persistence.ManyToMany; import jakarta.persistence.Table; @@ -45,26 +48,30 @@ final class ViewEntity { private final String viewName; // Tracks number of spaces for key nesting (pretty print) private int nesting; + private final boolean manyToMany; - // Track parent types to prevent stacking of nested types - private final Set> parentTypes = new HashSet<>(); + // Track views to prevent stacking of nested types + private final Set views = new HashSet<>(); - ViewEntity(Class javaType, StringBuilder sb, String accessMode, String viewName, int nesting) { - this(javaType, sb, null, accessMode, viewName, nesting); + private final List nestedEntities = new ArrayList<>(); + + ViewEntity(Class javaType, StringBuilder sb, String accessMode, String viewName, int nesting, boolean manyToMany) { + this(javaType, sb, null, accessMode, viewName, nesting, manyToMany); } - ViewEntity(Class javaType, StringBuilder sb, RootSnippet rootSnippet, String accessMode, String viewName, int nesting) { + ViewEntity(Class javaType, StringBuilder sb, RootSnippet rootSnippet, String accessMode, String viewName, int nesting, boolean manyToMany) { this.javaType = javaType; this.sb = sb; this.rootSnippet = rootSnippet; this.accessMode = accessMode; this.viewName = viewName; this.nesting = nesting; - parentTypes.add(javaType); + this.manyToMany = manyToMany; + views.add(viewName); } - void addParentTypes(Set> parentTypes) { - this.parentTypes.addAll(parentTypes); + void addViews(Set views) { + this.views.addAll(views); } /** @@ -91,8 +98,16 @@ ViewEntity build() { parseField(f); } } + for (ViewEntity ve : nestedEntities) { + ve.addViews(views); + sb.append(ve.build()); + } // Close the entity after processing fields. addTrailer(rootSnippet == null); + if (manyToMany) { + // Add join table trailer if necessary + addTrailer(true, ARRAY_TERMINAL); + } return this; } @@ -160,21 +175,20 @@ private void parseRelationalEntity(Field f, JsonRelationalDualityView dvAnnotati )); } - // Prevent stack overflow of circular references. - if (parentTypes.contains(entityJavaType)) { - return; - } // Add join table if present. ManyToMany manyToMany = f.getAnnotation(ManyToMany.class); if (manyToMany != null) { parseManyToMany(manyToMany, dvAnnotation, f, entityJavaType); } // Add nested entity. - parseNestedEntity(entityJavaType, dvAnnotation, manyToMany); - // Additional trailer for join table if present. - if (manyToMany != null) { - addTrailer(true, ARRAY_TERMINAL); - } + JoinColumn joinColumn = f.getAnnotation(JoinColumn.class); + parseNestedEntity(entityJavaType, dvAnnotation, manyToMany, joinColumn); + } + + private boolean visit(String viewName) { + boolean visited = views.contains(viewName); + views.add(viewName); + return visited; } /** @@ -194,18 +208,25 @@ private Class getGenericFieldType(Field f) { return f.getType(); } - private void parseNestedEntity(Class entityJavaType, JsonRelationalDualityView dvAnnotation, ManyToMany manyToMany) { + private void parseNestedEntity(Class entityJavaType, + JsonRelationalDualityView dvAnnotation, + ManyToMany manyToMany, + JoinColumn joinColumn) { Table tableAnnotation = entityJavaType.getAnnotation(Table.class); String viewEntityName = getNestedViewName(entityJavaType, manyToMany == null ? dvAnnotation : null, tableAnnotation); - String accessMode = getAccessModeStr(dvAnnotation.accessMode(), manyToMany); + // Prevent infinite recursion + if (visit(viewEntityName)) { + return; + } + String accessMode = getAccessModeStr(dvAnnotation.accessMode(), manyToMany, joinColumn); ViewEntity ve = new ViewEntity(entityJavaType, new StringBuilder(), accessMode, viewEntityName, - nesting + nesting, + manyToMany != null ); - ve.addParentTypes(parentTypes); - sb.append(ve.build()); + nestedEntities.add(ve); } private void parseColumn(Field f) { @@ -218,8 +239,12 @@ private void parseManyToMany(ManyToMany manyToMany, JsonRelationalDualityView dv if (!StringUtils.hasText(propertyName)) { propertyName = getJsonbPropertyName(f); } + // Don't parse if we've already visited this entity. + if (visit(propertyName)) { + return; + } addProperty(propertyName, joinTable.name(), false); - sb.append(" ").append(getAccessModeStr(dvAnnotation.accessMode(), null)); + sb.append(" ").append(getAccessModeStr(dvAnnotation.accessMode(), null, null)); sb.append(BEGIN_ARRAY); incNesting(); } diff --git a/database/starters/oracle-spring-boot-json-relational-duality-views/src/test/java/com/oracle/spring/json/duality/SpringBootDualityTest.java b/database/starters/oracle-spring-boot-json-relational-duality-views/src/test/java/com/oracle/spring/json/duality/SpringBootDualityTest.java index ef890253..5fc1d9ce 100644 --- a/database/starters/oracle-spring-boot-json-relational-duality-views/src/test/java/com/oracle/spring/json/duality/SpringBootDualityTest.java +++ b/database/starters/oracle-spring-boot-json-relational-duality-views/src/test/java/com/oracle/spring/json/duality/SpringBootDualityTest.java @@ -17,6 +17,7 @@ import com.oracle.spring.json.duality.model.book.Book; import com.oracle.spring.json.duality.model.book.Loan; import com.oracle.spring.json.duality.model.book.Member; +import com.oracle.spring.json.duality.model.employee.Employee; import com.oracle.spring.json.duality.model.movie.Actor; import com.oracle.spring.json.duality.model.movie.Director; import com.oracle.spring.json.duality.model.movie.DirectorBio; @@ -157,4 +158,19 @@ void books() { assertThat(byId.get().getLoans()).hasSize(1); assertThat(byId.get().getLoans().get(0).getBook().getTitle()).isEqualTo("my book"); } + + @Test + void employees() { + Employee manager = new Employee(); + manager.setName("manager"); + + Employee report = new Employee(); + report.setName("report"); + manager.setReports(List.of(report)); + dvClient.save(manager, Employee.class); + + Optional byId = dvClient.findById(Employee.class, 1); + assertThat(byId.isPresent()).isTrue(); + assertThat(byId.get().getReports()).hasSize(1); + } } diff --git a/database/starters/oracle-spring-boot-json-relational-duality-views/src/test/java/com/oracle/spring/json/duality/builder/DualityViewBuilderTest.java b/database/starters/oracle-spring-boot-json-relational-duality-views/src/test/java/com/oracle/spring/json/duality/builder/DualityViewBuilderTest.java index cf4bda7d..c477ec21 100644 --- a/database/starters/oracle-spring-boot-json-relational-duality-views/src/test/java/com/oracle/spring/json/duality/builder/DualityViewBuilderTest.java +++ b/database/starters/oracle-spring-boot-json-relational-duality-views/src/test/java/com/oracle/spring/json/duality/builder/DualityViewBuilderTest.java @@ -6,6 +6,7 @@ import java.util.stream.Stream; import com.oracle.spring.json.duality.model.book.Member; +import com.oracle.spring.json.duality.model.employee.Employee; import com.oracle.spring.json.duality.model.movie.Actor; import com.oracle.spring.json.duality.model.products.Order; import com.oracle.spring.json.duality.model.student.Student; @@ -26,7 +27,8 @@ public class DualityViewBuilderTest { Arguments.of(Student.class, "student-create.sql", "create"), Arguments.of(Actor.class, "actor-create.sql", "create"), Arguments.of(Order.class, "order-create.sql", "create"), - Arguments.of(Member.class, "member-create-drop.sql", "create-drop") + Arguments.of(Member.class, "member-create-drop.sql", "create-drop"), + Arguments.of(Employee.class, "employee-create.sql", "create") ); } diff --git a/database/starters/oracle-spring-boot-json-relational-duality-views/src/test/java/com/oracle/spring/json/duality/model/employee/Employee.java b/database/starters/oracle-spring-boot-json-relational-duality-views/src/test/java/com/oracle/spring/json/duality/model/employee/Employee.java new file mode 100644 index 00000000..421b23d8 --- /dev/null +++ b/database/starters/oracle-spring-boot-json-relational-duality-views/src/test/java/com/oracle/spring/json/duality/model/employee/Employee.java @@ -0,0 +1,70 @@ +// Copyright (c) 2025, Oracle and/or its affiliates. +// Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl. +package com.oracle.spring.json.duality.model.employee; + +import java.util.List; +import java.util.Objects; + +import com.oracle.spring.json.duality.annotation.AccessMode; +import com.oracle.spring.json.duality.annotation.JsonRelationalDualityView; +import jakarta.json.bind.annotation.JsonbProperty; +import jakarta.json.bind.annotation.JsonbTypeAdapter; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.OneToMany; +import jakarta.persistence.Table; +import lombok.Getter; +import lombok.Setter; + +import static com.oracle.spring.json.duality.builder.Annotations._ID_FIELD; + +@Entity +@Table(name = "employee") +@JsonRelationalDualityView(name = "employee_dv", accessMode = @AccessMode( + insert = true, + update = true, + delete = true +)) +@Getter +@Setter +public class Employee { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @JsonbProperty(_ID_FIELD) + private Long id; + + private String name; + + @ManyToOne + @JoinColumn(name = "manager_id", referencedColumnName = "id") + @JsonRelationalDualityView(name = "manager", accessMode = @AccessMode( + insert = true, + update = true + )) + @JsonbTypeAdapter(ManagerAdapter.class) + private Employee manager; + + @OneToMany(mappedBy = "manager") + @JsonRelationalDualityView(name = "reports", accessMode = @AccessMode( + insert = true, + update = true + )) + @JsonbTypeAdapter(ReportsAdapter.class) + private List reports; + + @Override + public final boolean equals(Object o) { + if (!(o instanceof Employee employee)) return false; + + return Objects.equals(getId(), employee.getId()); + } + + @Override + public int hashCode() { + return Objects.hashCode(getId()); + } +} diff --git a/database/starters/oracle-spring-boot-json-relational-duality-views/src/test/java/com/oracle/spring/json/duality/model/employee/ManagerAdapter.java b/database/starters/oracle-spring-boot-json-relational-duality-views/src/test/java/com/oracle/spring/json/duality/model/employee/ManagerAdapter.java new file mode 100644 index 00000000..b7996e6d --- /dev/null +++ b/database/starters/oracle-spring-boot-json-relational-duality-views/src/test/java/com/oracle/spring/json/duality/model/employee/ManagerAdapter.java @@ -0,0 +1,23 @@ +// Copyright (c) 2025, Oracle and/or its affiliates. +// Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl. +package com.oracle.spring.json.duality.model.employee; + +import jakarta.json.bind.adapter.JsonbAdapter; + +public class ManagerAdapter implements JsonbAdapter { + @Override + public SimpleEmployee adaptToJson(Employee employee) throws Exception { + SimpleEmployee simpleEmployee = new SimpleEmployee(); + simpleEmployee.set_id(employee.getId()); + simpleEmployee.setName(employee.getName()); + return simpleEmployee; + } + + @Override + public Employee adaptFromJson(SimpleEmployee simpleEmployee) throws Exception { + Employee employee = new Employee(); + employee.setId(simpleEmployee.get_id()); + employee.setName(simpleEmployee.getName()); + return employee; + } +} diff --git a/database/starters/oracle-spring-boot-json-relational-duality-views/src/test/java/com/oracle/spring/json/duality/model/employee/ReportsAdapter.java b/database/starters/oracle-spring-boot-json-relational-duality-views/src/test/java/com/oracle/spring/json/duality/model/employee/ReportsAdapter.java new file mode 100644 index 00000000..508e2243 --- /dev/null +++ b/database/starters/oracle-spring-boot-json-relational-duality-views/src/test/java/com/oracle/spring/json/duality/model/employee/ReportsAdapter.java @@ -0,0 +1,30 @@ +// Copyright (c) 2025, Oracle and/or its affiliates. +// Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl. +package com.oracle.spring.json.duality.model.employee; + +import java.util.List; + +import jakarta.json.bind.adapter.JsonbAdapter; + +public class ReportsAdapter implements JsonbAdapter, List> { + + @Override + public List adaptToJson(List employees) throws Exception { + return employees.stream().map(e -> { + SimpleEmployee simpleEmployee = new SimpleEmployee(); + simpleEmployee.set_id(e.getId()); + simpleEmployee.setName(e.getName()); + return simpleEmployee; + }).toList(); + } + + @Override + public List adaptFromJson(List simpleEmployees) throws Exception { + return simpleEmployees.stream().map(s -> { + Employee employee = new Employee(); + employee.setId(s.get_id()); + employee.setName(s.getName()); + return employee; + }).toList(); + } +} diff --git a/database/starters/oracle-spring-boot-json-relational-duality-views/src/test/java/com/oracle/spring/json/duality/model/employee/SimpleEmployee.java b/database/starters/oracle-spring-boot-json-relational-duality-views/src/test/java/com/oracle/spring/json/duality/model/employee/SimpleEmployee.java new file mode 100644 index 00000000..b9324593 --- /dev/null +++ b/database/starters/oracle-spring-boot-json-relational-duality-views/src/test/java/com/oracle/spring/json/duality/model/employee/SimpleEmployee.java @@ -0,0 +1,13 @@ +// Copyright (c) 2025, Oracle and/or its affiliates. +// Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl. +package com.oracle.spring.json.duality.model.employee; + +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +public class SimpleEmployee { + private Long _id; + private String name; +} diff --git a/database/starters/oracle-spring-boot-json-relational-duality-views/src/test/resources/views/actor-create.sql b/database/starters/oracle-spring-boot-json-relational-duality-views/src/test/resources/views/actor-create.sql index 3fbd8560..34241c5d 100644 --- a/database/starters/oracle-spring-boot-json-relational-duality-views/src/test/resources/views/actor-create.sql +++ b/database/starters/oracle-spring-boot-json-relational-duality-views/src/test/resources/views/actor-create.sql @@ -10,4 +10,4 @@ create force editionable json relational duality view actor_dv as actor @insert genre } } ] -} \ No newline at end of file + } \ No newline at end of file diff --git a/database/starters/oracle-spring-boot-json-relational-duality-views/src/test/resources/views/employee-create.sql b/database/starters/oracle-spring-boot-json-relational-duality-views/src/test/resources/views/employee-create.sql new file mode 100644 index 00000000..230e9240 --- /dev/null +++ b/database/starters/oracle-spring-boot-json-relational-duality-views/src/test/resources/views/employee-create.sql @@ -0,0 +1,12 @@ +create force editionable json relational duality view employee_dv as employee @insert @update @delete { + _id : id + name + manager : employee @insert @update @link (from : [manager_id]) { + _id : id + name + } + reports : employee @insert @update { + _id : id + name + } +} \ No newline at end of file diff --git a/database/starters/oracle-spring-boot-json-relational-duality-views/src/test/resources/views/member-create-drop.sql b/database/starters/oracle-spring-boot-json-relational-duality-views/src/test/resources/views/member-create-drop.sql index 76b86e75..59de6297 100644 --- a/database/starters/oracle-spring-boot-json-relational-duality-views/src/test/resources/views/member-create-drop.sql +++ b/database/starters/oracle-spring-boot-json-relational-duality-views/src/test/resources/views/member-create-drop.sql @@ -3,7 +3,7 @@ create force editionable json relational duality view members_dv as members @ins fullName : name loans @insert @update { _id : loan_id - book : books @insert @update { + book : books @insert @update @link (from : [book_id]) { _id : book_id title } diff --git a/database/starters/oracle-spring-boot-json-relational-duality-views/src/test/resources/views/order-create.sql b/database/starters/oracle-spring-boot-json-relational-duality-views/src/test/resources/views/order-create.sql index 684057cc..95d0397f 100644 --- a/database/starters/oracle-spring-boot-json-relational-duality-views/src/test/resources/views/order-create.sql +++ b/database/starters/oracle-spring-boot-json-relational-duality-views/src/test/resources/views/order-create.sql @@ -1,10 +1,10 @@ create force editionable json relational duality view orders_dv as orders @insert { _id : order_id + quantity + orderDate : order_date product : products { _id : product_id name price } - quantity - orderDate : order_date } \ No newline at end of file From 27f4404169e21c5bc5bc001d27e2340dcbaad929 Mon Sep 17 00:00:00 2001 From: Anders Swanson Date: Thu, 15 May 2025 11:16:21 -0700 Subject: [PATCH 11/13] Cleanup Signed-off-by: Anders Swanson --- .../pom.xml | 2 +- .../json/duality/builder/JoinTableSerde.java | 25 ------------------- 2 files changed, 1 insertion(+), 26 deletions(-) delete mode 100644 database/starters/oracle-spring-boot-json-relational-duality-views/src/main/java/com/oracle/spring/json/duality/builder/JoinTableSerde.java diff --git a/database/starters/oracle-spring-boot-json-relational-duality-views/pom.xml b/database/starters/oracle-spring-boot-json-relational-duality-views/pom.xml index 3cf0fb6e..036a1a8d 100644 --- a/database/starters/oracle-spring-boot-json-relational-duality-views/pom.xml +++ b/database/starters/oracle-spring-boot-json-relational-duality-views/pom.xml @@ -1,5 +1,5 @@ - + >, JsonbDeserializer> { - @Override - public void serialize(Class aClass, JsonGenerator jsonGenerator, SerializationContext serializationContext) { - - } - - @Override - public Class deserialize(JsonParser jsonParser, DeserializationContext deserializationContext, Type type) { - return null; - } -} From 752f62c4cf6e1a5f85611d535d2cbf9166435f6a Mon Sep 17 00:00:00 2001 From: Anders Swanson Date: Thu, 15 May 2025 11:39:00 -0700 Subject: [PATCH 12/13] Disable test Signed-off-by: Anders Swanson --- .../com/oracle/spring/json/duality/SpringBootDualityTest.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/database/starters/oracle-spring-boot-json-relational-duality-views/src/test/java/com/oracle/spring/json/duality/SpringBootDualityTest.java b/database/starters/oracle-spring-boot-json-relational-duality-views/src/test/java/com/oracle/spring/json/duality/SpringBootDualityTest.java index 5fc1d9ce..41c7265c 100644 --- a/database/starters/oracle-spring-boot-json-relational-duality-views/src/test/java/com/oracle/spring/json/duality/SpringBootDualityTest.java +++ b/database/starters/oracle-spring-boot-json-relational-duality-views/src/test/java/com/oracle/spring/json/duality/SpringBootDualityTest.java @@ -25,6 +25,7 @@ import com.oracle.spring.json.duality.model.products.Order; import com.oracle.spring.json.duality.model.products.Product; import com.oracle.spring.json.duality.model.student.Student; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; @@ -38,6 +39,7 @@ @SpringBootTest @Testcontainers +@Disabled public class SpringBootDualityTest { public static String readViewFile(String fileName) { try { From d09d383324625550e2f40f48886a59f3c063e3df Mon Sep 17 00:00:00 2001 From: Anders Swanson Date: Mon, 19 May 2025 08:25:36 -0700 Subject: [PATCH 13/13] Copyrights Signed-off-by: Anders Swanson --- database/starters/oracle-spring-boot-json-data-tools/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/database/starters/oracle-spring-boot-json-data-tools/pom.xml b/database/starters/oracle-spring-boot-json-data-tools/pom.xml index f2348536..14702fe5 100644 --- a/database/starters/oracle-spring-boot-json-data-tools/pom.xml +++ b/database/starters/oracle-spring-boot-json-data-tools/pom.xml @@ -1,5 +1,5 @@ - +