JPA & Palmyra coexistence#
Palmyra reads the DB via JDBC metadata — it does not require JPA entities. But most real apps also need Spring Data JPA for ancillary queries (custom JPQL, typed findBy… methods, transactions spanning multiple tables). This page covers how the two layers share a database without stepping on each other.
The rules#
- Palmyra → JDBC.
@PalmyraType+@PalmyraFielddescribe a view over the physical table. No JPA dependency. - JPA → JPA.
@Entity+@Id+@ManyToOnedescribe the same table to Hibernate. No Palmyra dependency. - The table is shared. Both point at the same rows; neither tool is aware of the other’s annotations.
- Keep them out of each other’s way. A change on the Palmyra side never requires a JPA edit, and vice versa — unless the underlying column or FK constraint changes, which requires both.
When do I add a JPA entity?#
Add one for a given table only if you need at least one of:
- Custom repository methods —
Repository.findByLoginName(String),@Query("...")JPQL. - Cross-table transactions driven from Java code (not from a single CRUD handler).
- Ancillary flows — user-password read in
LocalDBAuthenticationProvider, TUS upload metadata write, background reconcilers.
For a plain CRUD entity that only shows up in a grid + a form — skip the JPA entity. Palmyra handles reads and writes through the handler + JDBC. Less code to maintain; no drift between two mappings.
What about relations?#
// JPA entity
@Entity
@Table(name = "mrci_exam")
public class ExamEntity {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "patient", nullable = false)
private PatientEntity patient; // JPA-side relation
}// Palmyra model — independent mapping over the same table
@PalmyraType(type = "MrciExam", table = "mrci_exam")
public class MrciExamModel {
@PalmyraField(primaryKey = true) private Long id;
@PalmyraField private MrcpPatientModel patient; // Palmyra-side nested object
}Two observations:
- The JPA
@ManyToOnehas no effect on Palmyra’s nested-object resolution. Palmyra reads the FK from the DB at startup. - Removing the
@ManyToOne(keeping just@Column private Long patient) has no effect on Palmyra either — but it breaksJpaRepository.findByPatient(PatientEntity).
If you don’t need JPA-side relations, don’t add them. The DB FK alone is enough.
The findBy… gotcha#
This is the one real trap. If you had:
public interface SeriesJpaRepo extends JpaRepository<SeriesEntity, Long> {
List<SeriesEntity> findByExam(Long examId); // works when SeriesEntity.exam is Long
}and you later change SeriesEntity.exam from Long to @ManyToOne ExamEntity exam (to support some JPQL join elsewhere), Hibernate rejects findByExam(Long) at bean-creation time:
Cannot compare left expression of type 'ExamEntity' with right expression of type 'Long'Fix — rename the method and tell Spring Data it’s filtering on the id field:
List<SeriesEntity> findByExamId(Long examId); // Spring Data translates "ExamId" to entity.exam.idThis is JPA-internal; Palmyra doesn’t care either way.
Coordinates for the “both” case#
When you do need both JPA and Palmyra on the same table:
- Keep the
@Entityminimal — just the columns the ancillary flow touches. - Keep the
@PalmyraTypemodel complete — every field the grid / form / response needs. - The column names agree, but neither knows about the other’s annotations.
- Schema migrations add / remove / rename columns in the DB. Both models update to track.
What JPA-side changes DON’T require Palmyra edits#
- Adding
@ManyToOneon an existing column. - Adding a
findByXrepository method. - Adding
@Transactionalon a service. - Renaming the entity class (Palmyra doesn’t know it exists).
What schema changes DO require edits on both sides#
- Renaming a column — update JPA
@Column(name = …)and Palmyra@PalmyraField(column = …)(or the field name if you use the convention-based binding). - Dropping a column — remove from both.
- Adding a new FK in the DB — Palmyra picks it up for free; JPA needs the new
@ManyToOneonly if you want it as a typed relation.
Heuristic#
Default: write a Palmyra model, skip the JPA entity. Add the JPA entity when you actually write a query that needs it.
Most handler logic — even moderately complex — fits within the Palmyra hooks (preCreate, applyQueryFilter, onUpdate, onQueryResult) without reaching for JPA. Reserve JPA for genuinely JPA-shaped work.
See also#
- Mental Model — “DB schema is source of truth” framing
- Schema Discovery — what Palmyra reads and when
@PalmyraTypeand@PalmyraField