2010-09-07

JPA2/Hibernate kao fil na torti ili kao temelj kuće

JPA je Java persistence API, zvanični standard (ili kako ga oni zovu specifikacija) za objektno-relaciono mapiranje u Javi. Deo je JEE 5 specifikacije iako se može koristiti i u običnoj javi (JSE). Kao svaki standard JPA je tu da uvede red među različitim specifičnim ORM bibliotekama i pruži jedinstven način za rad s njima na dobrobit programera koji sad mogu na isti način da rade sa Hibernate i sa TopLink ORM biblotekama.

Prva JPA verzija kao deo JEE 5 je bila prilično siromašna u mogućnostima. Od svih velikih ORM biblioteka uzeto je samo ono što je zajedničko, najveći zajednički sadržalac. Uz JEE6 6 je izašlo drugo izdanje JPA standarda a već je implementiran u Hibernate 3.5+ i EclipseLink 2.0+ (bivšem TopLinku). Nova verzija je donela dosta novina ali i par razočarenja (o tome više u nekom drugom postu).

E sad da pokažemo kako nam JPA preko Hibernate (JPA/Hibernate) može pomoći. Nastavljam se na prethodni post. Pokazao sam kako se treba aplikacija podeliti u slojeve i kako se sa Springom pojednostavljuje sistem. Uvođenje JPA/Hiberate u takav sistem ima vrlo mali uticaj na arhitekturu. Ako je sve urađeno po JUS-u prelazak sa JDBC na JPA način rada sa bazom će uticati samo na sloj perzistencije. Eventualno će uticati ali minimalno na domenski sloj ako se podaci o tome kako se klase povezuju sa tabelama stave u kod u vidu anotacija. Drugi pristup je da se ti podaci smeste u xml fajl a kod ostavi netaknut. Ovolika izmena sa tako fino lociranim mestom menjanja pokazuje kvalitet višeslojnosti. Da je aplikacija loše podeljena preomena bi se prenosila u gomilu klasa.

Pošto smo u prošlom primeri imali samo jednu klasu u sloju perzistencije dajem njen izgled kad se radi preko JDBC-a:

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import javax.sql.DataSource;
import org.springframework.jdbc.datasource.DataSourceUtils;

public class CustomersInDB implements Customers {
  private DataSource dataSource;

  public void setDataSource(DataSource dataSource) {
    this.dataSource = dataSource;
  }

  private Connection getConnection() {
    return DataSourceUtils.getConnection(dataSource);
  }

  @Override
  public Customer loadCustomer(String customerId) {
    try {
      PreparedStatement st = getConnection()
          .prepareStatement("select * from customers where id=?");
      try {
        st.setString(1, customerId);
        ResultSet rs = st.executeQuery();
        if (!rs.next()) {
          throw new RuntimeException("Customer has been deleted");
        }
        return new Customer(
            customerId,
            rs.getString("name"),
            rs.getString("address"));
      } finally {
        st.close();
      }
    } catch (SQLException e) {
      throw new RuntimeException(e);
    }
  }

  @Override
  public void saveCustomer(Customer customer) {
    try {
      PreparedStatement st =
          getConnection().prepareStatement(
              "update customers set name=?, address=? where id=?");
      try {
        st.setString(1, customer.getName());
        st.setString(2, customer.getAddress());
        st.setString(3, customer.getId());
        st.executeUpdate();
      } finally {
        st.close();
      }
    } catch (SQLException e) {
      throw new RuntimeException(e);
    }
  }
}


Prelaskom na JPA dobija se sledeći izgled klase:

import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import javax.persistence.TypedQuery;

public class CustomersInDB implements Customers {
  @PersistenceContext
  private EntityManager entityManager; // Spring će uraditi dependency injection

  @Override
  public Customer loadCustomer(String customerId) {
    TypedQuery<Customer> q = entityManager
        .createQuery("select c from Customer as c where c.id=:id",
            Customer.class);
    q.setParameter("id", customerId);

    return q.getSingleResult();
  }

  @Override
  public void saveCustomer(Customer customer) {
    entityManager.merge(customer);
  }
}


U ovom kodu samo je TypedQuery deo JPA2. U JPA1 bi koristili Query interface koji nije generički pa bi morali Object referencu da pretvorimo u Customer prilikom poziva getSingleResult metode.

A da bi sve ovo radilo potrebo je u klasi u domenskoj klasi Customer ili u persistence.xml datoteci opisati povezivanje klasa i tabeli. Primer za opisivanje u domenskoj klasi preko antotacije:

@Entity
public class Customer implements Serializable, Cloneable {

  @Id
  @GeneratedValue
  private long internId;
  private String id;
  private String name;
  private String address;

  public Customer() {
  }

  public Customer(String id, String name, String address) {
    this.id = id;
    this.name = name;
    this.address = address;
  }

  // getters, setters, equals, hash, clone kao i ranije

}


Tabela sličnosti pojmova u JDBC i JPA:

JDBC JPA
database persistence unit
DataSourceEntityManagerFactory
Connection EntityManager


Mogli smo promene da izvedemo i na drugi način. Mogli smo da uopšte ne menjamo CustomersInDB klasu nego napravimo novu implementaciju Cusotmers interfejsa, recimo JPACustomersInDB i da u xml config datoteci za Spring DefaultCusotmersService beanu podmtnemo preko dependency injectiona tu novu implementaciju. To pokazuje kako je dependenci injection moćna stvar. Možemo izmeniti sistem a da ne menjamo postojeći kod!

Ako ste zainteresovani za JPA pogledajte tutorijal na http://www.vogella.de/articles/JavaPersistenceAPI/article.html Iako se tu obrađuje JPA 1 i to u Java SE, mislim da je za početak sasvim odgovarajuć i pošto se radi o običnoj javi nema dependency injection-a, Springa, aplikacionog servera, web programiranja i ostalih zbunjujućih stvari.

2 comments:

trine said...
This comment has been removed by the author.
Zlatan Kadragić said...

Drago mi je da si pažljivo pročitao i uočio grešku :)