I've searched for quite a while trying to figure this out. I am using JPA with EclipseLink (Oracle DB). I have a lookup table full of values. I have another table that has a FK relationship to that table. I can insert data fine, but when I try to update the table with a different value, I get an exception. I've tried setting the CASCADE_TYPE but that doesn't have any impact. I thought this would be simple, but maybe I'm missing something.
Lookup table:
public class SomeType implements Serializable {
#Id
#Column(name = "ID")
private Short id;
#Column(name = "TYPE")
private String type;
:
(getters & setters)
}
Contents:
ID Type
------------
1 Type1
2 Type2
3 Type3
: :
Person table (I've left out the Sequencing stuff for brevity):
public class Person implements Serializable {
#Id
#Column(name = "ID")
private Short id;
#JoinColumn(name = "SOME_TYPE", referencedColumnName = "ID")
#ManyToOne(optional = false)
private SomeType someType;
:
(getters & setters)
}
Inserting works fine:
EntityManager em;
:
Person p = new Person();
p.setSomeType(new SomeType(1));
em.persist(p);
That results in:
ID SOME_TYPE
------------------
1 1
But if I want to update Person to change the type:
EntityManager em;
:
Person p = em.find(1);
SomeType newtype = new SomeType(2);
p.setSomeType(newtype);
em.merge(p);
I see the following exception:
Exception [EclipseLink-7251] (Eclipse Persistence Services - 2.5.2.v20140319-9ad6abd): org.eclipse.persistence.exceptions.ValidationException
Exception Description: The attribute [id] of class [SomeType] is mapped to a primary key column in the database. Updates are not allowed.
All I want is the value in the Person table to be updated, like:
UPDATE PERSON set SOME_TYPE = 2 where ID = 1;
Any assistance would be greatly appreciated. Thanks.
Thanks to Chris for answering this:
The update is possible if you use a reference to the managed instance of the object that you want to refer to, not create a new instance.
Person p = em.find(1);
p.setSomeType(em.find(SomeType.class, 2));
em.merge(p);
Related
I'm starting a project to know more in detail JPA.
Context:
At the end of his internship, the student has a report to make and a presentation in front of his professor to do about the internship.
I've a database, which is called "grade_management". It must contains a "student", "presentation", "report", "professor" and a "mark" (there are several rating criteria such as expression, quality of powerpoint ...) table. But now it's empty, since I want to make it throught JPA.
I've a "Presentation" class. Which countain this:
#Entity
public class Presentation implements Serializable {
#Id
#GeneratedValue (strategy=GenerationType.AUTO)
private int presentation_id;
private Date date;
private mark_id;
private int professor_id;
public Soutenance() {}
public Soutenance(Date date) {
this.date = date;
}
}
But the Presentation table contain 2 foreign key: professor_id and mark_id.
My question is: How can I indicate that both of them are foreign key ?
I'm sorry if I'm not clear, don't hesitation to ask question.
Cordially
You shouldn't reference other entities by their ID, but by a direct reference to the entity.
Something like that :
#ManyToOne
#JoinColumn(name = "mark_id", referencedColumnName = "id")
private Mark mark; // supposed here that mark_id if link to entity `Mark`
#ManyToOne
#JoinColumn(name = "professor_id", referencedColumnName = "id") // suppose "id" is the column name of the PK inside the table Professor.
private Professor professor; // supposed here that professor_id if link to entity `Professor`
This code is supposing that you use an unidirectional relation.
For bidirectional you have to define this in the other side (Mark/Professor type)
#OneToMany(mappedBy = "professor")
private Presentation presentation;
From your explanation, it looks like you have a Database named grade_management and in that database you have "student", "presentation", "report", "professor" and a "mark" tables (i.e: which are #Entity by themselves defined in their separate respective classes )
I'm not sure whether you have defined them or not. If not then you have to define them first and then use the refactored code mentioned below.
So, you will have many-to-one relation mapping. You can annotate your foreign keys belonging to different tables using #ManyToOne annotation to indicate relation type and #JoinColumn annotation to indicate that this entity has a foreign key to the referenced table.
You can redefine your Presentation class show below:
#Entity
#Table(name = "Presentation")
public class Presentation implements Serializable {
#Id
#Column(name="presentation_id")
#GeneratedValue (strategy=GenerationType.AUTO)
private int presentation_id;
private Date date;
#ManyToOne
#JoinColumn(name = "mark_id")
private Mark mark_id;
#ManyToOne
#JoinColumn(name = "professor_id")
private Professor professor_id;
public Soutenance() {}
public Soutenance(Date date) {
this.date = date;
}
//getter and setter
}
Also, if you need more information to read upon for yourself you can always checkout this Hibernate Documentation that explains everything you'll need to know.
I'm using QueryHints in Spring Data JPA to use EclipseLink Batch Fetch with a type of IN. Ultimately, I need to use this around 30 fields but it doesn't seem to work right for 2 fields. Field A has a ManyToOne relationship and Field B has a ManyToMany. Based on the results of the initial query, I would expect the batch hint to generate an IN clause with 2 ids for Field A and 12 for Field B. This works fine when the hint is turned on for one field at a time. When it is enabled for both fields, the hint only applies to whichever field is the last hint in the list of QueryHints. I've tried EAGER and LAZY fetch on the fields as a shot in the dark, but it had not impact.
Is there a limitation with mixing batch fetch hints based on the relationship type? Is there something different going on? The EclipseLink documentation isn't very detailed for this feature.
EDIT: It seems it doesn't matter what fields I enable it only, it only works for one at at time. Here is sample code for two entities. The BaseEntity defines the PK id generation.
#Entity
#Table(name = "MainEntity")
public class MainEntity extends BaseEntity implements Cloneable {
...
#ManyToMany(fetch=FetchType.LAZY, cascade=CascadeType.PERSIST)
#JoinTable(
name="EntityBMapping",
joinColumns={#JoinColumn(name="mainId", referencedColumnName="id")},
inverseJoinColumns={#JoinColumn(name="bId", referencedColumnName="id")})
#JsonIgnore
private Set<EntityB> bSet = new HashSet<>();
#ManyToMany(fetch=FetchType.LAZY, cascade=CascadeType.ALL)
#JoinTable(
name="EntityAMapping",
joinColumns={#JoinColumn(name="mainId", referencedColumnName="id")},
inverseJoinColumns={#JoinColumn(name="aId", referencedColumnName="id")})
#JsonIgnore
#OrderColumn(name="order_index", columnDefinition="SMALLINT")
private List<EntityA> aList = new ArrayList<>();
...
}
#Entity
#Cache(type=CacheType.FULL)
#Table(name = "EntityA")
public class EntityA extends BaseEntity {
#Column(name = "name", columnDefinition = "VARCHAR(100)")
private String name;
#ManyToMany(mappedBy = "entityASet", fetch=FetchType.LAZY)
#JsonIgnore
private Set<MainEntity> mainEntityList = new HashSet<>();
}
#Entity
#Cache(type=CacheType.FULL)
#Table(name = "EntityB")
public class EntityB extends BaseEntity {
#Column(name = "name", columnDefinition = "VARCHAR(100)")
private String name;
#ManyToMany(mappedBy = "entityBSet", cascade=CascadeType.ALL)
#JsonIgnore
private Set<MainEntity> mainEntityList = new HashSet<>();
}
The repository query:
#QueryHints(value = {
#QueryHint(name = org.eclipse.persistence.config.QueryHints.BATCH_TYPE, value = "IN"),
#QueryHint(name = org.eclipse.persistence.config.QueryHints.BATCH_SIZE, value = "250"),
#QueryHint(name = org.eclipse.persistence.config.QueryHints.BATCH, value = "o.aList")},
#QueryHint(name = org.eclipse.persistence.config.QueryHints.BATCH, value = "o.bSet")},
forCounting = false)
List<MainEntity> findAll(Specification spec);
Generated queries:
SELECT id, STATUS, user_id FROM MainEntity WHERE ((STATUS = ?) OR ((STATUS = ?) AND (user_id = ?)))--bind => [ONESTAT, TWOSTAT, myuser]
..
SELECT t1.id, t1.name, t0.order_index FROM EntityAMapping t0, EntityA t1 WHERE ((t0.mainId = ?) AND (t1.id = t0.aId))--bind => [125e17d2-9327-4c6b-a65d-9d0bd8c040ac]
...
SELECT t1.id, t1.name, t0.mainId FROM EntityBMapping t0, EntityB t1 WHERE ((t1.id = t0.bId) AND (t0.mainId IN (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)))--bind => [125e17d2-9327-4c6b-a65d-9d0bd8c040ac, 1c07a3a9-7028-48ba-abe8-2296d58ebd57, 235bb4f2-d724-4237-b73b-725db2b9ca9f, 264f64b3-c355-4476-8530-11d2037b1f3c, 2d9a7044-73b3-491d-b5f1-d5b95cbb1fab, 31621c93-2b0b-4162-9e42-32705b7ba712, 39b33b19-c333-4523-a5a7-4ba0108fe9de, 40ba7706-4023-4b7e-9bd5-1641c5ed6498, 52eed760-9eaf-4f6a-a36f-076b3eae9297, 71797f0c-5528-4588-a82c-5e1d4d9c2a66, 89eda2ef-80ff-4f54-9e6a-cf69211dfa61, 930ba300-52fa-481c-a0ae-bd491e7dc631, 96dfadf9-2490-4584-b0d4-26757262266d, ae079d02-b0b5-4b85-8e6f-d3ff663afd6e, b2974160-33e8-4faf-ad06-902a8a0beb04, b86742d8-0368-4dde-8d17-231368796504, caeb79ce-2819-4295-948b-210514376f60, cafe838f-0993-4441-8b99-e012bbd4c5ee, da378482-27f9-40b7-990b-89778adc4a7e, e4d7d6b9-2b8f-40ab-95c1-33c6c98ec2ee, e557acf4-df01-4e66-9d5e-84742c99870d, ef55a83c-2f4c-47b9-99bb-6fa2f5c19a76, ef55a83c-2f4c-47b9-99bb-6fa2f5c19a77]
...
SELECT t1.id, t1.name, t0.order_index FROM EntityAMapping t0, EntityA t1 WHERE ((t0.mainId = ?) AND (t1.id = t0.aId))--bind => [1c07a3a9-7028-48ba-abe8-2296d58ebd57]
As Chris mentioned, Named Queries are the best work around for this issue. The other option is to use a custom repository and call setHint on the EntityManager yourself for each hint specified (plenty of examples out there for creating custom repos in Spring Data JPA). You could attempt to override findOne(...) and protected <S extends T> TypedQuery<S> getQuery(Specification<S> spec, Class<S> domainClass, Sort sort) on SimpleJpaRepository to try and create a generic way to properly set the hints but you'll likely want to check that you don't duplicate hint setting on getQuery(...) as you'll still want to call super() for that and then apply your additional hints before returning the query. I'm not sure what the behavior would be if you applied a duplicate hint. Save yourself the trouble and use Named Queries is my advice.
I need some help with the design of my entities.
I have a single table MyEntity with the following columns:
id | column1 | column2 | type
And the entity looks like:
#Entity
#DiscriminatorColumn(discriminatorType = DiscriminatorType.STRING, name = "type", length = 255)
#Inheritance(strategy = InheritanceType.SINGLE_TABLE)
public class MyEntity {
#id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private long id;
}
I would like to be able to have several entities inheriting from MyEntity and where each entity define how they use the columns column1 and column2. I would like to just use one single table for all my subclasses, and use the discriminator.
Like this (the following doesn't work):
#Entity
#DiscriminatorValue("TestClass1")
public class TestClass1 extends MyEntity {
#Column(name = "column1");
private long testField1;
#Column(name = "column2");
private long testField2;
}
#Entity
#DiscriminatorValue("TestClass2")
public class TestClass2 extends MyEntity {
#Column(name = "column1");
private long anotherName1;
#Column(name = "column2");
private long anotherName2;
}
I know this approach is not working, so I am reaching out to you guys for help.
EDIT
I have added the inheritance strategy to the MyEntity class, but SINGLE_TABLE is default, so it shouldn't make a difference.
I have tried two approaches:
1) If I don't put two fields MyEntity named column1 and column2, then I cannot create any named queries in MyEntity such as:
#NamedQuery(name=TEST, query="select m from MyEntity m where m.column1 = :val")
I get the following exception:
Caused by: org.hibernate.QueryException: could not resolve property: column1 of: com.test.MyEntity
I would really prefer to specify the queries in MyEntity and not in the subclasses.
2) If I put the fields (column1 and column2) in MyEntity, then I get the following exception:
Caused by: org.hibernate.MappingException: Repeated column in mapping for entity: com.test.TestClass1 column: column1 (should be mapped with insert="false" update="false")
and I don't know how to move on from here. I'm not sure how to do the mapping, which is suggested in the exception message.
I have these two entities:
Anagrafica
#Entity
#Access(AccessType.FIELD)
#Table(name = "S_MC_CC_USER")
#SequenceGenerator(name = "SEQ_ID", sequenceName = "SEQ_ID", allocationSize = 1)
public class Anagrafica implements Serializable{
private static final long serialVersionUID = 332466838544720886L;
#EmbeddedId
private AnagraficaId anagraficaId;
#Column(name = "USER_ID")
#GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "SEQ_ID")
private Long userId;
#OneToMany(cascade = CascadeType.ALL)
#JoinColumn(name = "USER_ID", updatable = false, insertable = false)
private List<Mobile> mobiles;
/**
* La classe di dominio che modella la chiave primaria di un {#link Anagrafica}
*
* #author Massimo Ugues
*
*/
#Embeddable
static public class AnagraficaId implements Serializable {
private static final long serialVersionUID = -54640203292300521L;
#Column(name = "ANAG_UTENTE")
private String bt;
#Column(name = "COD_ABI")
private String abi;
public AnagraficaId() {
super();
}
Mobile
#Entity
#Table(name = "S_MOBILE")
#SequenceGenerator(name = "SEQ_MOBILE", sequenceName = "SEQ_MOBILE", allocationSize = 1)
public class Mobile implements Serializable{
private static final long serialVersionUID = 5999493664911497370L;
#Id
#GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "SEQ_MOBILE_DEVICE_REGISTRY")
#Column(name = "ID_MOBILE")
private Long mobileId;
#Column(name = "DEVICE_TOKEN")
private String deviceToken;
#Column(name = "DATA_INSERIMENTO")
#Temporal(TemporalType.TIMESTAMP)
private Calendar dataInserimento = Calendar.getInstance();
With eclispe-link 2.1.2 all works great, but with eclispe-link 2.5.1 I got this exception:
Caused by: javax.persistence.PersistenceException: Exception [EclipseLink-28018] (Eclipse Persistence Services - 2.5.1.v20130918-f2b9fc5): org.eclipse.persistence.exceptions.EntityManagerSetupException
Exception Description: Predeployment of PersistenceUnit [persistence-unit] failed.
Internal Exception: Exception [EclipseLink-7220] (Eclipse Persistence Services - 2.5.1.v20130918-f2b9fc5): org.eclipse.persistence.exceptions.ValidationException
Exception Description: The #JoinColumns on the annotated element [field mobiles] from the entity class [class com.intesasanpaolo.domain.entities.sub.Anagrafica] is incomplete. When the source entity class uses a composite primary key, a #JoinColumn must be specified for each join column using the #JoinColumns. Both the name and the referencedColumnName elements must be specified in each such #JoinColumn.
at org.eclipse.persistence.internal.jpa.EntityManagerSetupImpl.createPredeployFailedPersistenceException(EntityManagerSetupImpl.java:1954)
at org.eclipse.persistence.internal.jpa.EntityManagerSetupImpl.predeploy(EntityManagerSetupImpl.java:1945)
at org.eclipse.persistence.jpa.PersistenceProvider.createContainerEntityManagerFactory(PersistenceProvider.java:322)
at org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean.createNativeEntityManagerFactory(LocalContainerEntityManagerFactoryBean.java:288)
at org.springframework.orm.jpa.AbstractEntityManagerFactoryBean.afterPropertiesSet(AbstractEntityManagerFactoryBean.java:310)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.invokeInitMethods(AbstractAutowireCapableBeanFactory.java:1571)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.initializeBean(AbstractAutowireCapableBeanFactory.java:1509)
... 40 more
The problem is the OneToMany association based on a foreign key that is not primary key.
Since I cannot change the database model how can I make it work?
Kind regards
Massimo
The reason it worked in a prior version was that it EclipseLink doesn't look at the fields in the mapping, but with JPA adding derived Id support, EclipseLink now validates the number of foreign keys match the number of ID fields.
James' answer here
JPA #JoinColumn issues while joining on non primary key columns
explains it that you'll need to use a descriptorCustomizer to change the JPA mapping. So you would either not map the field in JPA (mark it as #Transient) and then add a mapping in the customizer, or have the JPA mapping to use all primary key fields and then change the mapping in the customizer to only use the USER_ID->USER_ID fields.
EclipseLink customizers are shown here:
http://eclipse.org/eclipselink/documentation/2.4/jpa/extensions/a_customizer.htm
Ok, this is the Customizer I created:
public void customize(ClassDescriptor descriptor) throws Exception {
// handle the oneToManyMapping to non foreign keys
ManyToManyMapping mapping = (ManyToManyMapping) descriptor.getMappingForAttributeName("mobileDevices");
ExpressionBuilder builder = new ExpressionBuilder();
mapping.setSelectionCriteria(builder.getField("USER_ID").equal(builder.getParameter("USER_ID")));
// handle the insert statement
mapping.setInsertCall(new SQLCall(""));
}
As suggested from Chris this works great with the selection.
I had to modify the Insert Call since eclipse-link tried to create and insert statement on a mapping table that I haven't.
The problem now is on the delete: when I try to delete the collection from the source association (i.e. Cliente) as described here
Cliente.ClienteId id = new Cliente.ClienteId(abi, bt);
Cliente cliente = clienteRepository.findOne(id);
cliente.setMobileDevices(null);
I need eclipse link to delete the orphan.
The dml generated is the following:
DELETE FROM S_MC_CC_CLIENTI_S_MOBILE_DEVICE_REGISTRY WHERE ((mobileDevices_ID_MOBILE_DEVICE_REGISTRY = 13) AND ((ANAG_UTENTE = '71576493') AND (COD_ABI = '01025')))
Since I haven't the mapping table I modified the customizer adding a setDeleteCall statement :
mapping.setDeleteCall(new SQLCall("DELETE FROM S_MOBILE_DEVICE_REGISTRY WHERE USER_ID = #USER_ID"));
In this way eclipse link generates 2 dml:
DELETE FROM S_MOBILE_DEVICE_REGISTRY WHERE USER_ID = NULL
DELETE FROM S_MOBILE_DEVICE_REGISTRY WHERE (ID_MOBILE_DEVICE_REGISTRY = 13)
The first is the translation of my SQLCall, but without the correct parameter: any idea how to generate only the correct delete statement?
Kind regards.
Massimo
I have the following entities and would like to seek help on how to query for selected attributes from both side of the relationship. Here is my model. Assume all tables are properly created in the db. JPA provider I am using is Hibernate.
#Entity
public class Book{
#Id
private long id;
#Column(nullable = false)
private String ISBNCode;
#ManyToOne(cascade = CascadeType.DETACH, fetch = FetchType.LAZY, optional = false)
private Person<Author> author;
#ManyToOne(cascade = CascadeType.DETACH, fetch = FetchType.LAZY, optional = true)
private Person<Borrower> borrower;
}
#Inheritance
#DiscriminatorColumn(name = "personType")
public abstract class Person<T>{
#Id
private long id;
#OneToOne(cascade = CascadeType.ALL, fetch = FetchType.EAGER)
private Info information;
}
#Entity
#DiscriminatorValue(PersonType.Author)
public class Author extends Person<Author> {
private long copiesSold;
}
#Entity
#DiscriminatorValue(PersonType.Borrower)
public class Borrower extends Person<Borrower> {
.....
}
#Entity
public class Info {
#Id
private long id;
#Column(nullable=false)
private String firstName;
#Column(nullable=false)
private String lastName;
......;
}
As you can see, the book table has a many to one relation to Person that is not nullable and Person that is nullable.
I have a requirement to show, the following in a tabular format -
ISBNCode - First Name - Last Name - Person Type
How can I write a JPA query that will allow me to select only attributes that I would want. I would want to get the attributes ISBN Code from Book, and then first and last names from the Info object that is related to Person Object that in turn is related to the Book object. I would not want to get all information from Info object, interested only selected information e.g first and last name in this case.
Please note that the relation between the Borrower and Book is marked with optional=true, meaning there may be a book that may not have been yet borrowed by someone (obviously it has an author).
Example to search for books by the author "Marc":
Criteria JPA Standard
CriteriaQuery<Book> criteria = builder.createQuery( Book.class );
Root<Book> personRoot = criteria.from( Book.class );
Predicate predicate = builder.conjunction();
List<Expression<Boolean>> expressions = predicate.getExpressions();
Path<Object> firtsName = personRoot.get("author").get("information").get("firstName");
expressions.add(builder.equal(firtsName, "Marc"));
criteria.where( predicate );
criteria.select(personRoot);
List<Book> books = em.createQuery( criteria ).getResultList();
Criteria JPA Hibernate
List<Book> books = (List<Book>)sess.createCriteria(Book.class).add( Restrictions.eq("author.information.firstName", "Marc") ).list();
We recommend using hibernate criterias for convenience and possibilities.
Regards,