Simplified version of my problem is this: I have three entities (annotations also simplified):
#Entity
public class A
{
#Id
private int id;
#ManyToMany(cascade = {CascadeType.PERSIST, CascadeType.MERGE, CascadeType.REFRESH})
private Collection<B> bs;
}
#Entity
public class B
{
#Id
private int id;
#ManyToMany(cascade = {CascadeType.PERSIST, CascadeType.MERGE, CascadeType.REFRESH})
private Collection<C> cs;
}
#Entity
public class C
{
#Id
private short id;
private Object someProperty;
#Override
public boolean equals(Object obj)
{
if (this == obj)
{
return true;
}
if (obj != null && getClass() == obj.getClass())
{
final C other = (C) obj;
return id == other.id;
}
return false;
}
#Override
public int hashCode()
{
final int prime = 31;
int result = 1;
result = 31 + id;
return result;
}
}
Now I need to persist these entities (to an empty DB). The entities are created as a result of parsing XML data, so it is not as simple as the following code, but in reality it is done like that (that is why I'm not reusing c1):
A a = new A;
a.setId(1);
Collection<B> bs = new ArrayList<B>();
B b1 = new B();
b1.setId(21);
Collection<C> cs1 = new ArrayList<C>();
C c1 = new C();
c1.setId(31);
c1.setOtherProperty(null);
cs1.add(c1);
b1.setCs(cs1);
B b2 = new B();
b2.setId(22);
Collection<C> cs2 = new ArrayList<C>();
C c2 = new C();
c2.setId(31); // notice this is the same id as c1
c2.setOtherProperty(null);
cs2.add(c2);
b2.setCs(cs2);
bs.add(b1);
bs.add(b2);
a.setBs(bs);
Now when I try to persist the newly created entity A via
entityManager.merge(a);
I get an exception
Unique index or primary key violation: "PRIMARY KEY ON PUBLIC.C(ID)"; SQL statement:
INSERT INTO PUBLIC.C (ID, SOMEPROPERTY) VALUES (?, ?) ...
In the SQL log, I can see that OpenJPA (which is the provider I'm using) is trying to insert a row with id 31 to table C two times. However, I would expect that c1 and c2 objects are treated as equal, and just one row is inserted. How can I possibly achieve that?
Related
Product product = new Product();
product.setName( "foo" );
product.setPrice(BigDecimal.valueOf( 4.5 ) );
pm.create( product ); // pm delegates calls to an entity manager object using persist method and tx is immediately commited after the call
List<Product> products = pm.findAllProducts();
products.stream().forEach( System.out::println ); // New product is listed too.
pm.create( product ); // Causes no exception! But, as per API, it should.
products = pm.findAllProducts(); // Fetch successful
products.stream().forEach( System.out::println ); // No difference from first print.
As per persistence API, if an entity alredy exists, persist(called from pm.create) throw's EntityExistsException, but its not happening as per code.
Pesistence provider(PP) - EclipseLink.
Why is PP ignoring repeat persist?
In what circumstances does a PP choose to throw an exception?
EDIT:
Product.java
NOTE:
Excluded getters and setters(for all fields) and toString() for brevity.
I tried my best to format code as per guidelines, but its not happening, please bear.
#Entity #Table(name = "PRODUCTS") #XmlRootElement #NamedQueries({
#NamedQuery(name = "Product.findAll", query = "SELECT p FROM Product p")
, #NamedQuery(name = "Product.findById", query = "SELECT p FROM Product p WHERE p.id = :id")
, #NamedQuery(name = "Product.findByName", query = "SELECT p FROM Product p WHERE p.name like :name")
, #NamedQuery(name = "Product.findByPrice", query = "SELECT p FROM Product p WHERE p.price = :price")
, #NamedQuery(name = "Product.findByBestBefore", query = "SELECT p FROM Product p WHERE p.bestBefore = :bestBefore")
, #NamedQuery(name = "Product.findByVersion", query = "SELECT p FROM Product p WHERE p.version = :version")
, #NamedQuery(name = "Product.findTotal", query = "SELECT count(p.id), sum(p.price) FROM Product p WHERE p.id in :ids" ) })
public class Product implements Serializable {
private static final long serialVersionUID = 1L;
#Id
#Basic(optional = false)
#NotNull
#SequenceGenerator( name="pidGen", sequenceName="PID_SEQ", allocationSize=1 )
#GeneratedValue( strategy=SEQUENCE, generator="pidGen" )
private Integer id;
#Basic(optional = false)
#NotNull
#Size(min = 3, max = 40, message="{prod.name}")
private String name;
// #Max(value=?) #Min(value=?)//if you know range of your decimal fields consider using these annotations to enforce field validation
#Basic(optional = false)
#NotNull
#Max( value=1000, message="{prod.price.max}")
#Min( value=1, message="{prod.price.min}")
private BigDecimal price;
#Column(name = "BEST_BEFORE")
#Temporal(TemporalType.DATE)
//private Date bestBefore;
private LocalDate bestBefore;
#Version
private Integer version;
public Product() {
}
public Product(Integer id) {
this.id = id;
}
public Product(Integer id, String name, BigDecimal price) {
this.id = id;
this.name = name;
this.price = price;
}
#Override
public int hashCode() {
int hash = 0;
hash += (id != null ? id.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 Product)) {
return false;
}
Product other = (Product) object;
if ((this.id == null && other.id != null) || (this.id != null && !this.id.equals(other.id))) {
return false;
}
return true;
}
}
As per the JPA Spec:
If X is a new entity, it becomes managed. The entity X will be entered into the database at or
before transaction commit or as a result of the flush operation.
If X is a preexisting managed entity, it is ignored by the persist operation (...)
If X is a detached object, the EntityExistsException may be thrown when the persist
operation is invoked, or the EntityExistsException or another PersistenceException may be thrown at flush or commit time
When you invoke EntityManager.persist(product), product becomes a managed entity (#1). Any subsequent calls to EntityManager.persist(product) are ignored, as described in #2. The final point applies only when you try to invoke persist() on a detached entity.
Native SQL with aliased field names + remapping to receive managed entities is required for more complex queries with joined tables.
However, the mapping of the SQL aliases leads to an exception where the aliased fields cannot be found. Can anybody detect an error in the code below, or is SQLResultSetMapping broken? (The sample below is intentionally simple to allow quick checking)
RDBMS H2, DDL
create table A(
ID INTEGER DEFAULT NOT NULL AUTO_INCREMENT PRIMARY KEY,
VAL VARCHAR(10)
);
insert into A (val) values ('val1');
insert into A (val) values ('val2');
Java class
#Entity
#NamedNativeQuery(name = "queryall",
query="select ID as AID, val from A",
resultSetMapping = "mapping")
#SqlResultSetMapping(name = "mapping",
entities = #EntityResult(
entityClass = A.class,
fields = {#FieldResult(name = "ID", column = "AID")})
)
public class A implements Serializable {
private static final long serialVersionUID = 1L;
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name = "ID")
private Integer id;
#Column(name = "VAL")
private String val;
public A() {
}
public A(Integer id) {
this.id = id;
}
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getVal() {
return val;
}
public void setVal(String val) {
this.val = val;
}
#Override
public String toString() {
return "entities.A[ id=" + id +", val="+val+ " ]";
}
public static void main(String[] args) {
EntityManagerFactory entityManagerFactory =
Persistence.createEntityManagerFactory("JavaApplication6PU");
EntityManager em = entityManagerFactory.createEntityManager();
Query sqlQuery = em.createNamedQuery("queryall");
List list = sqlQuery.getResultList();
for (Iterator<A> iterator = list.iterator(); iterator.hasNext();) {
a = iterator.next();
System.out.println(String.format("entity %s, managed: %s", a, em.contains(a)));
}
}
}
Execution stops with exception:
[EL Warning]: 2018-01-12 21:45:42.748--UnitOfWork(1823014131)--Exception
[EclipseLink-6044] (Eclipse Persistence Services - 2.5.2.v20140319-9ad6abd):
org.eclipse.persistence.exceptions.QueryException
Exception Description: The primary key read from the row [DatabaseRecord(
A.ID => null
A.VAL => val1)] during the execution of the query was detected to be null.
Primary keys must not contain null.
Query: ResultSetMappingQuery(name="queryall" referenceClass=A sql="select ID as AID, val from A")
This, in other words, means: No mapping has taken place -> aliased fields not found
The same when the mapping is announced in adhoc Queries.
Query sqlQuery = em.createNativeQuery("select ID as AID, val from A","mapping");
If resultClass is used instead of resultSetMapping and no SQL aliases exist, the output is as it should be. (This proves that there is no misspelling of fields or any other error)
#NamedNativeQuery(name = "queryall",
query="select ID, val from A",
resultClass = A.class)
Output:
entity entities.A[ id=1, val=val1 ], managed: true
entity entities.A[ id=2, val=val2 ], managed: true
i have two tables:
area (
id int PK autoincrement
code varchar
)
products (
id int PK autoincrement
area_id int
)
And the objets are defined like this:
class Product {
...
#JoinColumn(name = "area_id", referencedColumnName = "id")
#ManyToOne
#Expose
private Area area;
...
}
This works fine but I want that area to be a String with the code used in the table area column code.
class Product {
...
???
private String area;
...
}
What should be the annotations to make this work?
Thanks!
Try to use a combination of #SecondaryTable and #Column annotations. Something like this:
#Entity
#SecondaryTable(name="area", pkJoinColumns=#PrimaryKeyJoinColumn(name="id", referencedColumnName="area_id"))
class Product {
...
#Column(name="code", table = "area")
private String code;
...
}
If there is some poor soul with the same problem, here is how I did it:
Using transformers. So the field area is defined like this:
#Transformation(fetch = FetchType.EAGER, optional = false)
#ReadTransformer(transformerClass = AreaAttributeTransformer.class)
#WriteTransformers({
#WriteTransformer(
transformerClass = AreaFieldTransformer.class,
column = #Column(name = "area_id", nullable = false))
})
#Expose
private String area;
Then those clases work like this:
AreaAttributeTransformer
public class AreaAttributeTransformer implements AttributeTransformer {
private AbstractTransformationMapping mapping;
#Override
public void initialize(AbstractTransformationMapping abstractTransformationMapping) {
this.mapping = abstractTransformationMapping;
}
#Override
public Object buildAttributeValue(Record record, Object o, Session session) {
for (DatabaseField field : mapping.getFields()) {
if (field.getName().contains("area_id")) {
EntityManager em = MyEntityManagerFactory.getENTITY_MANAGER_FACTORY().createEntityManager();
List results = em.createNamedQuery("Areas.findById")
.setParameter("id", record.get(field))
.getResultList();
if (results.size() > 0)
return ((Area) results.get(0)).getCode();
}
}
return null;
}
}
AreaFieldTransformer
public class AreaFieldTransformer implements FieldTransformer {
private AbstractTransformationMapping mapping;
#Override
public void initialize(AbstractTransformationMapping abstractTransformationMapping) {
this.mapping = abstractTransformationMapping;
}
#Override
public Object buildFieldValue(Object o, String s, Session session) {
if (o instanceof RouSub) {
EntityManager em = MyEntityManagerFactory.getENTITY_MANAGER_FACTORY().createEntityManager();
List results = em.createNamedQuery("Area.findByCode")
.setParameter("area", ((Area) o).getCode())
.getResultList();
if (results.size() > 0)
return ((Area)results.get(0)).getId();
}
return null;
}
}
I have been trying to create a bidirectional #OneToMany relation with composite keys but some pieces are missing or wrong.
I have a draft entity which holds a list of sub draft entities.
Here is what I got:
#Entity
#Table(name = "draft")
#IdClass(Pk.class)
public class Draft {
#Id
#GeneratedValue(strategy=GenerationType.AUTO)
protected Long id;
#Id
protected Integer rev;
#OneToMany(cascade = CascadeType.ALL, fetch = FetchType.LAZY, mappedBy = "draft")
List<SubDraft> subDrafts = new ArrayList<SubDraft>();
// getters / setters omitted
}
#Entity
#Table(name = "sub_draft")
#IdClass(PK.class)
public class DraftToDoDAO {
#Id
#GeneratedValue(strategy=GenerationType.AUTO)
protected Long id;
#Id
protected Integer rev;
#ManyToOne(fetch = FetchType.LAZY)
#PrimaryKeyJoinColumns({
#PrimaryKeyJoinColumn(name = "draft_id", referencedColumnName = "id"),
#PrimaryKeyJoinColumn(name = "draft_rev", referencedColumnName = "rev")
})
protected DraftDAO draft;
// getters / setters omitted
}
public class PK implements Serializable {
protected Long id;
protected Integer rev;
public PK() {
}
public PK(Long id, Integer rev) {
this.id = id;
this.rev = rev;
}
public Long getId() {
return id;
}
public Integer getRev() {
return rev;
}
#Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((id == null) ? 0 : id.hashCode());
result = prime * result + ((rev == null) ? 0 : rev.hashCode());
return result;
}
#Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
MetaDataDAO other = (MetaDataDAO) obj;
if (id == null) {
if (other.id != null)
return false;
} else if (!id.equals(other.id))
return false;
if (rev == null) {
if (other.rev != null)
return false;
} else if (!rev.equals(other.rev))
return false;
return true;
}
}
I have no problems saving a draft with a list of sub drafts but the relation is not created both ways.
In the sub draft table the SchemaTool (DataNucleus) creates a column named draft_id but it is empty. And I wonder why it is not creating the columns I specified (draft_id, draft_rev) and settles the relation there.
I have search a lot for an answer but just can not get thing to work.
Your help is appreciated!
Thanks.
EDIT!
Here is the actual persistence code:
public Draft create(Draft draft, SubDraft subDraft) {
EntityManager em = PersistenceHelper.getEntityManager();
draft.setCreated(Calendar.getInstance());
// This should do it
draft.setSubDraft(subDraft);
subDraft.setDraft(draft);
em.persist(draft);
em.close();
return draft;
}
Some where along the line of code this subDraft attribute was nilled and therefore is did not work.
Given the following two tables:
CREATE TABLE `x` (
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
`name_hash` char(32) NOT NULL,
`access_time` bigint(20) unsigned NOT NULL,
`name` varchar(1024) NOT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `name_hash` (`name_hash`),
KEY `access_time` (`access_time`),
CONSTRAINT `x_ibfk_1` FOREIGN KEY (`access_time`) REFERENCES `update_time` (`id`) ON DELETE CASCADE ON UPDATE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
CREATE TABLE `y` (
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
`x` bigint(20) unsigned NOT NULL,
`update_time` bigint(20) unsigned NOT NULL,
`reason` bigint(20) unsigned NOT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `x` (`x`,`update_time`),
KEY `reason` (`reason`),
KEY `update_time` (`update_time`),
CONSTRAINT `y_ibfk_1` FOREIGN KEY (`reason`) REFERENCES `reason` (`id`) ON DELETE CASCADE ON UPDATE CASCADE,
CONSTRAINT `y_ibfk_2` FOREIGN KEY (`x`) REFERENCES `x` (`id`) ON DELETE CASCADE ON UPDATE CASCADE,
CONSTRAINT `y_ibfk_3` FOREIGN KEY (`update_time`) REFERENCES `update_time` (`id`) ON DELETE CASCADE ON UPDATE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
I used NetBeans to create the following JPA classes (X and Y are not the real names, think I did all of the required changes):
#Entity
#Table(name = "X", catalog = "topiclymobile", schema = "", uniqueConstraints = {
#UniqueConstraint(columnNames = {"name_hash"})})
#NamedQueries({
#NamedQuery(name = "X.findAll", query = "SELECT t FROM X t"),
#NamedQuery(name = "X.findById", query = "SELECT t FROM X t WHERE t.id = :id"),
#NamedQuery(name = "X.findByNameHash", query = "SELECT t FROM X t WHERE t.nameHash = :nameHash"),
#NamedQuery(name = "X.findByName", query = "SELECT t FROM X t WHERE t.name = :name")})
public class X implements Serializable
{
private static final long serialVersionUID = 1L;
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Basic(optional = false)
#Column(name = "id", nullable = false)
private Long id;
#Basic(optional = false)
#Column(name = "name_hash", nullable = false, length = 32)
private String nameHash;
#Basic(optional = false)
#Column(name = "name", nullable = false, length = 1024)
private String name;
#JoinColumn(name = "access_time", referencedColumnName = "id", nullable = false)
#ManyToOne(optional = false)
private UpdateTime accessTime;
#OneToMany(cascade = CascadeType.ALL, mappedBy = "X")
private List<Y> YList;
public X() {
}
public X(Long id) {
this.id = id;
}
public X(Long id, String nameHash, String name) {
this.id = id;
this.nameHash = nameHash;
this.name = name;
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getNameHash() {
return nameHash;
}
public void setNameHash(String nameHash) {
this.nameHash = nameHash;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public UpdateTime getAccessTime() {
return accessTime;
}
public void setAccessTime(UpdateTime accessTime) {
this.accessTime = accessTime;
}
public List<Y> getYList() {
return YList;
}
public void setYList(List<Y> YList) {
this.YList = YList;
}
#Override
public int hashCode() {
int hash = 5;
hash = 89 * hash + (this.nameHash != null ? this.nameHash.hashCode() : 0);
return hash;
}
#Override
public boolean equals(Object obj) {
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
final X other = (X) obj;
if ((this.nameHash == null) ? (other.nameHash != null) : !this.nameHash.equals(other.nameHash)) {
return false;
}
return true;
}
}
#Entity
#Table(name = "Y", catalog = "topiclymobile", schema = "", uniqueConstraints = {
#UniqueConstraint(columnNames = {"X", "update_time"})})
#NamedQueries({
#NamedQuery(name = "Y.findAll", query = "SELECT t FROM Y t"),
#NamedQuery(name = "Y.findById", query = "SELECT t FROM Y t WHERE t.id = :id")})
public class Y implements Serializable
{
private static final long serialVersionUID = 1L;
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Basic(optional = false)
#Column(name = "id", nullable = false)
private Long id;
#JoinColumn(name = "reason", referencedColumnName = "id", nullable = false)
#ManyToOne(optional = false)
private Reason reason;
#JoinColumn(name = "X", referencedColumnName = "id", nullable = false)
#ManyToOne(optional = false)
private X X;
#JoinColumn(name = "update_time", referencedColumnName = "id", nullable = false)
#ManyToOne(optional = false)
private UpdateTime updateTime;
public Y() {
}
public Y(Long id) {
this.id = id;
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public Reason getReason() {
return reason;
}
public void setReason(Reason reason) {
this.reason = reason;
}
public X getX() {
return X;
}
public void setX(X X) {
this.X = X;
}
public UpdateTime getUpdateTime() {
return updateTime;
}
public void setUpdateTime(UpdateTime updateTime) {
this.updateTime = updateTime;
}
#Override
public int hashCode() {
int hash = 7;
hash = 13 * hash + (this.X != null ? this.X.hashCode() : 0);
hash = 13 * hash + (this.updateTime != null ? this.updateTime.hashCode() : 0);
return hash;
}
#Override
public boolean equals(Object obj) {
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
final Y other = (Y) obj;
if (this.X != other.X && (this.X == null || !this.X.equals(other.X))) {
return false;
}
if (this.updateTime != other.updateTime && (this.updateTime == null || !this.updateTime.equals(other.updateTime))) {
return false;
}
return true;
}
}
What I am after is all of the cases that "x" does not have a corresponding "y" for a given time (access_time and update_time are the same thing).
This SQL query works, I just cannot seem to translate it into an JPA query:
SELECT t.id FROM x t LEFT JOIN y r ON t.id = r.x WHERE r.x IS NULL AND t.access_time = 1
It'd be helpful to see your entity classes to construct the actual query, but JPA does support LEFT JOINs. This blog post has a full example, as does this question, but something like
SELECT x FROM X x LEFT JOIN x.y ...
I'm not sure what the rest of the query should be as what you posted does not look like valid SQL (you have WHERE r.x IS NULL, but the schema given defines x on table y as NOT NULL; similarly, having WHERE r.x IS NULL ought to make your left join match nothing, since t.id = r.x would always evaluate to NULL).
EDIT: I'm still confused as to how your sample SQL is a valid query, but something like this seems like it ought to translate into the SQL you provided:
SELECT x FROM X x LEFT JOIN x.yList y where y.x IS NULL and x.accessTime = :accessTime
Where the :accessTime parameter is the value of entityManager.getReference(UpdateTime.class, 1).
Again, though, the FROM x LEFT JOIN y on x.id = y.x WHERE y.x IS NULL should match precisely no rows in Y, whereas (since it's a LEFT JOIN), it will include all the rows in X. In other words, I think your query is equivalent to:
SELECT x.id FROM X where x.access_time = 1
Which would be this in JPA:
SELECT x FROM X x where x.accessTime = :accessTime