Главное в любом 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.