JPA entity mapping

Главное в любом 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 и посмотреть схему, увидим, что данные хранятся в двух таблицах, названных именно так, как написано в аннотациях!

 

Скачать код примера

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *