I have two Entities OrgType and OrgField:
#Entity(name = "T_ORG_FIELD")
public class OrgField extends Model {
#MinSize(value = 2)
#Column(nullable = false)
public String name;
#ManyToOne
public OrgType orgType;
...
}
#Entity(name = "T_ORG_TYPE")
public class OrgType extends Model {
#Column(nullable = false, unique = true)
public String name;
#OneToMany(mappedBy = "orgType")
public List<OrgField> orgFields = new ArrayList<OrgField>();
...
}
Now, I'm writing unit test for them:
public class OrganizationTest extends UnitTest {
#Test
public void saveOrRemoveOrg() {
OrgType orgType = new OrgType("org type", "description");
orgType.save();
OrgField field = new OrgField();
field.name = "field1";
field.orgType = orgType;
field.save();
Model.em().flush();
System.out.println(OrgField.count("name = ?", "field1")); // Output : 1
int size = orgType.orgFields.size();
assertEquals(1, size); // Error , expect 1 but get 0.
...
I created a new OrgField and update its reference of orgType, and expected to have orgType.orgFields automatically be filled, but it didn't.
Any help ?
Try to fill fetch attribute as below:
#ManyToOne(fetch=FetchType.EAGER)
#OneToMany(mappedBy="orgType", fetch = FetchType.EAGER)
FecthType.EAGER will fetch it as you desire. You can define different values for each class, if you like. Instead, you can you FetchType.LAZY, it is a good practive to explicity set this attribute instead of leaving the default value to be used.
Moreover, you can use the attribute cascade with CascadeType.ALL to persist, etc. automatically as well, or check CascadeType enum to see other values. It is also a good practice to explicity define the value of cascade attribute.
Related
My application operates in a weird way. In fact when debugging I can clearly see that my objects get persisted on the DB but when in running mode, JPA does not seem to persist them. The following is a code snippet from my source code :
#Entity
#Table(name = "a", schema="myschema")
public class A implements Serializable{
#Id
#Column(name = "id", unique = true, nullable = false)
#NotNull(message = "id can not be null")
private UUID id = UUID.randomUUID();
#JsonIgnore
// #ManyToMany(fetch = FetchType.EAGER)
#ManyToMany
#JoinTable(name = "a_b", joinColumns = { #JoinColumn(name = "a_id") },
inverseJoinColumns = { #JoinColumn(name = "b_id") }
)
private List<b> blist = new ArrayList<>();
//omitted source code
}
#Entity
#Table(name = "b", schema="myschema")
public class B implements Serializable{
#Id
#Column(name = "id", unique = true, nullable = false)
#NotNull(message = "id can not be null")
private Integer id;
#ManyToMany(mappedBy = "blist")
#JsonIgnore
private List<A> alist = new ArrayList<>();
//omitted source code
}
#Service
public class MyService{
//omitted source code
public Optional<A> createCopy(A source, int bId) {
B b = bRepository.findById(bId);
A copy_ = this.copy(source);
A target = aRepository.save(copy_);
b.getAlist().add(target);
bRepository.save(b);
return Optional.of(target);
}
private A copy(A source){
A target = new A();
//copy one to one from source to target
target.setB(source.getB());
return target;
}
}
When debugging I can see that after making a call to MyService#createCopy() method, a new record is persisted in the DB within the table a_b. However when I simply run the server and then proceeds with a call to MyService#createCopy(), no additional record in a_b gets persisted.
Anyone ever encountered such an odd behavior before? And if yes how can one solve it please?
It's look like you have not set your new list again to b objet in your service, That's why you are not able to get records in within the table a_b. Change your service as below.
#Service
public class MyService{
//omitted source code
public Optional<A> createCopy(A source, int bId) {
B b = bRepository.findById(bId);
A copy_ = this.copy(source);
A target = aRepository.save(copy_);
List<A> alist= b.getAlist();// Here b.getAlist() will return a independent list and adding any item within this list will not affect the list inside the object b.
alist.add(target);
b.setAlist(alist);// setting the new list to object b.
bRepository.save(b);
return Optional.of(target);
}
Try it once and let me know if it works.
I managed to fix the problem as following (Thanks to #Ajit hint):
private A copy(A source){
A target = new A();
//copy one to one from source to target
//instead of target.setB(source.getB()); I did this
List<B> targetBlist = target.getBlist();
source.getBlist().forEach(bObj -> {
Optional<B> optB = bRepository.findById(bObj.getId());
if(optB.isPresent())
targetBlist.add(optB.get());
});
return target;
}
Now that my problem is fixed I still cannot figure out what was the actual issue. It smells like this has to do something with lazy loading. In fact the only case where the problem does not occur is when I do inspect the source#bList before proceeding to the copy(). If anyone could clarify this to me I will be thankful.
Hibernate 4.3.11
I have an issue saving the following object graph in hibernate. The Employer is being saved using the merge() method.
Employer
|_ List<EmployerProducts> employerProductsList;
|_ List<EmployerProductsPlan> employerProductsPlan;
The Employer & EmployerProducts have a auto generated pk. The EmployerProductsPlan is a composite key consisting of the EmployerProducts id and a String with the plan code.
The error occurs when there is a transient object in the EmployerProducts list that cascades to List<EmployerProductsPlan>. The 1st error that I encountered which I have been trying to get past was an internal hibernate NPE. This post here perfectly describes the issue that I am having which causes the null pointer Hibernate NullPointer on INSERTED id when persisting three levels using #Embeddable and cascade
The OP left a comment specifying what they did to resolve, but I end up with a different error when changing to the suggested mapping. After changing the mapping, I am now getting
org.hibernate.NonUniqueObjectException: A different object with the same identifier value was already associated with the session : [com.webexchange.model.EmployerProductsPlan#com.webexchange.model.EmployerProductsPlanId#c733f9bd]
Due to other library dependencies, I cannot upgrade above 4.3.x at this time. This project is using spring-boot-starter-data-jpa 1.3.3. No other work is being performed on the session other than calling merge() and passing the employer object.
Below is the mappings for each class:
Employer
#Entity
#Table(name = "employer")
#lombok.Getter
#lombok.Setter
#lombok.EqualsAndHashCode(of = {"employerNo"})
public class Employer implements java.io.Serializable {
#Id
#GeneratedValue(strategy = IDENTITY)
#Column(name = "EMPLOYER_NO", unique = true, nullable = false)
private Long employerNo;
.....
#OneToMany(cascade = CascadeType.ALL, fetch = FetchType.LAZY, mappedBy = "employer", orphanRemoval = true)
private List<EmployerProducts> employerProductsList = new ArrayList<>(0);
}
EmployerProducts
#Entity
#Table(name = "employer_products")
#Accessors(chain = true) // has to come before #Getter and #Setter
#lombok.Getter
#lombok.Setter
#lombok.EqualsAndHashCode(of = {"employerProductsNo"})
public class EmployerProducts implements Serializable {
#Id
#GeneratedValue(strategy = IDENTITY)
#Column(name = "employer_products_no", unique = true, nullable = false)
private Long employerProductsNo;
#ManyToOne
#JoinColumn(name = "employer_no", nullable = false)
private Employer employer;
......
#OneToMany(cascade = CascadeType.ALL, fetch = FetchType.LAZY, mappedBy = "employerProducts", orphanRemoval = true)
private List<EmployerProductsPlan> employerProductsPlanList = new ArrayList<>(0);
}
EmployerProductsPlan
#Accessors(chain = true) // has to come before #Getter and #Setter
#lombok.Getter
#lombok.Setter
#lombok.EqualsAndHashCode(of = {"id"})
#Entity
#Table(name="employer_products_plan")
public class EmployerProductsPlan implements Serializable {
#EmbeddedId
#AttributeOverrides({ #AttributeOverride(name = "plan", column = #Column(name = "epp_plan", nullable = false)),
#AttributeOverride(name = "employerProductsNo", column = #Column(name = "employer_products_no", nullable = false)) })
private EmployerProductsPlanId id;
#ManyToOne
#JoinColumn(name = "employer_products_no")
#MapsId("employerProductsNo")
private EmployerProducts employerProducts;
}
I am populating the employerProducts above with the same instance of the EmployerProducts object that is being saved. It is transient and has no id populated as it does not existing in the db yet.
EmployerProductsPlanId
#Accessors(chain = true) // has to come before #Getter and #Setter
#lombok.Getter
#lombok.Setter
#lombok.EqualsAndHashCode(of = {"plan", "employerProductsNo"})
#Embeddable
public class EmployerProductsPlanId implements Serializable {
private String plan;
private Long employerProductsNo;
// This was my previous mapping that was causing the internal NPE in hibernate
/* #ManyToOne
#JoinColumn(name = "employer_products_no")
private EmployerProducts employerProducts;*/
}
UPDATE:
Showing struts controller and dao. The Employer object is never loaded from the db prior to the save. Struts is creating this entire object graph from the Http request parameters.
Struts 2.5 controller
#lombok.Getter
#lombok.Setter
public class EditEmployers extends ActionHelper implements Preparable {
#Autowired
#lombok.Getter(AccessLevel.NONE)
#lombok.Setter(AccessLevel.NONE)
private IEmployerDao employerDao;
private Employer entity;
....
public String save() {
beforeSave();
boolean newRecord = getEntity().getEmployerNo() == null || getEntity().getEmployerNo() == 0;
Employer savedEmployer = newRecord ?
employerDao.create(getEntity()) :
employerDao.update(getEntity());
setEntity(savedEmployer);
return "success";
}
private void beforeSave() {
Employer emp = getEntity();
// associate this employer record with any products attached
for (EmployerProducts employerProduct : emp.getEmployerProductsList()) {
employerProduct.setEmployer(emp);
employerProduct.getEmployerProductsPlanList().forEach(x ->
x.setEmployerProducts(employerProduct));
}
// check to see if branding needs to be NULL. It will create the object from the select parameter with no id
// if a branding record has not been selected
if (emp.getBranding() != null && emp.getBranding().getBrandingNo() == null) {
emp.setBranding(null);
}
}
}
Employer DAO
#Repository
#Transactional
#Service
#Log4j
public class EmployerDao extends WebexchangeBaseDao implements IEmployerDao {
private Criteria criteria() {
return getCurrentSession().createCriteria(Employer.class);
}
#Override
#Transactional(readOnly = true)
public Employer read(Serializable id) {
return (Employer)getCurrentSession().load(Employer.class, id);
}
#Override
public Employer create(Employer employer) {
getCurrentSession().persist(employer);
return employer;
}
#Override
public Employer update(Employer employer) {
getCurrentSession().merge(employer);
return employer;
}
}
As of right now, my solution is to loop through the EmployerProducts and check for new records. I called a persist on the new ones before calling the merge() on the parent Employer. I also moved the logic I had associating all the keys into the dao instead of having it in my Struts action. Below is what my update() method in the Employer DAO now looks like
public Employer update(Employer employer) {
// associate this employer record with any products attached
for (EmployerProducts employerProduct : employer.getEmployerProductsList()) {
employerProduct.setEmployer(employer);
if (employerProduct.getEmployerProductsNo() == null) {
// The cascade down to employerProductsPlanList has issues getting the employerProductsNo
// automatically if the employerProduct does not exists yet. Persist the new employer product
// before we try to insert the new composite key in the plan
// https://stackoverflow.com/questions/54517061/hibernate-4-3-cascade-merge-through-multiple-lists-with-embeded-id
List<EmployerProductsPlan> plansToBeSaved = employerProduct.getEmployerProductsPlanList();
employerProduct.setEmployerProductsPlanList(new ArrayList<>());
getCurrentSession().persist(employerProduct);
// add the plans back in
employerProduct.setEmployerProductsPlanList(plansToBeSaved);
}
// associate the plan with the employer product
employerProduct.getEmployerProductsPlanList().forEach(x ->
x.getId().setEmployerProductsNo(employerProduct.getEmployerProductsNo())
);
}
return (Employer)getCurrentSession().merge(employer);
}
I'm trying to use JPA (with Hibernate) to save 2 entities. Spring data is providing the interface but I don't think it matters here.
I have a main entity called 'Model'. This model has many 'Parameter' entities linked. I'm writing a method to save a model and its parameters.
This is the method:
private void cascadeSave(Model model) {
modelRepository.save(model);
for (ParameterValue value : model.getParameterValues()) {
parameterValueRepository.save(value);
}
}
This is the problem:
When I load a Model that already existed before, add some new parameters to it and then call this method to save both of them something strange happens:
Before the first save (modelRepository.save) this is what the model's data looks like when debugging:
The model has 2 parameters, with filled in values (name and model are filled).
Now, after saving the model the first save in my method, this happens. Note that the object reference is a different one so Hibernate must have done something magical and recreated the values instead of leaving them alone:
For some reason hibernate cleared all the attributes of the parameters in the set.
Now when the saving of the new parameters happens in the following code it fails because of not null constraints etc.
My question: Why does hibernate clear all of the fields?
Here are the relevant mappings:
ParameterValue
#Entity
#Table(name = "tbl_parameter_value")
#Inheritance(strategy = InheritanceType.SINGLE_TABLE)
#DiscriminatorColumn(name = "PARAMETER_TYPE")
public abstract class ParameterValue extends AbstractBaseObject {
#Column(nullable = false)
#NotBlank
private String name;
private String stringValue;
private Double doubleValue;
private Integer intValue;
private Boolean booleanValue;
#Enumerated(EnumType.STRING)
private ModelType modelParameterType;
#Column(precision = 7, scale = 6)
private BigDecimal bigDecimalValue;
#Lob
private byte[] blobValue;
ParameterValue() {
}
ParameterValue(String name) {
this.name = name;
}
ModelParameterValue
#Entity
#DiscriminatorValue(value = "MODEL")
public class ModelParameterValue extends ParameterValue {
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "model_id", foreignKey = #ForeignKey(name = "FK_VALUE_MODEL"))
private Model model;
ModelParameterValue() {
super();
}
ModelParameterValue(String name) {
super(name);
}
Model
#Entity
#Table(name = "tbl_model")
public class Model extends AbstractBaseObject implements Auditable {
#OneToMany(fetch = FetchType.LAZY, mappedBy = "model")
private Set<ModelParameterValue> parameterValues = new HashSet<>();
EDIT
I was able to reproduce this with a minimal example.
If you replace everything spring data does this is what happened under the hood (em is a JPA EntityManager):
public Model simpleTest() {
Model model = new Model("My Test Model");
em.persist(model);
model.addParameter(new Parameter("Param 1"));
em.merge(model);
for (Parameter child : model.getParameters()) {
em.persist(child);
}
return model;
}
When the merge is executed, all of the attributes of the parameters are set to null. They are actually just replaced with completely new parameters.
I guess you are using Spring Data Jpa as your modelRepository. This indicates following consequences.
Spring Repository Save
S save(S entity)
Saves a given entity. Use the returned
instance for further operations as the save operation might have
changed the entity instance completely.
So it is normal behaviour which you had encountered.
Code should be changed to :
model = modelRepository.save(model);
for (ParameterValue value : model.getParameterValues()) {
parameterValueRepository.save(value);
}
EDIT:
I think that your saving function is broken in sense, that you do it backwards. Either you can use CascadeType on your relation or you have to save children first.
Cascade
Cascade works like that "If you save Parent, save Children, if you update Parent, update Children ..."
So we can put cascade on your relation like that :
#Entity
#Table(name = "tbl_model")
public class Model extends AbstractBaseObject implements Auditable {
#OneToMany(fetch = FetchType.LAZY, mappedBy = "model", cascade = CascadeType.ALL)
private Set<ModelParameterValue> parameterValues = new HashSet<>();
and then only save like this
private void cascadeSave(Model model) {
modelRepository.save(model);
//ParamValues will be saved/updated automaticlly if your model has changed
}
Bottom-Up save
Second option is just to save params first and then model with them.
private void cascadeSave(Model model) {
model.setParameterValues(
model.getParameterValues().stream()
.map(param -> parameterValueRepository.save(param))
.collect(Collectors.toSet())
);
modelRepository.save(model);
}
I haven't checked second code in my compiler but the idea is to first save children (ParamValues), put it into Model and then save Model : )
I'm using hibernate 4.0 with jpa and I've a one to many relationship that can load lots of data from database and I set it to lazy load (as the code bellow)
To keep the historic, i never remove the B from database when i want to delete it I simple set the "closed" attribute to true...
The problem is if i try to load all the A instances using:
session.createCriteria(A.class).list();
for each instance hibernate will lazy load the B what are markeds as closed. I would like to know if there are any annotation where i can define to only loads those with "closed" as false.
Avoiding to specify it at every code I use to load A
public class A {
#Id
private Integer id;
private String fullName;
#OneToMany(cascade = CascadeType.ALL, orphanRemoval = true, targetEntity = B.class)
#LazyCollection(LazyCollectionOption.TRUE)
private List<B> series = new Vector<B>();
}
public class B {
#Id
private Integer id;
private Boolean closed;
private Date createdDate;
/**lots of other things**/
}
I found the answer: http://www.mkyong.com/hibernate/hibernate-data-filter-example-xml-and-annotation/
by default jpa doenst allow it, but hibernate provide an annotation for this case, the solution was:
public class A {
#Id
private Integer id;
private String fullName;
#OneToMany(cascade = CascadeType.ALL, orphanRemoval = true, targetEntity = B.class)
#LazyCollection(LazyCollectionOption.TRUE)
#Filter(name="bNotClosed")
private List<B> series = new Vector<B>();
}
#FilterDef(name="bNotClosed", defaultCondition="closed= :value", parameters=
#ParamDef(name="value",type="boolean"))
public class B {
#Id
private Integer id;
private Boolean closed;
private Date createdDate;
/**lots of other things**/
}
session = HibernateUtils.getSessionFactory().openSession();
session.enableFilter("bNotClosed").setParameter("value", false);
if my parameters was an integer i could put it literally in defaultCondition="unliked = 123", therefore hibernate was interpreting false as a class attribute and going to error so i must define the value at session creation.
I have created some Hibernate mappings with Hibernate 4.3.8.
#Entity
#Table(name = ErrorEntity.TABLE_ID)
#XmlRootElement(name = ErrorEntity.XML_ROOT_TAG)
public class ErrorEntity {
/**
*
*/
private static final long serialVersionUID = 8083918635458543738L;
public static final String TABLE_ID = "Error";
public static final String ERRORCODE = "error_code";
public static final String ENV_ID = "envid";
private Integer error_code;
private Integer envId;
private EnvironmentEntity environment;
public ErrorEntity() {
}
#Id
#Column(name = ErrorEntity.ERRORCODE)
public Integer getError_code() {
return error_code;
}
public void setError_code(Integer errorcode) {
this.error_code = errorcode;
}
#Column(name = ErrorEntity.ENV_ID)
public Integer getEnvId() {
return envId;
}
public void setEnvId(Integer envId) {
this.envId = envId;
}
#ManyToOne
#JoinColumn(name = ErrorEntity.ENV_ID, referencedColumnName = EnvironmentEntity.ENV_ID, insertable = false, updatable = false)
public EnvironmentEntity getEnvironment() {
return environment;
}
public void setEnvironment(EnvironmentEntity environment) {
this.environment = environment;
}
}
As you can see the mapping property ENV_ID is mapped twice.
Thisway I thought I would be able to set the JoinColumn value without querying the database to get the mapped object because I have the JoinColumn value at this point.
The value of ENV_ID is written correctly to the database but if I query this ErrorEntity later and try to get the EnvironmentEntity the reference is null.
ErrorEntity error = (ErrorEntity) criteria.uniqueResult();
System.out.println(error.getEnvironment().getName());
getEnvironment() returns null.
Any ideas how to achieve this?
Edit
It was working like expected to create a new object with the PK set.
Now I have a special situation where it does not work.
I need to reference another object where the joincolumn is not the PK. I know that the value i will join on is unique but there are also some duplicate values i will not join on.
However Hibernate seems to be unable to map this relationship automatically.
ErrorEntity error = new ErrorEntity();
SignalEntity signal = new SignalEntity();
signal.setName(signalName);
error.setSignal(signal);
The problem is that I do not have the signalID (PK) in that situation. The other idea would be to query the db but thats too slow.
I tried to create an composite PK with 3 columns but this breaks the logic at another place.
Is it possible to create two independent PK's?
The ErrorEntity has two ErrorEntity.ENV_ID mappings, which unless you use #MapsId then it's a configuration issue.
You should have an env_id column in EnvironmentEntity table and just the:
#ManyToOne
#JoinColumn(name = ErrorEntity.ENV_ID, referencedColumnName = EnvironmentEntity.ENV_ID, insertable = false, updatable = false)
public EnvironmentEntity getEnvironment() {
return environment;
}
mapping in ErrorEntity.
My suggestion is to remove this:
#Column(name = ErrorEntity.ENV_ID)
public Integer getEnvId() {
return envId;
}
To set the envId directly without querying the database and request the whole EnvironmentEntity, you can do something like this:
errrorEntity.setEnvironment(new EnvironmentEntity());
errrorEntity.getEnvironment().setEnvId(envId);
This is not a JPA standard requirement but Hibernate supports it.