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 product object (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.