Skip to content

Commit 8bc099a

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

File tree

10 files changed

+251
-30
lines changed

10 files changed

+251
-30
lines changed

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

Lines changed: 40 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -27,17 +27,23 @@
2727
import static com.oracle.spring.json.duality.builder.Annotations.isFieldIncluded;
2828

2929
final class ViewEntity {
30+
// Separates JSON keys from database column names
3031
private static final String SEPARATOR = " : ";
31-
private static final String END_ENTITY = "}";
32-
private static final String END_ARRAY_ENTITY = "} ]";
33-
private static final String BEGIN_ARRAY_ENTITY = "[ {\n";
32+
// Terminal for view entity
33+
private static final String OBJECT_TERMINAL = "}";
34+
// Terminal for view array entity
35+
private static final String ARRAY_TERMINAL = "} ]";
36+
// Begin array entity
37+
private static final String BEGIN_ARRAY = "[ {\n";
38+
// Nesting spacing
3439
private static final int TAB_WIDTH = 2;
3540

3641
private final Class<?> javaType;
3742
private final StringBuilder sb;
3843
private final RootSnippet rootSnippet;
3944
private final String accessMode;
4045
private final String viewName;
46+
// Tracks number of spaces for key nesting (pretty print)
4147
private int nesting;
4248

4349
// Track parent types to prevent stacking of nested types
@@ -61,27 +67,41 @@ void addParentTypes(Set<Class<?>> parentTypes) {
6167
this.parentTypes.addAll(parentTypes);
6268
}
6369

70+
/**
71+
* Parse view from javaType.
72+
* @return this
73+
*/
6474
ViewEntity build() {
6575
Table tableAnnotation = javaType.getAnnotation(Table.class);
6676

6777
if (rootSnippet != null) {
68-
// Root duality view statement
78+
// Add create view snippet
6979
sb.append(getStatementPrefix(tableAnnotation));
7080
} else {
81+
// Process nested entity
7182
sb.append(getPadding());
7283
sb.append(getNestedEntityPrefix(tableAnnotation));
7384
}
7485

86+
// Increment the nesting (left padding) after processing an entity.
7587
incNesting();
88+
// Parse each field of the javaType.
7689
for (Field f : javaType.getDeclaredFields()) {
7790
if (isFieldIncluded(f)) {
7891
parseField(f);
7992
}
8093
}
94+
// Close the entity after processing fields.
8195
addTrailer(rootSnippet == null);
8296
return this;
8397
}
8498

99+
/**
100+
* Parse the javaType and tableAnnotation to generate the view prefix, e.g.,
101+
* 'create force editionable json relational duality view my_view as my table @insert @update @delete {}
102+
* @param tableAnnotation of the javaType.
103+
* @return view prefix String.
104+
*/
85105
private String getStatementPrefix(Table tableAnnotation) {
86106
String tableName = getTableName(javaType, tableAnnotation);
87107
return "%s %s as %s %s{\n".formatted(
@@ -103,14 +123,21 @@ private void parseField(Field f) {
103123
JsonRelationalDualityView dvAnnotation;
104124
Id id = f.getAnnotation(Id.class);
105125
if (id != null && rootSnippet != null) {
126+
// Parse the root entity's _id field.
106127
parseId(f);
107128
} else if ((dvAnnotation = f.getAnnotation(JsonRelationalDualityView.class)) != null) {
129+
// Parse the related sub-entity.
108130
parseRelationalEntity(f, dvAnnotation);
109131
} else {
132+
// Parse the field as a database column.
110133
parseColumn(f);
111134
}
112135
}
113136

137+
/**
138+
* Parse the view's root _id field.
139+
* @param f The view's root _id field.
140+
*/
114141
private void parseId(Field f) {
115142
String jsonbPropertyName = getJsonbPropertyName(f);
116143
if (!jsonbPropertyName.equals(_ID_FIELD)) {
@@ -121,6 +148,7 @@ private void parseId(Field f) {
121148
_ID_FIELD
122149
));
123150
}
151+
// Add the root _id field to the view.
124152
addProperty(_ID_FIELD, getDatabaseColumnName(f));
125153
}
126154

@@ -145,10 +173,15 @@ private void parseRelationalEntity(Field f, JsonRelationalDualityView dvAnnotati
145173
parseNestedEntity(entityJavaType, dvAnnotation, manyToMany);
146174
// Additional trailer for join table if present.
147175
if (manyToMany != null) {
148-
addTrailer(true, END_ARRAY_ENTITY);
176+
addTrailer(true, ARRAY_TERMINAL);
149177
}
150178
}
151179

180+
/**
181+
* Returns the type of f, or parameterized type of f.
182+
* @param f to introspect for type information.
183+
* @return type of f or parameterized type of f.
184+
*/
152185
private Class<?> getGenericFieldType(Field f) {
153186
Type genericType = f.getGenericType();
154187
if (genericType instanceof ParameterizedType p) {
@@ -187,7 +220,7 @@ private void parseManyToMany(ManyToMany manyToMany, JsonRelationalDualityView dv
187220
}
188221
addProperty(propertyName, joinTable.name(), false);
189222
sb.append(" ").append(getAccessModeStr(dvAnnotation.accessMode(), null));
190-
sb.append(BEGIN_ARRAY_ENTITY);
223+
sb.append(BEGIN_ARRAY);
191224
incNesting();
192225
}
193226

@@ -210,7 +243,7 @@ private void addProperty(String jsonbPropertyName, String databaseColumnName) {
210243
}
211244

212245
private void addTrailer(boolean addNewLine) {
213-
addTrailer(addNewLine, END_ENTITY);
246+
addTrailer(addNewLine, OBJECT_TERMINAL);
214247
}
215248

216249
private void addTrailer(boolean addNewLine, String terminal) {

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

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,15 @@
88
import java.nio.file.Files;
99
import java.nio.file.Path;
1010
import java.time.Duration;
11-
import java.util.Date;
11+
import java.util.List;
1212
import java.util.Optional;
1313
import java.util.Set;
1414
import java.util.UUID;
1515

1616
import com.oracle.spring.json.duality.builder.DualityViewScanner;
17+
import com.oracle.spring.json.duality.model.book.Book;
18+
import com.oracle.spring.json.duality.model.book.Loan;
19+
import com.oracle.spring.json.duality.model.book.Member;
1720
import com.oracle.spring.json.duality.model.movie.Actor;
1821
import com.oracle.spring.json.duality.model.movie.Director;
1922
import com.oracle.spring.json.duality.model.movie.DirectorBio;
@@ -131,4 +134,27 @@ void orders() {
131134
Optional<Order> OrderById = dvClient.findById(Order.class, 1);
132135
assertThat(OrderById.isPresent()).isTrue();
133136
}
137+
138+
@Test
139+
void books() {
140+
Book book = new Book();
141+
book.setTitle("my book");
142+
143+
dvClient.save(book, Book.class);
144+
145+
Loan loan = new Loan();
146+
loan.setBook(book);
147+
148+
Member member = new Member();
149+
member.setFullName("member");
150+
member.setLoans(List.of(loan));
151+
152+
dvClient.save(member, Member.class);
153+
154+
Optional<Member> byId = dvClient.findById(Member.class, 1);
155+
assertThat(byId.isPresent()).isTrue();
156+
assertThat(byId.get().getFullName()).isEqualTo("member");
157+
assertThat(byId.get().getLoans()).hasSize(1);
158+
assertThat(byId.get().getLoans().get(0).getBook().getTitle()).isEqualTo("my book");
159+
}
134160
}

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,8 @@
55

66
import java.util.stream.Stream;
77

8+
import com.oracle.spring.json.duality.model.book.Member;
89
import com.oracle.spring.json.duality.model.movie.Actor;
9-
import com.oracle.spring.json.duality.model.movie.Director;
10-
import com.oracle.spring.json.duality.model.movie.Movie;
1110
import com.oracle.spring.json.duality.model.products.Order;
1211
import com.oracle.spring.json.duality.model.student.Student;
1312
import org.jetbrains.annotations.NotNull;
@@ -26,7 +25,8 @@ public class DualityViewBuilderTest {
2625
Arguments.of(Student.class, "student-update.sql", "update"),
2726
Arguments.of(Student.class, "student-create.sql", "create"),
2827
Arguments.of(Actor.class, "actor-create.sql", "create"),
29-
Arguments.of(Order.class, "order-create.sql", "create")
28+
Arguments.of(Order.class, "order-create.sql", "create"),
29+
Arguments.of(Member.class, "member-create-drop.sql", "create-drop")
3030
);
3131
}
3232

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
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.book;
4+
5+
import java.util.Objects;
6+
7+
import com.oracle.spring.json.duality.annotation.AccessMode;
8+
import com.oracle.spring.json.duality.annotation.JsonRelationalDualityView;
9+
import jakarta.json.bind.annotation.JsonbProperty;
10+
import jakarta.persistence.Column;
11+
import jakarta.persistence.Entity;
12+
import jakarta.persistence.GeneratedValue;
13+
import jakarta.persistence.GenerationType;
14+
import jakarta.persistence.Id;
15+
import jakarta.persistence.Table;
16+
import lombok.Getter;
17+
import lombok.Setter;
18+
19+
import static com.oracle.spring.json.duality.builder.Annotations._ID_FIELD;
20+
21+
@Entity
22+
@Table(name = "books")
23+
@JsonRelationalDualityView(name = "book_dv", accessMode = @AccessMode(
24+
insert = true,
25+
update = true
26+
))
27+
@Getter
28+
@Setter
29+
public class Book {
30+
31+
@Id
32+
@JsonbProperty(_ID_FIELD)
33+
@GeneratedValue(strategy = GenerationType.IDENTITY)
34+
@Column(name = "book_id")
35+
private Long bookId;
36+
37+
@Column(nullable = false)
38+
private String title;
39+
40+
@Override
41+
public final boolean equals(Object o) {
42+
if (!(o instanceof Book book)) return false;
43+
44+
return Objects.equals(bookId, book.bookId) && Objects.equals(title, book.title);
45+
}
46+
47+
@Override
48+
public int hashCode() {
49+
int result = Objects.hashCode(bookId);
50+
result = 31 * result + Objects.hashCode(title);
51+
return result;
52+
}
53+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
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.book;
4+
5+
import java.util.Objects;
6+
7+
import com.oracle.spring.json.duality.annotation.AccessMode;
8+
import com.oracle.spring.json.duality.annotation.JsonRelationalDualityView;
9+
import jakarta.json.bind.annotation.JsonbProperty;
10+
import jakarta.json.bind.annotation.JsonbTransient;
11+
import jakarta.persistence.Column;
12+
import jakarta.persistence.Entity;
13+
import jakarta.persistence.FetchType;
14+
import jakarta.persistence.GeneratedValue;
15+
import jakarta.persistence.GenerationType;
16+
import jakarta.persistence.Id;
17+
import jakarta.persistence.JoinColumn;
18+
import jakarta.persistence.ManyToOne;
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 = "loans")
27+
@Getter
28+
@Setter
29+
@JsonRelationalDualityView(name = "loan_dv", accessMode = @AccessMode(
30+
insert = true,
31+
update = true
32+
))
33+
public class Loan {
34+
35+
@Id
36+
@JsonbProperty(_ID_FIELD)
37+
@GeneratedValue(strategy = GenerationType.IDENTITY)
38+
@Column(name = "loan_id")
39+
private Long loanId;
40+
41+
@ManyToOne(fetch = FetchType.LAZY)
42+
@JoinColumn(name = "member_id", nullable = false)
43+
@JsonbTransient
44+
private Member member;
45+
46+
@ManyToOne(fetch = FetchType.LAZY)
47+
@JoinColumn(name = "book_id", nullable = false)
48+
@JsonRelationalDualityView(name = "book", accessMode = @AccessMode(
49+
insert = true,
50+
update = true
51+
))
52+
private Book book;
53+
54+
@Override
55+
public final boolean equals(Object o) {
56+
if (!(o instanceof Loan loan)) return false;
57+
58+
return Objects.equals(getLoanId(), loan.getLoanId());
59+
}
60+
61+
@Override
62+
public int hashCode() {
63+
return Objects.hashCode(getLoanId());
64+
}
65+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
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.book;
4+
5+
import java.util.List;
6+
7+
import com.oracle.spring.json.duality.annotation.AccessMode;
8+
import com.oracle.spring.json.duality.annotation.JsonRelationalDualityView;
9+
import jakarta.json.bind.annotation.JsonbProperty;
10+
import jakarta.persistence.CascadeType;
11+
import jakarta.persistence.Column;
12+
import jakarta.persistence.Entity;
13+
import jakarta.persistence.GeneratedValue;
14+
import jakarta.persistence.GenerationType;
15+
import jakarta.persistence.Id;
16+
import jakarta.persistence.OneToMany;
17+
import jakarta.persistence.Table;
18+
import lombok.Getter;
19+
import lombok.Setter;
20+
21+
import static com.oracle.spring.json.duality.builder.Annotations._ID_FIELD;
22+
23+
@Entity
24+
@Table(name = "members")
25+
@JsonRelationalDualityView(accessMode = @AccessMode(
26+
insert = true,
27+
update = true,
28+
delete = true
29+
))
30+
@Getter
31+
@Setter
32+
public class Member {
33+
34+
@Id
35+
@GeneratedValue(strategy = GenerationType.IDENTITY)
36+
@JsonbProperty(_ID_FIELD)
37+
@Column(name = "member_id")
38+
private Long memberId;
39+
40+
@Column(name = "name", nullable = false)
41+
private String fullName;
42+
43+
@JsonRelationalDualityView(name = "loans", accessMode = @AccessMode(
44+
insert = true,
45+
update = true
46+
))
47+
@OneToMany(mappedBy = "member", cascade = CascadeType.ALL)
48+
private List<Loan> loans;
49+
}

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

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,11 @@
66
import java.util.Objects;
77
import java.util.Set;
88

9-
import com.oracle.spring.json.duality.annotation.JsonRelationalDualityView;
109
import jakarta.json.bind.annotation.JsonbProperty;
1110
import jakarta.json.bind.annotation.JsonbTransient;
1211
import jakarta.persistence.CascadeType;
1312
import jakarta.persistence.Column;
1413
import jakarta.persistence.Entity;
15-
import jakarta.persistence.GeneratedValue;
16-
import jakarta.persistence.GenerationType;
1714
import jakarta.persistence.Id;
1815
import jakarta.persistence.OneToMany;
1916
import jakarta.persistence.OneToOne;

0 commit comments

Comments
 (0)