Skip to content

Commit 6f1f842

Browse files
Duality view builder
Signed-off-by: Anders Swanson <anders.swanson@oracle.com>
1 parent 8bc099a commit 6f1f842

File tree

14 files changed

+231
-31
lines changed

14 files changed

+231
-31
lines changed

database/starters/oracle-spring-boot-json-relational-duality-views/src/main/java/com/oracle/spring/json/duality/annotation/JsonRelationalDualityView.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,5 +15,7 @@
1515
public @interface JsonRelationalDualityView {
1616
String name() default "";
1717

18+
boolean selfReferential() default false;
19+
1820
AccessMode accessMode() default @AccessMode();
1921
}

database/starters/oracle-spring-boot-json-relational-duality-views/src/main/java/com/oracle/spring/json/duality/builder/Annotations.java

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
import jakarta.json.bind.annotation.JsonbProperty;
1111
import jakarta.json.bind.annotation.JsonbTransient;
1212
import jakarta.persistence.Column;
13+
import jakarta.persistence.JoinColumn;
1314
import jakarta.persistence.JoinTable;
1415
import jakarta.persistence.ManyToMany;
1516
import jakarta.persistence.Table;
@@ -92,7 +93,7 @@ static String getDatabaseColumnName(Field f) {
9293
return f.getName();
9394
}
9495

95-
static String getAccessModeStr(AccessMode accessMode, ManyToMany manyToMany) {
96+
static String getAccessModeStr(AccessMode accessMode, ManyToMany manyToMany, JoinColumn joinColumn) {
9697
StringBuilder sb = new StringBuilder();
9798
if (manyToMany != null) {
9899
sb.append("@unnest ");
@@ -109,6 +110,11 @@ static String getAccessModeStr(AccessMode accessMode, ManyToMany manyToMany) {
109110
}
110111
}
111112

113+
// Add join from if join column present
114+
if (joinColumn != null && StringUtils.hasText(joinColumn.name())) {
115+
sb.append("@link (from : [%s]) ".formatted(joinColumn.name()));
116+
}
117+
112118
return sb.toString();
113119
}
114120
}

database/starters/oracle-spring-boot-json-relational-duality-views/src/main/java/com/oracle/spring/json/duality/builder/DualityViewBuilder.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -75,13 +75,14 @@ public String build(Class<?> javaType) {
7575
);
7676
}
7777
String viewName = getViewName(javaType, dvAnnotation);
78-
String accessMode = getAccessModeStr(dvAnnotation.accessMode(), null);
78+
String accessMode = getAccessModeStr(dvAnnotation.accessMode(), null, null);
7979
ViewEntity ve = new ViewEntity(javaType,
8080
new StringBuilder(),
8181
rootSnippet,
8282
accessMode,
8383
viewName,
84-
0);
84+
0,
85+
false);
8586
String ddl = ve.build().toString();
8687
dualityViews.put(viewName, ddl);
8788
return ddl;

database/starters/oracle-spring-boot-json-relational-duality-views/src/main/java/com/oracle/spring/json/duality/builder/ViewEntity.java

Lines changed: 48 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,15 @@
66
import java.lang.reflect.Field;
77
import java.lang.reflect.ParameterizedType;
88
import java.lang.reflect.Type;
9+
import java.util.ArrayList;
910
import java.util.HashSet;
11+
import java.util.List;
1012
import java.util.Set;
1113

