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..14702fe5 --- /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 6afacdbc..5a2a9663 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 new file mode 100644 index 00000000..036a1a8d --- /dev/null +++ b/database/starters/oracle-spring-boot-json-relational-duality-views/pom.xml @@ -0,0 +1,98 @@ + + + + + 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-json-data-tools + ${project.version} + + + + org.springframework.boot + spring-boot-starter-data-jpa + + + + 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.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..00df54f9 --- /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,16 @@ +// 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; +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/annotation/JsonRelationalDualityView.java b/database/starters/oracle-spring-boot-json-relational-duality-views/src/main/java/com/oracle/spring/json/duality/annotation/JsonRelationalDualityView.java new file mode 100644 index 00000000..041343b6 --- /dev/null +++ b/database/starters/oracle-spring-boot-json-relational-duality-views/src/main/java/com/oracle/spring/json/duality/annotation/JsonRelationalDualityView.java @@ -0,0 +1,21 @@ +// 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, ElementType.FIELD}) +@Retention(RetentionPolicy.RUNTIME) +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/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 new file mode 100644 index 00000000..a3e6e03f --- /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,120 @@ +// 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 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.JoinColumn; +import jakarta.persistence.JoinTable; +import jakarta.persistence.ManyToMany; +import jakarta.persistence.Table; +import org.springframework.util.StringUtils; + +public final class Annotations { + public static final String _ID_FIELD = "_id"; + + 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 getNestedViewName(Class javaType, + JsonRelationalDualityView dvAnnotation, + Table tableAnnotation) { + if (dvAnnotation != null && StringUtils.hasText(dvAnnotation.name())) { + return dvAnnotation.name(); + } + return getTableName(javaType, tableAnnotation); + } + + 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())) { + 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 isFieldIncluded(Field f) { + return f.getAnnotation(JsonbTransient.class) == null; + } + + + 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, ManyToMany manyToMany, JoinColumn joinColumn) { + StringBuilder sb = new StringBuilder(); + if (manyToMany != null) { + sb.append("@unnest "); + } + if (accessMode != null) { + if (accessMode.insert()) { + sb.append("@insert "); + } + if (accessMode.update()) { + sb.append("@update "); + } + if (accessMode.delete()) { + sb.append("@delete "); + } + } + + // 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 new file mode 100644 index 00000000..5d4e8fb3 --- /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,136 @@ +// 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; +import java.sql.SQLException; +import java.sql.Statement; +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; +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 static final int TABLE_OR_VIEW_DOES_NOT_EXIST = 942; + + private final DataSource dataSource; + private final boolean isShowSql; + private final RootSnippet rootSnippet; + private final Map dualityViews = new HashMap<>(); + + public DualityViewBuilder(DataSource dataSource, + JpaProperties jpaProperties, + HibernateProperties hibernateProperties) { + this.dataSource = dataSource; + this.isShowSql = jpaProperties.isShowSql(); + this.rootSnippet = RootSnippet.fromDdlAuto( + hibernateProperties.getDdlAuto() + ); + } + + void apply() { + switch (rootSnippet) { + case NONE -> { + return; + } + case CREATE_DROP -> { + try { + createDrop(); + } catch (SQLException e) { + throw new RuntimeException(e); + } + } + } + + for (String ddl : dualityViews.values()) { + if (isShowSql) { + System.out.println(PREFIX + ddl); + } + if (rootSnippet.equals(RootSnippet.VALIDATE)) { + // TODO: Handle view validation. + return; + } + + runDDL(ddl); + } + } + + public 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(), null, null); + ViewEntity ve = new ViewEntity(javaType, + new StringBuilder(), + rootSnippet, + accessMode, + viewName, + 0, + false); + String ddl = ve.build().toString(); + dualityViews.put(viewName, ddl); + return 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); + } + } + + @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()) { + try (Connection conn = dataSource.getConnection(); + Statement stmt = conn.createStatement()) { + 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 new file mode 100644 index 00000000..2c2ac4cf --- /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,73 @@ +// 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.Map; +import java.util.Set; + +import com.oracle.spring.json.duality.annotation.JsonRelationalDualityView; +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 ApplicationContext applicationContext; + private final AnnotatedTypeScanner scanner; + + public DualityViewScanner(DualityViewBuilder dualityViewBuilder, + ApplicationContext applicationContext, + @Qualifier("jsonRelationalDualityViewScanner") AnnotatedTypeScanner scanner) { + this.dualityViewBuilder = dualityViewBuilder; + this.applicationContext = applicationContext; + this.scanner = scanner; + } + + @EventListener(ApplicationReadyEvent.class) + public void scan() { + 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()) { + addClass(javaType); + } + dualityViewBuilder.apply(); + } + + private void scanPackage(String packageName) { + Set> types = scanner.findTypes(packageName); + for (Class type : types) { + addClass(type); + } + } + + private void addClass(Class javaType) { + JsonRelationalDualityView dvAnnotation = javaType.getAnnotation(JsonRelationalDualityView.class); + if (dvAnnotation != null) { + 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 new file mode 100644 index 00000000..63f714ea --- /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,36 @@ +// 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 { + 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/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 new file mode 100644 index 00000000..56a10604 --- /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,301 @@ +// 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.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; +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.getNestedViewName; +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 = " : "; + // 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; + private final StringBuilder sb; + private final RootSnippet rootSnippet; + private final String accessMode; + private final String viewName; + // Tracks number of spaces for key nesting (pretty print) + private int nesting; + private final boolean manyToMany; + + // Track views to prevent stacking of nested types + private final Set views = new HashSet<>(); + + 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, boolean manyToMany) { + this.javaType = javaType; + this.sb = sb; + this.rootSnippet = rootSnippet; + this.accessMode = accessMode; + this.viewName = viewName; + this.nesting = nesting; + this.manyToMany = manyToMany; + views.add(viewName); + } + + void addViews(Set views) { + this.views.addAll(views); + } + + /** + * Parse view from javaType. + * @return this + */ + ViewEntity build() { + Table tableAnnotation = javaType.getAnnotation(Table.class); + + if (rootSnippet != null) { + // 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); + } + } + 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; + } + + /** + * 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( + rootSnippet.getSnippet(), viewName, tableName, accessMode + ); + } + + private String getNestedEntityPrefix(Table tableAnnotation) { + String tableName = getTableName(javaType, tableAnnotation); + if (tableName.equals(viewName)) { + return "%s %s{\n".formatted(tableName, accessMode); + } + return "%s%s%s %s{\n".formatted( + viewName, SEPARATOR, tableName, accessMode + ); + } + + 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)) { + 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 + )); + } + // Add the root _id field to the view. + addProperty(_ID_FIELD, getDatabaseColumnName(f)); + } + + 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(), JsonRelationalDualityView.class.getSimpleName() + )); + } + + // Add join table if present. + ManyToMany manyToMany = f.getAnnotation(ManyToMany.class); + if (manyToMany != null) { + parseManyToMany(manyToMany, dvAnnotation, f, entityJavaType); + } + // Add nested entity. + 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; + } + + /** + * 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) { + 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, + ManyToMany manyToMany, + JoinColumn joinColumn) { + Table tableAnnotation = entityJavaType.getAnnotation(Table.class); + String viewEntityName = getNestedViewName(entityJavaType, manyToMany == null ? dvAnnotation : null, tableAnnotation); + // Prevent infinite recursion + if (visit(viewEntityName)) { + return; + } + String accessMode = getAccessModeStr(dvAnnotation.accessMode(), manyToMany, joinColumn); + ViewEntity ve = new ViewEntity(entityJavaType, + new StringBuilder(), + accessMode, + viewEntityName, + nesting, + manyToMany != null + ); + nestedEntities.add(ve); + } + + private void parseColumn(Field f) { + addProperty(getJsonbPropertyName(f), getDatabaseColumnName(f)); + } + + private void parseManyToMany(ManyToMany manyToMany, JsonRelationalDualityView dvAnnotation, Field f, Class entityJavaType) { + JoinTable joinTable = getJoinTableAnnotation(f, manyToMany, entityJavaType); + String propertyName = dvAnnotation.name(); + 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, null)); + sb.append(BEGIN_ARRAY); + incNesting(); + } + + private void addProperty(String jsonbPropertyName, String databaseColumnName, boolean addNewLine) { + sb.append(getPadding()); + if (jsonbPropertyName.equals(databaseColumnName)) { + sb.append(jsonbPropertyName); + } else { + sb.append(jsonbPropertyName) + .append(SEPARATOR) + .append(databaseColumnName); + } + if (addNewLine) { + sb.append("\n"); + } + } + + private void addProperty(String jsonbPropertyName, String databaseColumnName) { + addProperty(jsonbPropertyName, databaseColumnName, true); + } + + private void addTrailer(boolean addNewLine) { + addTrailer(addNewLine, OBJECT_TERMINAL); + } + + private void addTrailer(boolean addNewLine, String terminal) { + decNesting(); + if (nesting > 0) { + sb.append(getPadding()); + } + sb.append(terminal); + if (addNewLine) { + sb.append("\n"); + } + } + + private String getPadding() { + return " ".repeat(nesting); + } + + 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/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..e000e3b8 --- /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,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; + +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; + +@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/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..f81c1ab1 --- /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,52 @@ +// 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.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; + + public JsonRelationalDualityClient(JdbcClient jdbcClient, JSONB jsonb) { + this.jdbcClient = jdbcClient; + this.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 (?) + """.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 = """ + select * from %s dv + where dv.data."_id" = ? + """.formatted(viewName); + + 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 new file mode 100644 index 00000000..41c7265c --- /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,178 @@ +// 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; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.time.Duration; +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.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; +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.Disabled; +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; + +import static org.assertj.core.api.Assertions.assertThat; + +@SpringBootTest +@Testcontainers +@Disabled +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. + */ + @Container + @ServiceConnection + static OracleContainer oracleContainer = new OracleContainer("gvenzl/oracle-free:23.7-slim-faststart") + .withStartupTimeout(Duration.ofMinutes(5)) + .withInitScript("products.sql") + .withUsername("testuser") + .withPassword("testpwd"); + + @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 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.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, actorId); + 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(); + } + + @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"); + } + + @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 new file mode 100644 index 00000000..c477ec21 --- /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,54 @@ +// 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.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; +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"), + Arguments.of(Order.class, "order-create.sql", "create"), + Arguments.of(Member.class, "member-create-drop.sql", "create-drop"), + Arguments.of(Employee.class, "employee-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/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/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/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..33ef62ec --- /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,67 @@ +// 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; +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.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(accessMode = @AccessMode(insert = true)) +public class Actor { + @JsonbProperty(_ID_FIELD) + @Id + @Column(name = "actor_id") + private String 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") + @JsonRelationalDualityView(name = "movies", accessMode = @AccessMode(insert = true)) + 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..9f00169b --- /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,73 @@ +// 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; +import java.util.Set; + +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.Id; +import jakarta.persistence.OneToMany; +import jakarta.persistence.OneToOne; +import jakarta.persistence.PrimaryKeyJoinColumn; +import jakarta.persistence.Table; +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 + @Column(name = "director_id") + private String directorId; + + @Column(name = "first_name", nullable = false, length = 50) + private String firstName; + + @Column(name = "last_name", nullable = false, length = 50) + private String lastName; + + @JsonbTransient + @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 + @JsonbTransient + //@JsonRelationalDualityView(name = "directorBio", accessMode = @AccessMode(insert = true)) + 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..38ac1757 --- /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,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.movie; + +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; +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; + +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 String directorId; + + @OneToOne(fetch = FetchType.LAZY) + // The primary key will be copied from the director entity + @MapsId + @JoinColumn(name = "director_id") + @JsonbTransient + 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..9148ec1a --- /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,72 @@ +// 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; +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; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +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 +@JsonRelationalDualityView(name = "movie_dv", accessMode = @AccessMode( + insert = true +)) +public class Movie { + @Id + @Column(name = "movie_id") + @JsonbProperty("_id") + private String 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") + //@JsonRelationalDualityView(accessMode = @AccessMode(insert = true)) + @JsonbTransient + private Director director; + + @ManyToMany + @JsonbTransient + @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/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..950082c2 --- /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,48 @@ +// 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.Getter; +import lombok.Setter; + +import java.util.Date; +import java.util.Objects; + +import static com.oracle.spring.json.duality.builder.Annotations._ID_FIELD; + +@Getter +@Setter +@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; + + @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 new file mode 100644 index 00000000..51d3b6e4 --- /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,46 @@ +// 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 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.Getter; +import lombok.Setter; + +import static com.oracle.spring.json.duality.builder.Annotations._ID_FIELD; + +@Getter +@Setter +@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; + + @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/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 new file mode 100644 index 00000000..20f235f6 --- /dev/null +++ b/database/starters/oracle-spring-boot-json-relational-duality-views/src/test/java/com/oracle/spring/json/duality/model/student/Student.java @@ -0,0 +1,48 @@ +// 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; +import com.oracle.spring.json.duality.annotation.AccessMode; +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.EqualsAndHashCode; +import lombok.Getter; +import lombok.Setter; + +import static com.oracle.spring.json.duality.builder.Annotations._ID_FIELD; + +@Entity +@Table(name = "STUDENT") +@JsonRelationalDualityView( + accessMode = @AccessMode( + insert = true, + update = true, + delete = true + ) +) +@EqualsAndHashCode +@Getter +@Setter +public class Student { + @JsonbProperty(_ID_FIELD) + @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() {} +} 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..64c69a27 --- /dev/null +++ b/database/starters/oracle-spring-boot-json-relational-duality-views/src/test/resources/application.yaml @@ -0,0 +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-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 new file mode 100644 index 00000000..ced55ae8 --- /dev/null +++ b/database/starters/oracle-spring-boot-json-relational-duality-views/src/test/resources/products.sql @@ -0,0 +1,15 @@ +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/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..34241c5d --- /dev/null +++ b/database/starters/oracle-spring-boot-json-relational-duality-views/src/test/resources/views/actor-create.sql @@ -0,0 +1,13 @@ +create force editionable json relational duality view actor_dv as actor @insert { + _id : actor_id + firstName : first_name + lastName : last_name + movies : movie_actor @insert [ { + movie @unnest @insert { + _id : movie_id + title + releaseYear : release_year + genre + } + } ] + } \ 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 new file mode 100644 index 00000000..59de6297 --- /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 @link (from : [book_id]) { + _id : book_id + title + } + } +} \ No newline at end of file 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..95d0397f --- /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 + quantity + orderDate : order_date + product : products { + _id : product_id + name + price + } +} \ 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 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 88d28d82..5022df9c 100644 --- a/database/starters/pom.xml +++ b/database/starters/pom.xml @@ -53,6 +53,8 @@ + 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