JPA JOIN for simple access - java

I would like to be able to include an #Entity from another table using a foreign key. I'm following guides but I'm still confused and can't seem to get this working. The end goal would be something like this:
#Entity
#Table(name = "Labor", schema = "dbo", catalog = "database")
public class LaborEntity {
private int laborId;
private Timestamp laborDate;
private Integer jobNumber;
private Integer customerId;
//mapping of customer to labor
private CustomerEntity customer;
#Id
#Column(name = "LaborID", nullable = false)
public int getLaborId() {
return laborId;
}
public void setLaborId(int laborId) {
this.laborId = laborId;
}
#Basic
#Column(name = "LaborDate", nullable = true)
public Timestamp getLaborDate() {
return laborDate;
}
public void setLaborDate(Timestamp laborDate) {
this.laborDate = laborDate;
}
#Basic
#Column(name = "JobNumber", nullable = true)
public Integer getJobNumber() {
return jobNumber;
}
public void setJobNumber(Integer jobNumber) {
this.jobNumber = jobNumber;
}
#Basic
#Column(name = "CustomerID", nullable = true)
public Integer getCustomerId() {
return customerId;
}
public void setCustomerId(Integer customerId) {
this.customerId = customerId;
}
#ManyToOne(targetEntity = CustomerEntity.class)
#NotFound(action = NotFoundAction.IGNORE)
#JoinColumn(name = "CustomerID", //in this table
referencedColumnName = "CustomerID", //from CustomerEntity
insertable = false, updatable = false,
foreignKey = #javax.persistence.ForeignKey(value = ConstraintMode.NO_CONSTRAINT))
public CustomerEntity getCustomer() {
return this.customer;
}
public void setCustomer(CustomerEntity customer) {
this.customer = customer;
}
Anyway, the end goal is to get the Customer data from the Customer table as part of the Labor entity so it can be accessed directly with something like getCustomerEntity(). I supposed I would have to accomplish it by querying first using a JOIN like so:
TypedQuery<LaborEntity> query = entityManager.createQuery(
"SELECT l FROM LaborEntity l " +
"INNER JOIN CustomerEntity c " +
"ON l.customerId = c.customerId " +
"WHERE l.laborDate = '" + date + "'", LaborEntity.class);
List<LaborEntity> resultList = query.getResultList();
And then I can simply access the Customer that's associated like so:
resultList.get(0).getCustomer().getCustomerName();
Am I dreaming or is this actually possible?

Yes, this is completely possible.
(I'm not sure about what was the question though - but assuming you only want get it working)
You query needs to be a JPQL, not an SQL. And the Join is different on JPQL:
"SELECT l FROM LaborEntity l " +
"JOIN l.customer c " +
"WHERE ... "
The join starts from the root entity and then you use the field name (not column).
You can also use JOIN FETCH, then the associated entity (customer) will be loaded in the same query. (that is, Fetch EAGER)
Other recommendations:
Don't concat the parameters like that date. Instead use setParameter.
You don't need those #Basic
You don't need that targetEntity = CustomerEntity.class. It'll be detected automatically.

Related

java jpa one to one relation is always null for one side?

I have two entity booking and travelAgentBooking, booking could exist by itself while travelAgentBooing must have one booking of it.
TABookingEntity is below
#Entity
#ApplicationScoped
#Table(name = "TABooking")
#NamedQuery(name = "TABooking.findAll", query = "SELECT t FROM TABookingEntity t ORDER BY t.id ASC")
public class TABookingEntity {
#Id
#GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "TABookingId_seq")
#SequenceGenerator(name = "TABookingId_seq", initialValue = 1, allocationSize = 1)
private Long id;
#OneToOne(fetch = FetchType.EAGER)
#JoinColumn(name = "booking_id", nullable = false)
private BookingEntity flightbooking;
// belong to upstream booking so we just store id here
private Long taxibookingid;
private Long hotelbookingid;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public BookingEntity getFlightbooking() {
return flightbooking;
}
public void setFlightbooking(BookingEntity flightbooking) {
this.flightbooking = flightbooking;
if (flightbooking != null) {
flightbooking.setTravelAgentBooking(this);
}
}
public Long getTaxibookingId() {
return taxibookingid;
}
public void setTaxibookingId(Long taxibookingid) {
this.taxibookingid = taxibookingid;
}
public Long getHotelbookingId() {
return hotelbookingid;
}
public void setHotelbookingId(Long hotelbookingid) {
this.hotelbookingid = hotelbookingid;
}
BookingEntity is below
#Entity
#ApplicationScoped
#Table(name = "booking")
#NamedQueries({ #NamedQuery(name = "Booking.findAll", query = "SELECT b FROM BookingEntity b ORDER BY b.d ASC"),
#NamedQuery(name = "Booking.findByFlight", query = "SELECT b FROM BookingEntity b WHERE b.flight = :flight"),
#NamedQuery(name = "Booking.findByDate", query = "SELECT b FROM BookingEntity b WHERE b.d = :d") })
public class BookingEntity {
#Id
#GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "bookingId_seq")
#SequenceGenerator(name = "bookingId_seq", initialValue = 1, allocationSize = 1)
private Long id;
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "customer_id", nullable = false)
private CustomerEntity customer;
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "flight_id", nullable = false)
private FlightEntity flight;
#OneToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "travelAgentBooking_id", nullable = true)
private TABookingEntity travelAgentBooking;
#NotNull
#Column(name = "date")
private Date d;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public CustomerEntity getCustomer() {
return customer;
}
public void setCustomer(CustomerEntity customer) {
this.customer = customer;
if(customer != null)
customer.addBooking(this);
}
public FlightEntity getFlight() {
return flight;
}
public void setFlight(FlightEntity flight) {
this.flight = flight;
}
public Date getDate() {
return new Date(d.getTime());
}
public void setDate(Date d) {
this.d = d;
}
public TABookingEntity getTravelAgentBooking() {
return travelAgentBooking;
}
public void setTravelAgentBooking(TABookingEntity travelAgentBooking) {
this.travelAgentBooking = travelAgentBooking;
}
here is the code I creating booking firstly, and then set it to tabooking.
then I'm trying to update the booking since when it is created, there is no travelAngentBooking for it to associate.
Booking booking = flightService.createBooking(tabooking.getFlightbooking());
tabooking.setFlightbooking(booking);
,,,,,,,,,,,
,,,,,,,,,,,
tabookingService.create(tabooking);
flightService.updateBooking(tabooking.getFlightbooking().getId(), tabooking.getFlightbooking());
After running it the table of travelAgentBooking is perfect.
But booking table column referred to travelAgentBooking is always null for any booking object.
UPDATE:
#PUT
#Path("/{id:[0-9]+}")
#Operation(description = "Update a Booking in the database")
#APIResponses(value = { #APIResponse(responseCode = "200", description = "Booking updated successfully"),
#APIResponse(responseCode = "400", description = "Invalid Booking supplied in request body"),
#APIResponse(responseCode = "404", description = "Booking with id not found"),
#APIResponse(responseCode = "409", description = "Booking details supplied in request body conflict with another existing Booking"),
#APIResponse(responseCode = "500", description = "An unexpected error occurred whilst processing the request") })
#Transactional
public Response updateBooking(
#Parameter(description = "Id of Booking to be updated", required = true) #Schema(minimum = "0") #PathParam("id") Integer id,
#Parameter(description = "JSON representation of Booking object to be updated in the database", required = true) Booking booking) {
Customer customer = customerService.findById(booking.getCustomer().getId())
.orElseThrow(() -> new RestServiceException("We can't found customer", Response.Status.BAD_REQUEST));
if (!customer.equals(booking.getCustomer()))
throw new RestServiceException("use custoemr's own API for it update", Response.Status.BAD_REQUEST);
Flight flight = flightService.findById(booking.getFlight().getId())
.orElseThrow(() -> new RestServiceException("We can't found flight", Response.Status.BAD_REQUEST));
if (!flight.equals(booking.getFlight()))
throw new RestServiceException("use custoemr's own API for it update", Response.Status.BAD_REQUEST);
try {
bookingService.validateBooking(booking);
} catch (ConstraintViolationException ce) {
// Handle bean validation issues
Map<String, String> responseObj = new HashMap<>();
for (ConstraintViolation<?> violation : ce.getConstraintViolations()) {
responseObj.put(violation.getPropertyPath().toString(), violation.getMessage());
}
throw new RestServiceException("Bad Request", responseObj, Response.Status.BAD_REQUEST, ce);
} catch (UniqueFlightWithDateException e) {
// we are updating an existence flight, so ignore this as expected
}
try {
bookingService.update(id);
} catch (ServiceException e) {
Map<String, String> responseObj = new HashMap<>();
responseObj.put("id", "please ensure the id is associated with this number");
throw new RestServiceException("Bad Request", responseObj, Response.Status.NOT_FOUND, e);
}
bookingService.update(id);
return Response.ok(booking).build();
}
BookingEntity update(BookingEntity booking) {
log.info("BookingRepository.update() - Updating " + booking.getId());
em.merge(booking);
return booking;
}
From the original posted code, the problem is that you have two very independent unidirectional relationships and only setting one of them. Since they are independent, the other remains null and can not be anything other than null until it is set.
#OneToOne(fetch = FetchType.EAGER)
#JoinColumn(name = "booking_id", nullable = false)
private BookingEntity flightbooking;
The join column sets a foreign key in the "TABooking" table to point at the bookingEntity. It requires this relationship reference be set to populate that foreign key value. Same thing with:
#OneToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "travelAgentBooking_id", nullable = true)
private TABookingEntity travelAgentBooking
It creates its own travelAgentBooking_id foreign key column in the "booking" table that will remain null until you update a booking instance and set this reference. If you only set one side, the other will always remain null in the database.
But there are two problems with the model and your expectations. First, from the comments, you didn't intend this to be two separate relationships and instead expect it to be a single bidirectional relationship. For that, you need a single foreign key, and to pick a side that 'owns' it. The side that owns it controls it:
#OneToOne(mappedBy "flightbooking", fetch = FetchType.LAZY)
private TABookingEntity travelAgentBooking
Using mappedBy tells JPA that the other side owns the relationship. The foreign key column then is only set when you set the TABookingEntity.flightbooking reference and save/merge the TABookingEntity instance.
Second is you are using JSON and so Json serialization and assuming it abides by your object model and the JPA mappings. It does not. JPA annotations are for your persistence provider to tell it how to serialize/deserialize your model into the database but mean nothing for JSON serialization (or xml or any other REST formats). You need to tell your JSON tool how to handle your relationships, and that completely depends on how you are going to be expecting and sending the JSON. There are many tutorials and different strategies to deal with this (see this link for a good primer), but easiest is usually just to pick parts of the graph and exclude them with #JsonIgnore:
#OneToOne(mappedBy "flightbooking", fetch = FetchType.LAZY)
#JsonIgnore
private TABookingEntity travelAgentBooking
This will mean that any JSON you receive representing a booking will have a null travelAgentBooking. So if you need to see or set this relationship, your api would have to send/receive TABookingEntity which would still have the flightbooking reference serialized. I picked this way because flightbooking owns the relationship, so it matches JPA, but it doesn't need to. You can and should figure out what works for your client application and it may be different from the JPA mappings. I would expect that bookings always need to know the TABookingEntity and you'll want that sent to the client, so you might put the #JsonIgnore annotation on the other side. If you do, you'll just have to be sure that when you want to change or add TABookingEntity, that you fix the TABookingEntity.flightbooking reference appropriately so that you don't null out the foreign key.

How to rewrite native query to JPA criteria

How to rewrite the query on the criteria
#Query(value = "select count(*), hicn.name \n" +
"from \n" +
" table1 cid, \n" +
" table2 hicn \n" +
"where TRUNC(cid.CREATED_WHEN) = TRUNC(?) \n" +
" and hicn.ID = cid.ID\n" +
"group by hicn.name", nativeQuery = true)
and put result in the DTO?
public class DataDto {
private String name;
private Long count;
public DataDto(String name, Long count) {
this.name = name;
this.count = count;
}
}
Entity for example.Entity large, reduced for convenience. Can you show the solution with 'join' and 'and'. The main problem is that I don't understand how to access two tables and get data from them using criteria.
For table 1
#Data
#Entity
#Table(name = "TABLE_ONE")
public class TableOneModel {
#Id
#Column(name = "TAB_ONE_ID")
private Long tabOneId;
#Column(name = "CREATED_WHEN")
private Date createdWhen;
}
For table 2
#Data
#Entity
#Table(name = "TABLE_TWO")
public class TableTwoModel {
#Id
#Column(name = "TAB_TWO_ID")
private Long tabTwoId;
#Column(name = "NAME")
private String name;
}
Criteria criteria = session.createCriteria(TableOneModel.class);
criteria.setFetchMode("TableTwoModel", FetchMode.JOIN).add(Restrictions.eq("trunc(cid.CREATED_WHEN)", "checkpoint"))
.setProjection(Projections.projectionList().add(Projections.property("name"), "name")
.add(Projections.rowCount(),"Count")
.add(Projections.groupProperty("name"))).uniqueResult();
List list = criteria.list();
Your target must be looking something near to this. I didnt test out code though. I was not sure about what to put on your sql trunc method, so I added checkpoint there. If thats a dynamic variable, please research yourself how you can add it to criteria. In fact that may depend more on your code as well.

Hibernate/JPA JPQL to wrong SQL when querying Map<String,String> field

This is my Entity configuration
#Entity
#NamedQuery(name = "Payment.findByEmail", query = "SELECT p FROM Payment p JOIN p.additionalAuthData a " +
"WHERE KEY(a) = 'email' AND VALUE(a) = ?1 AND (p.paymentType = 4 OR p.paymentType = 10)")
public class Payment {
#Id
#GeneratedValue(strategy = GenerationType.SEQUENCE)
private Long id;
#Column(name = "payment_type")
private Integer paymentType;
/** other properties, getters and setters */
#ElementCollection
#CollectionTable(name = "additional_auth_data")
#MapKeyJoinColumn(name = "id", referencedColumnName = "id")
#MapKeyColumn(name = "field")
#Column(name = "data_value")
private Map<String, String> additionalAuthData;
}
The NamedQuery findByEmail("test#example.com") generates the following SQL
select -- all fields ...
from payment payment0_ inner join additional_auth_data additional1_ on payment0_.id=additional1_.id
where
additional1_.field='email' and (select additional1_.data_value from additional_auth_data additional1_ where payment0_.id=additional1_.id)='test#example.com' and (payment0_.payment_type=4 or payment0_.payment_type=10)
which is wrong: it may work if you have only one row but it blows up otherwise. H2 complains Scalar subquery contains more than one row and PostgreSQL more than one row returned by a subquery used as an expression. In fact, query's where condition compares a scalar value ('test#example.com') with a subquery.
The correct SQL should be:
select -- all fields
from payment payment0_ inner join additional_auth_data additional1_ on payment0_.id=additional1_.id
where additional1_.field='payerEmail' and additional1_.data_value='test#example.com' and (payment0_.payment_type=4 or payment0_.payment_type=10)
Is the HSQL correct? Is there a way to instruct Hibernate to generates a clever, better SQL? Is this a Hibernate bug?
Note: Hibernate shipped with Spring Boot Starter 1.3.7.RELEASE
Edit:
Using an #Embeddable class
#ElementCollection
#JoinTable(name = "additional_auth_data", joinColumns = #JoinColumn(name = "id"))
#MapKeyColumn(name = "field")
#Column(name = "data_value")
private Set<AdditionalData> additionalAuthData;
#Embeddable
public static class AdditionalData {
#Column(name = "field", nullable = false)
private String field;
#Column(name = "data_value")
private String dataValue;
protected AdditionalData() {
}
public AdditionalData(String field, String dataValue) {
this.field = field;
this.dataValue = dataValue;
}
/** Getters, setters; equals and hashCode on "field" */
}
#NamedQuery(name = "Payment.findByEmail", query = "SELECT p FROM Payment p JOIN p.additionalAuthData a " +
"WHERE a.field = 'email' AND a.dataValue = ?1 AND (p.paymentType = 4 OR p.paymentType = 10)")
solves the problem, and the SQL is correct, but it looks just plain wrong, like shooting a fly with a bazooka...
It generates correct SQL without value().
Use just a=?1
But I would expect is should generate it simple also with it.