1214
import com.oracle.spring.json.duality.annotation.JsonRelationalDualityView;
1315
import jakarta.json.bind.annotation.JsonbProperty;
1416
import jakarta.persistence.Id;
17+
import jakarta.persistence.JoinColumn;
1518
import jakarta.persistence.JoinTable;
1619
import jakarta.persistence.ManyToMany;
1720
import jakarta.persistence.Table;
@@ -45,26 +48,30 @@ final class ViewEntity {
4548
private final String viewName;
4649
// Tracks number of spaces for key nesting (pretty print)
4750
private int nesting;
51+
private final boolean manyToMany;
4852

49-
// Track parent types to prevent stacking of nested types
50-
private final Set<Class<?>> parentTypes = new HashSet<>();
53+
// Track views to prevent stacking of nested types
54+
private final Set<String> views = new HashSet<>();
5155

52-
ViewEntity(Class<?> javaType, StringBuilder sb, String accessMode, String viewName, int nesting) {
53-
this(javaType, sb, null, accessMode, viewName, nesting);
56+
private final List<ViewEntity> nestedEntities = new ArrayList<>();
57+
58+
ViewEntity(Class<?> javaType, StringBuilder sb, String accessMode, String viewName, int nesting, boolean manyToMany) {
59+
this(javaType, sb, null, accessMode, viewName, nesting, manyToMany);
5460
}
5561

56-
ViewEntity(Class<?> javaType, StringBuilder sb, RootSnippet rootSnippet, String accessMode, String viewName, int nesting) {
62+
ViewEntity(Class<?> javaType, StringBuilder sb, RootSnippet rootSnippet, String accessMode, String viewName, int nesting, boolean manyToMany) {
5763
this.javaType = javaType;
5864
this.sb = sb;
5965
this.rootSnippet = rootSnippet;
6066
this.accessMode = accessMode;
6167
this.viewName = viewName;
6268
this.nesting = nesting;
63-
parentTypes.add(javaType);
69+
this.manyToMany = manyToMany;
70+
views.add(viewName);
6471
}
6572

66-
void addParentTypes(Set<Class<?>> parentTypes) {
67-
this.parentTypes.addAll(parentTypes);
73+
void addViews(Set<String> views) {
74+
this.views.addAll(views);
6875
}
6976

7077
/**
@@ -91,8 +98,16 @@ ViewEntity build() {
9198
parseField(f);
9299
}
93100
}
101+
for (ViewEntity ve : nestedEntities) {
102+
ve.addViews(views);
103+
sb.append(ve.build());
104+
}
94105
// Close the entity after processing fields.
95106
addTrailer(rootSnippet == null);
107+
if (manyToMany) {
108+
// Add join table trailer if necessary
109+
addTrailer(true, ARRAY_TERMINAL);
110+
}
96111
return this;
97112
}
98113

@@ -160,21 +175,20 @@ private void parseRelationalEntity(Field f, JsonRelationalDualityView dvAnnotati
160175
));
161176
}
162177

163-
// Prevent stack overflow of circular references.
164-
if (parentTypes.contains(entityJavaType)) {
165-
return;
166-
}
167178
// Add join table if present.
168179
ManyToMany manyToMany = f.getAnnotation(ManyToMany.class);
169180
if (manyToMany != null) {
170181
parseManyToMany(manyToMany, dvAnnotation, f, entityJavaType);
171182
}
172183
// Add nested entity.
173-
parseNestedEntity(entityJavaType, dvAnnotation, manyToMany);
174-
// Additional trailer for join table if present.
175-
if (manyToMany != null) {
176-
addTrailer(true, ARRAY_TERMINAL);
177-
}
184+
JoinColumn joinColumn = f.getAnnotation(JoinColumn.class);
185+
parseNestedEntity(entityJavaType, dvAnnotation, manyToMany, joinColumn);
186+
}
187+
188+
private boolean visit(String viewName) {
189+
boolean visited = views.contains(viewName);
190+
views.add(viewName);
191+
return visited;
178192
}
179193

180194
/**
@@ -194,18 +208,25 @@ private Class<?> getGenericFieldType(Field f) {
194208
return f.getType();
195209
}
196210

197-
private void parseNestedEntity(Class<?> entityJavaType, JsonRelationalDualityView dvAnnotation, ManyToMany manyToMany) {
211+
private void parseNestedEntity(Class<?> entityJavaType,
212+
JsonRelationalDualityView dvAnnotation,
213+
ManyToMany manyToMany,
214+
JoinColumn joinColumn) {
198215
Table tableAnnotation = entityJavaType.getAnnotation(Table.class);
199216
String viewEntityName = getNestedViewName(entityJavaType, manyToMany == null ? dvAnnotation : null, tableAnnotation);
200-
String accessMode = getAccessModeStr(dvAnnotation.accessMode(), manyToMany);
217+
// Prevent infinite recursion
218+
if (visit(viewEntityName)) {
219+
return;
220+
}
221+
String accessMode = getAccessModeStr(dvAnnotation.accessMode(), manyToMany, joinColumn);
201222
ViewEntity ve = new ViewEntity(entityJavaType,
202223
new StringBuilder(),
203224
accessMode,
204225
viewEntityName,
205-
nesting
226+
nesting,
227+
manyToMany != null
206228
);
207-
ve.addParentTypes(parentTypes);
208-
sb.append(ve.build());
229+
nestedEntities.add(ve);
209230
}
210231

211232
private void parseColumn(Field f) {
@@ -218,8 +239,12 @@ private void parseManyToMany(ManyToMany manyToMany, JsonRelationalDualityView dv
218239
if (!StringUtils.hasText(propertyName)) {
219240
propertyName = getJsonbPropertyName(f);
220241
}
242+
// Don't parse if we've already visited this entity.
243+
if (visit(propertyName)) {
244+
return;
245+
}
221246
addProperty(propertyName, joinTable.name(), false);
222-
sb.append(" ").append(getAccessModeStr(dvAnnotation.accessMode(), null));
247+
sb.append(" ").append(getAccessModeStr(dvAnnotation.accessMode(), null, null));
223248
sb.append(BEGIN_ARRAY);
224249
incNesting();
225250
}

database/starters/oracle-spring-boot-json-relational-duality-views/src/test/java/com/oracle/spring/json/duality/SpringBootDualityTest.java

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
import com.oracle.spring.json.duality.model.book.Book;
1818
import com.oracle.spring.json.duality.model.book.Loan;
1919
import com.oracle.spring.json.duality.model.book.Member;
20+
import com.oracle.spring.json.duality.model.employee.Employee;
2021
import com.oracle.spring.json.duality.model.movie.Actor;
2122
import com.oracle.spring.json.duality.model.movie.Director;
2223
import com.oracle.spring.json.duality.model.movie.DirectorBio;
@@ -157,4 +158,19 @@ void books() {
157158
assertThat(byId.get().getLoans()).hasSize(1);
158159
assertThat(byId.get().getLoans().get(0).getBook().getTitle()).isEqualTo("my book");
159160
}
161+
162+
@Test
163+
void employees() {
164+
Employee manager = new Employee();
165+
manager.setName("manager");
166+
167+
Employee report = new Employee();
168+
report.setName("report");
169+
manager.setReports(List.of(report));
170+
dvClient.save(manager, Employee.class);
171+
172+
Optional<Employee> byId = dvClient.findById(Employee.class, 1);
173+
assertThat(byId.isPresent()).isTrue();
174+
assertThat(byId.get().getReports()).hasSize(1);
175+
}
160176
}

database/starters/oracle-spring-boot-json-relational-duality-views/src/test/java/com/oracle/spring/json/duality/builder/DualityViewBuilderTest.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import java.util.stream.Stream;
77

88
import com.oracle.spring.json.duality.model.book.Member;
9+
import com.oracle.spring.json.duality.model.employee.Employee;
910
import com.oracle.spring.json.duality.model.movie.Actor;
1011
import com.oracle.spring.json.duality.model.products.Order;
1112
import com.oracle.spring.json.duality.model.student.Student;
@@ -26,7 +27,8 @@ public class DualityViewBuilderTest {
2627
Arguments.of(Student.class, "student-create.sql", "create"),
2728
Arguments.of(Actor.class, "actor-create.sql", "create"),
2829
Arguments.of(Order.class, "order-create.sql", "create"),
29-
Arguments.of(Member.class, "member-create-drop.sql", "create-drop")
30+
Arguments.of(Member.class, "member-create-drop.sql", "create-drop"),
31+
Arguments.of(Employee.class, "employee-create.sql", "create")
3032
);
3133
}
3234

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
// Copyright (c) 2025, Oracle and/or its affiliates.
2+
// Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl.
3+
package com.oracle.spring.json.duality.model.employee;
4+
5+
import java.util.List;
6+
import java.util.Objects;
7+
8+
import com.oracle.spring.json.duality.annotation.AccessMode;
9+
import com.oracle.spring.json.duality.annotation.JsonRelationalDualityView;
10+
import jakarta.json.bind.annotation.JsonbProperty;
11+
import jakarta.json.bind.annotation.JsonbTypeAdapter;
12+
import jakarta.persistence.Entity;
13+
import jakarta.persistence.GeneratedValue;
14+
import jakarta.persistence.GenerationType;
15+
import jakarta.persistence.Id;
16+
import jakarta.persistence.JoinColumn;
17+
import jakarta.persistence.ManyToOne;
18+
import jakarta.persistence.OneToMany;
19+
import jakarta.persistence.Table;
20+
import lombok.Getter;
21+
import lombok.Setter;
22+
23+
import static com.oracle.spring.json.duality.builder.Annotations._ID_FIELD;
24+
25+
@Entity
26+
@Table(name = "employee")
27+
@JsonRelationalDualityView(name = "employee_dv", accessMode = @AccessMode(
28+
insert = true,
29+
update = true,
30+
delete = true
31+
))
32+
@Getter
33+
@Setter
34+
public class Employee {
35+
@Id
36+
@GeneratedValue(strategy = GenerationType.IDENTITY)
37+
@JsonbProperty(_ID_FIELD)
38+
private Long id;
39+
40+
private String name;
41+
42+
@ManyToOne
43+
@JoinColumn(name = "manager_id", referencedColumnName = "id")
44+
@JsonRelationalDualityView(name = "manager", accessMode = @AccessMode(
45+
insert = true,
46+
update = true
47+
))
48+
@JsonbTypeAdapter(ManagerAdapter.class)
49+
private Employee manager;
50+
51+
@OneToMany(mappedBy = "manager")
52+
@JsonRelationalDualityView(name = "reports", accessMode = @AccessMode(
53+
insert = true,
54+
update = true
55+
))
56+
@JsonbTypeAdapter(ReportsAdapter.class)
57+
private List<Employee> reports;
58+
59+
@Override
60+
public final boolean equals(Object o) {
61+
if (!(o instanceof Employee employee)) return false;
62+
63+
return Objects.equals(getId(), employee.getId());
64+
}
65+
66+
@Override
67+
public int hashCode() {
68+
return Objects.hashCode(getId());
69+
}
70+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
// Copyright (c) 2025, Oracle and/or its affiliates.
2+
// Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl.
3+
package com.oracle.spring.json.duality.model.employee;
4+
5+
import jakarta.json.bind.adapter.JsonbAdapter;
6+
7+
public class ManagerAdapter implements JsonbAdapter<Employee, SimpleEmployee> {
8+
@Override
9+
public SimpleEmployee adaptToJson(Employee employee) throws Exception {
10+
SimpleEmployee simpleEmployee = new SimpleEmployee();
11+
simpleEmployee.set_id(employee.getId());
12+
simpleEmployee.setName(employee.getName());
13+
return simpleEmployee;
14+
}
15+
16+
@Override
17+
public Employee adaptFromJson(SimpleEmployee simpleEmployee) throws Exception {
18+
Employee employee = new Employee();
19+
employee.setId(simpleEmployee.get_id());
20+
employee.setName(simpleEmployee.getName());
21+
return employee;
22+
}
23+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
// Copyright (c) 2025, Oracle and/or its affiliates.
2+
// Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl.
3+
package com.oracle.spring.json.duality.model.employee;
4+
5+
import java.util.List;
6+
7+
import jakarta.json.bind.adapter.JsonbAdapter;
8+
9+
public class ReportsAdapter implements JsonbAdapter<List<Employee>, List<SimpleEmployee>> {
10+
11+
@Override
12+
public List<SimpleEmployee> adaptToJson(List<Employee> employees) throws Exception {
13+
return employees.stream().map(e -> {
14+
SimpleEmployee simpleEmployee = new SimpleEmployee();
15+
simpleEmployee.set_id(e.getId());
16+
simpleEmployee.setName(e.getName());
17+
return simpleEmployee;
18+
}).toList();
19+
}
20+
21+
@Override
22+
public List<Employee> adaptFromJson(List<SimpleEmployee> simpleEmployees) throws Exception {
23+
return simpleEmployees.stream().map(s -> {
24+
Employee employee = new Employee();
25+
employee.setId(s.get_id());
26+
employee.setName(s.getName());
27+
return employee;
28+
}).toList();
29+
}
30+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
// Copyright (c) 2025, Oracle and/or its affiliates.
2+
// Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl.
3+
package com.oracle.spring.json.duality.model.employee;
4+
5+
import lombok.Getter;
6+
import lombok.Setter;
7+
8+
@Getter
9+
@Setter
10+
public class SimpleEmployee {
11+
private Long _id;
12+
private String name;
13+
}

database/starters/oracle-spring-boot-json-relational-duality-views/src/test/resources/views/actor-create.sql

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,4 +10,4 @@ create force editionable json relational duality view actor_dv as actor @insert
1010
genre
1111
}
1212
} ]
13-
}
13+
}

0 commit comments

Comments
 (0)