Главное в любом ORM решении, это описать, как ваши классы (entity) отображаются (maps) на реляционные таблицы. В JPA это делается с помощью аннотаций.
Как я уже писал, перед тем, как начать использовать класс в качестве JPA сущности, надо убедиться, что он соответствует требованиям к сущностям:
- У класса должен быть конструктор без аргументов, имеющий уровень доступа public или protected. Допускается иметь и другие конструкторы.
- Класс не должен быть final. Равно не должны быть final его методы и сохраняемые переменные.
- Если объект Entity класса будет передаваться по значению как отдельный объект (detached object), например через удаленный интерфейс (through a remote interface), он так же должен реализовывать Serializable интерфейс.
- Классы сущностей могут быть унаследовано от других классов, которые могут быть jpa сущностями, а могут и не быть. Обратно и от jpa сущностей могут быть унаследованы обычные классы.
- Сохраняемые переменные не должны быть public и доступ к ним должен предоставляться только через вызовы методов класса.
JPA mapped entity
Поскольку java это, чаще всего, кровавый энтерпрайз, то примерчик будет соответствующий: журнал финансовых операций. Каждая операция имеет счёт, сумму, дату операции, номер транзакции и, опционально, описание и некий код.
/** * 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, что этот класс явно имеет отношение к базе данных и должен быть в ней сохранён и прочитан обратно. Эта аннотация является обязательной.
@Table(name = "journal", indexes = {@Index( name = "j_account_idx", columnList = "account_id", unique = false)}, uniqueConstraints = {@UniqueConstraint( columnNames = {"id", "account_id"})})
@Table описывает главную таблицу, в которой должны быть сохранены данные класса. Поле name задаёт имя таблицы, если его опустить, имя таблицы будет совпадать с именем класса. Кроме имени можно дополнительно задать схему и каталог для размещения таблицы. indexes и @Index описывают индексы, которые должны быть созданы для таблицы. uniqueConstraints и @UniqueConstraint — ограничения уникальности значений полей или групп полей для таблицы. И indexes и uniqueConstraints используются только при создании таблицы средствами JPA. В случае, если таблицы создаётся каким-либо другим путём, эти опции будут проигнорированы. Аннотация @Table, равно как и все её опции, не является обязательной.
@SecondaryTable(name = "operations_details", pkJoinColumns = @PrimaryKeyJoinColumn( name = "op_id", referencedColumnName = "id"))
@SecondaryTable (и @SecondaryTables, если одной недостаточно) сообщают JPA, что класс должен сохраняться в нескольких таблицах. Указывать name в данном случае обязательно, так как это задаёт имя дополнительной таблицы. pkJoinColumns описывает связь между основной и дополнительной таблицами. Этот функционал полезен, если у вас есть единая сущность, часть полей которых используется одним способом, а часть другим. Например, в моём примере с финансовой операцией, я выделяю опциональные данные в отдельную таблицу, которая может храниться в отдельном tablespace, который более медленный, но более объёмный. Либо наоборот, когда у вас уже есть один объект, разбитый на несколько таблиц, в java он может быть автоматически представлен одним классом. Аннотация @SecondaryTable очевидно не является обязательной.
@Id @GeneratedValue @Column(name = "id", nullable = false, updatable = false) private Long rowId;
Аннотация @Column указывает JPA, как именно сохранять это поле в базу. name задаёт имя столбца, если его опустить, по умолчанию используется имя поля. updatable/insertable указывают можно либо значение поля изменять или вставлять при создании записи. nullable сообщает JPA, может ли поле быть null или нет. Значение nullable используется и при создании таблиц и при сохранении изменений. Так же можно задать опцию unique, которая добавляется к описанию uniqueContraints на уровне всего класса и делает конкретное поле уникальным. Так же, как и uniqueConstraints, unique используется только при создании таблиц.
Аннотация @Column не является обязательной. По умолчанию все поля класса сохраняются в базе данных. Если поле не должно быть сохранено, оно должно быть проаннотированно аннотацией @Transient.
@Id и @GeneratedValue говорят, что это поле — первичный ключ и что его значения должны создаваться автоматически.
/** * 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;
В аннотации @Column можно так же задать и размерности для полей. length для строковых значений задаёт размер соответствующего поля в базе данных, например CHAR(length) или VARCHAR(length). Для десятичных типов данных, таких как BigDecimal, ипользуются опции scale и precision. scale=2, precision=10 задают десятичное число, у которого десять знаков до запятой и два после: 1234567890.12
/** * Optional operation code. */ @Column(table = "operations_details") private Integer opCode;
Наконец, @Column используется, чтобы указать, в какой таблице сохранять поле, если заданые дополнительные таблицы.
Использование
Создадим операцию, сохраним её в базу и прочитаем обратно:
@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(); }
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 и посмотреть схему, увидим, что данные хранятся в двух таблицах, названных именно так, как написано в аннотациях!