Hibernate QueryException

Hello I am trying to reference in a Criteria a property of a composite key which is defined as and #Embeddable on an Entity
#Entity
#Table(name = "B_J_P")
public class BJP implements java.io.Serializable {
private BJPId id;
private BJI bJI;
public BJP() {
}
public BJP(BJPId id, BJI bJI) {
this.id = id;
this.bJI = bJI;
}
#EmbeddedId
#AttributeOverrides( {
#AttributeOverride(name = "jIId", column = #Column(name = "J_I_ID", nullable = false)),
#AttributeOverride(name = "kN", column = #Column(name = "K_N", nullable = false, length = 100)),
public BJPId getId() {
return this.id;
}
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "J_I_ID", nullable = false, insertable = false, updatable = false)
public BJI getBJI() {
return this.bJI;
}
}
I need to reach the kName from the following:
#Embeddable
public class BJPId implements java.io.Serializable {
private long jIId;
private String kName;
public BJPId() {
}
public BJPId(long jIId, String kN) {
this.jIId = jIId;
this.kN = kN;
}
#Column(name = "J_I_ID", nullable = false)
public long getJIId() {
return this.jIId;
}
#Column(name = "K_NAME", nullable = false, length = 100)
public String getKName() {
return this.kName;
}
}
But when I am trying to reach it from the base class where BJP is a property with the following Criteria
DetachedCriteria timestampFilter = DetachedCriteria.forClass(BJP.class)
.createAlias("id","alias")
.add(Restrictions.eq("alias.kName","DataSetName"))
.setProjection(Projections.property("kName"));
I get the following error:
org.hibernate.QueryException: Criteria objects cannot be created directly on components. Create a criteria on owning entity and use a dotted property to access component property: id
at org.hibernate.loader.criteria.CriteriaQueryTranslator.getPathInfo
How should I formulate the criteria query in order to reach the kName property to apply filtering based on it in a dynamic sql context ?
If I have not provided enough relevant information, please ask what have I forgotten to provide full context.
EDIT: Upon Genzetto advice I have managed to reach the elements(at least it is not giving errors now) but returns no results once I do this:
DetachedCriteria timestampFilter = DetachedCriteria.forClass(BJP.class)
.add(Restrictions.eq("id.kName","DataSetName"))
.setProjection(Projections.property("id.kName"));
Session currentSession = sessionFactory.getCurrentSession();
Criteria query = currentSession.createCriteria(BJI.class)
.add(Subqueries.propertyEq("bJP",timestampFilter))
as upon looking at the SQL it is of the format
... where this_.J_INST_ID = (select this_.K_NAME as y0_ from .B_J_P this_ where this_.K_NAME=?)
it is trying to add the subquery to the ID of the root object although I want it part of bJP. How can I add it to proper location ?
You don't need to use an alias to do this. You can access directly to the composite key attributes:
DetachedCriteria timestampFilter = DetachedCriteria.forClass(BJP.class)
.add(Restrictions.eq("id.kName","DataSetName"))
.setProjection(Projections.property("id.kName"));

