2. Entities, models, POJOs#
The clinic sample uses a three-file separation — and the split is load-bearing:
| Kind | Package | Role |
|---|---|---|
| JPA entity | entity/ |
Persistence shape — JPA annotations, relationships, audit fields |
| Palmyra model | model/ |
Public API shape — @PalmyraType + @PalmyraField, what clients see |
| Plain POJO | pojo/ |
Value objects for custom JPQL / native queries — no annotations |
Keeping the Palmyra model separate from the JPA entity lets you flatten joined attributes, hide internal columns, and evolve the API contract without reshaping the database.
The MstManufacturer example#
JPA entity#
// entity/MstManufacturerEntity.java
@Entity
@Table(name = "mst_manufacturer")
@Getter @Setter
public class MstManufacturerEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
@Column(nullable = false) private String name;
@Column private String contactMobile;
@Column private String contactEmail;
@Column private Integer rating;
@Column(length = 512) private String address;
@CreatedDate @Column(updatable = false) private Instant createdAt;
@CreatedBy @Column(updatable = false) private String createdBy;
@LastModifiedDate private Instant updatedAt;
@LastModifiedBy private String updatedBy;
}Palmyra model#
// model/MstManufacturerModel.java
@Getter @Setter
@PalmyraType(type = "MstManufacturer")
public class MstManufacturerModel {
@PalmyraField private Integer id;
@PalmyraField(sort = true, search = true) private String name;
@PalmyraField private String contactMobile;
@PalmyraField private String contactEmail;
@PalmyraField private Integer rating;
@PalmyraField private String address;
}@PalmyraType(type = "MstManufacturer") binds the DTO to a Palmyra type name. Plain @PalmyraField on a scalar maps it to a same-named column (snake-cased by the framework).
Nested and flattened references#
Palmyra resolves joins declaratively. The clinic sample uses three patterns frequently:
// model/StockEntryModel.java
@Getter @Setter
@PalmyraType(type = "StockEntry")
public class StockEntryModel {
@PalmyraField private Integer id;
// 1. Nested Palmyra model → handled as a FK lookup
@PalmyraField(attribute = "product")
private MstProductModel product;
// 2. Flatten a single attribute from an immediate parent
@PalmyraField(parentRef = "purchaseRef", attribute = "purchaseDate")
private LocalDate purchaseDate;
@PalmyraField(parentRef = "purchaseRef", attribute = "invoiceNumber")
private String invoiceNumber;
// 3. Flatten across a two-hop chain (parent-of-parent)
@PalmyraField(parentRef = "purchaseRef.supplier", attribute = "name")
private String supplier;
@PalmyraField private LocalDate expiryDate;
@PalmyraField private String batchNumber;
@PalmyraField private Integer purchasedQuantity;
@PalmyraField private Integer currentQuantity;
@PalmyraField private String status;
}- Nested model — the client can post a partial
productobject (or just{product: {id: 42}}) and Palmyra resolves the FK. parentRef+attribute— the DTO exposes a scalar, but the SQL reads from a joined table. Callers get a flat JSON shape; the backend still joins.- Chained
parentRef— dot-separated traversal across multiple joins.
Flattening hops through attribute instead of parentRef#
PurchasePaymentLineItemModel in the clinic sample is the richest flattening example in the codebase. Here the dotted path lives on attribute, not on parentRef — every field declares parentRef = "purchase" and then reaches further into the tree through the attribute expression:
// model/PurchasePaymentLineItemModel.java
@Getter @Setter
@PalmyraType(type = "PurchasePaymentLineItem")
public class PurchasePaymentLineItemModel {
@PalmyraField private Integer id;
// Single-hop flattening — line-item → purchase → column
@PalmyraField(parentRef = "purchase", attribute = "id") private Integer purchaseId;
@PalmyraField(parentRef = "purchase", attribute = "purchaseDate") private LocalDate purchaseDate;
@PalmyraField(parentRef = "purchase", attribute = "invoiceNumber") private String invoiceNumber;
@PalmyraField(parentRef = "purchase", attribute = "totalAmount") private String totalAmount;
@PalmyraField(parentRef = "purchase", attribute = "discount") private Double discount;
@PalmyraField(parentRef = "purchase", attribute = "finalAmount") private Double finalAmount;
// Two-hop flattening — line-item → purchase → supplier → column
@PalmyraField(parentRef = "purchase", attribute = "supplier.name") private String vendor;
@PalmyraField(parentRef = "purchase", attribute = "paymentStatus.name") private String statusName;
@PalmyraField(parentRef = "purchase", attribute = "paymentStatus.code") private String statusCode;
}Two ways to spell “two hops”#
Both of these resolve stockEntry → purchaseRef → supplier → name, but they put the dot in different places:
// Path lives in parentRef
@PalmyraField(parentRef = "purchaseRef.supplier", attribute = "name")
private String supplier;// Same result — path lives in attribute
@PalmyraField(parentRef = "purchase", attribute = "supplier.name")
private String vendor;Pick whichever reads more naturally for the hop chain — Palmyra compiles them the same way. A shared convention inside a codebase is more valuable than either choice.
Plain POJOs for custom queries#
For custom JPQL / native queries (dashboard aggregates, reports), the sample uses un-annotated value objects in pojo/ and maps them via Spring Data’s constructor-projection:
// pojo/StockSummary.java
@Getter @AllArgsConstructor
public class StockSummary {
private final Integer productId;
private final String productName;
private final Long totalQuantity;
}These do not flow through Palmyra — handlers return them directly from custom controllers or NativeQueryHandler implementations.
See also: @PalmyraType, @PalmyraField.