Главное в любом ORM решении, это описать, как ваши классы (entity) отображаются (maps) на реляционные таблицы. В JPA это делается с помощью аннотаций.
Как я уже писал, перед тем, как начать использовать класс в качестве JPA сущности, надо убедиться, что он соответствует требованиям к сущностям:
- У класса должен быть конструктор без аргументов, имеющий уровень доступа public или protected. Допускается иметь и другие конструкторы.
- Класс не должен быть final. Равно не должны быть final его методы и сохраняемые переменные.
- Если объект Entity класса будет передаваться по значению как отдельный объект (detached object), например через удаленный интерфейс (through a remote interface), он так же должен реализовывать Serializable интерфейс.
- Классы сущностей могут быть унаследовано от других классов, которые могут быть jpa сущностями, а могут и не быть. Обратно и от jpa сущностей могут быть унаследованы обычные классы.
- Сохраняемые переменные не должны быть public и доступ к ним должен предоставляться только через вызовы методов класса.
JPA mapped entity
Поскольку java это, чаще всего, кровавый энтерпрайз, то примерчик будет соответствующий: журнал финансовых операций. Каждая операция имеет счёт, сумму, дату операции, номер транзакции и, опционально, описание и некий код.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 | /** * Single financial operation. */ @SuppressWarnings("PMD") @ToString @Entity @Table(name = "journal", indexes = {@Index( name = "j_account_idx", columnList = "account_id", unique = false)}, uniqueConstraints = {@UniqueConstraint( columnNames = {"id", "account_id"})}) @SecondaryTable(name = "operations_details", pkJoinColumns = @PrimaryKeyJoinColumn( name = "op_id", referencedColumnName = "id")) public class Operation { /** * Operation id. */ @Id @GeneratedValue @Getter @Setter @Column(name = "id", nullable = false, updatable = false) private Long rowId; /** * Related transaction id. * * Single transaction could have * more then one operations. */ @Getter @Setter @Column(name = "trxId", nullable = false, updatable = false) private Long id; /** * Operation's account. */ @Getter @Setter @Column(nullable = false, updatable = false) private Integer accountId; /** * Operation's amount. */ @Getter @Setter @Column(nullable = false, updatable = false, scale = 2, precision = 10) private BigDecimal amount; /** * Operation's timestamp. */ @Getter @Setter @Column(nullable = false, updatable = false) private ZonedDateTime timestamp; /** * Optional operation description. */ @Getter @Setter @Column(table = "operations_details", length = 64) private String description; /** * Optional operation code. */ @Getter @Setter @Column(table = "operations_details") private Integer opCode; } |
Рассмотрим подробно:
@Entity говорит JPA, что этот класс явно имеет отношение к базе данных и должен быть в ней сохранён и прочитан обратно. Эта аннотация является обязательной.1 2 3 4 5 6 | @Table(name = "journal", indexes = {@Index( name = "j_account_idx", columnList = "account_id", unique = false)}, uniqueConstraints = {@UniqueConstraint( columnNames = {"id", "account_id"})}) |
1 2 3 4 | @SecondaryTable(name = "operations_details", pkJoinColumns = @PrimaryKeyJoinColumn( name = "op_id", referencedColumnName = "id")) |
1 2 3 4 | @Id @GeneratedValue @Column(name = "id", nullable = false, updatable = false) private Long rowId; |
Аннотация @Column не является обязательной. По умолчанию все поля класса сохраняются в базе данных. Если поле не должно быть сохранено, оно должно быть проаннотированно аннотацией @Transient.
@Id и @GeneratedValue говорят, что это поле — первичный ключ и что его значения должны создаваться автоматически.1 2 3 4 5 6 7 8 9 10 11 | /** * Operation's amount. */ @Column(nullable = false, updatable = false, scale = 2, precision = 10) private BigDecimal amount; /** * Optional operation description. */ @Column(table = "operations_details", length = 64) private String description; |
1 2 3 4 5 | /** * Optional operation code. */ @Column(table = "operations_details") private Integer opCode; |
Использование
Создадим операцию, сохраним её в базу и прочитаем обратно:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | @Before public void setUp() throws Exception { Operation op = new Operation(); op.setId(1L); op.setAccountId(100500); op.setAmount(BigDecimal.TEN); op.setTimestamp(ZonedDateTime.now()); op.setDescription("Test operation"); op.setOpCode(9000); entityManagerFactory = Persistence.createEntityManagerFactory("ru.easyjava.data.jpa.hibernate"); EntityManager em = entityManagerFactory.createEntityManager(); em.getTransaction().begin(); em.persist(op); em.getTransaction().commit(); em.close(); } @Test public void testGreeter() { EntityManager em = entityManagerFactory.createEntityManager(); em.getTransaction().begin(); em.createQuery("from Operation", Operation.class) .getResultList() .forEach(System.out::println); em.getTransaction().commit(); em.close(); } |
1 2 3 4 5 6 7 8 9 | Operation( rowId=1, id=1, account_id=100500, amount=10.00, timestamp=2016-03-18T11:08:58.745+02:00[Europe/Helsinki], description=Test operation, op_code=9000 ) |
Обратите внимание, что несмотря на то, что таблица называется journal, запрос делается from Operation, то есть по имени сущности.
Если подключиться к базе H2 и посмотреть схему, увидим, что данные хранятся в двух таблицах, названных именно так, как написано в аннотациях:


Код примера доступен на github.