JPA Null or zero primary key encountered in unit of work clone

I learn about JPA and had task to make database and insert some values to it. I wondered how I can find out what was the ID of recently inserted object, so I found a way that I need to use flush method of EntityManager.
Unfortunately I got the
Null or zero primary key encountered in unit of work clone
exception when I use the above method. I think the problem lies in that my database has all ID's set on autoincrement ( I use ORACLE 11G Express ), so before commiting it has null value and it rollbacks transaction.
What I can do to fix it ?
This is DB ( ID's are autoincrement[Sequences and Triggers in oracle]):
public class Client {
public static void main(String[] args) {
EntityManagerFactory emf =
Persistence.createEntityManagerFactory("JpaIntroductionPU");
EntityManager em = emf.createEntityManager();
EntityTransaction et = em.getTransaction();
et.begin();
Address ad1 = new Address();
ad1.setStreet("Skaraktki");
ad1.setCode("64-340");
em.persist(ad1);
em.flush();
System.out.println(ad1.getAId());
et.commit();
}
}
Address class
#Entity
#Table(name = "ADDRESS")
#NamedQueries({
#NamedQuery(name = "Address.findAll", query = "SELECT a FROM Address a"),
#NamedQuery(name = "Address.findByAId", query = "SELECT a FROM Address a WHERE a.aId = :aId"),
#NamedQuery(name = "Address.findByStreet", query = "SELECT a FROM Address a WHERE a.street = :street"),
#NamedQuery(name = "Address.findByCode", query = "SELECT a FROM Address a WHERE a.code = :code")})
public class Address implements Serializable {
private static final long serialVersionUID = 1L;
// #Max(value=?) #Min(value=?)//if you know range of your decimal fields consider using these annotations to enforce field validation
#Id
#Basic(optional = false)
#Column(name = "A_ID")
private BigDecimal aId;
#Basic(optional = false)
#Column(name = "STREET")
private String street;
#Basic(optional = false)
#Column(name = "CODE")
private String code;
#OneToOne(cascade = CascadeType.ALL, mappedBy = "aId")
private Employee employee;
#OneToOne(cascade = CascadeType.ALL, mappedBy = "aId")
private Department department;
public Address() {
}
public Address(BigDecimal aId) {
this.aId = aId;
}
public Address(BigDecimal aId, String street, String code) {
this.aId = aId;
this.street = street;
this.code = code;
}
public BigDecimal getAId() {
return aId;
}
public void setAId(BigDecimal aId) {
this.aId = aId;
}
public String getStreet() {
return street;
}
public void setStreet(String street) {
this.street = street;
}
public String getCode() {
return code;
}
public void setCode(String code) {
this.code = code;
}
public Employee getEmployee() {
return employee;
}
public void setEmployee(Employee employee) {
this.employee = employee;
}
public Department getDepartment() {
return department;
}
public void setDepartment(Department department) {
this.department = department;
}
#Override
public int hashCode() {
int hash = 0;
hash += (aId != null ? aId.hashCode() : 0);
return hash;
}
#Override
public boolean equals(Object object) {
// TODO: Warning - this method won't work in the case the id fields are not set
if (!(object instanceof Address)) {
return false;
}
Address other = (Address) object;
if ((this.aId == null && other.aId != null) || (this.aId != null && !this.aId.equals(other.aId))) {
return false;
}
return true;
}
#Override
public String toString() {
return "jpaintroduction.Address[ aId=" + aId + " ]";
}
}
This happend to me because I manually added an entry to my database with the id 0 (zero).
In my case EclipseLink "couldn't" handle an id with zero.
So I added following to my persistence.xml:
<property name="eclipselink.allow-zero-id" value="true"/>
This property says EclipseLink to handle zero as a valid id.
[1] http://meetrohan.blogspot.de/2011/11/eclipselink-null-primary-key.html
You need to annotate your id field with #GeneratedValue, in order for JPA to know that the DB will generate the id automatically:
#Id
#Basic(optional = false)
#Column(name = "A_ID")
#SequenceGenerator( name = "mySeq", sequenceName = "MY_SEQ", allocationSize = 1, initialValue = 1 )
#GeneratedValue(strategy=GenerationType.IDENTITY, generator="mySeq")
private BigDecimal aId;
With oracle you can use GenerationType.IDENTITY and #SequenceGenerator in which case you don't need a trigger to query the sequence and populate the ID, JPA will do it for you. I'm not sure if GenerationType.AUTO will work with oracle but if it does, you'd need a trigger to query the sequence and populate the id. GenerationType.TABLE is the most portable solution, since you use an independent table managed by JPA to store the sequence, it works across all databases.
Check the docs in the link above.
As you said, the problem lies in the fact that even when you commit a transaction, the Auto-increment function isn't invoked right away. Actually, while you are managing database interactions with the same EntityManager it won't.
My question would rather be: why are you persisting, then committing the transaction if you flush it anyway? Looks like duplicated code to me.
See this on flush.

Categories

Resources