I'm having an issue with converting an application to separate REST Server + Web Application rather than having a single application that handles pages + business logic.
I'm having a problem with an org.hibernate.LazyInitializationException being thrown when Jackson is serialising the output.
Below is my JPA configuration and the class in question. I'm aware the lazy initialisation is being caused by the List member, but I don't want this to be populated in the JSON returned. Hence the #JsonIgnore annotation.
PersistenceConfiguration.java
#Configuration
#EnableTransactionManagement
#EnableJpaRepositories(basePackageClasses = {ForumRepository.class})
public class PersistenceConfiguration {
#Bean
public EntityManagerFactory entityManagerFactory() {
HibernateJpaVendorAdapter vendorAdapter = new HibernateJpaVendorAdapter();
LocalContainerEntityManagerFactoryBean factory = new LocalContainerEntityManagerFactoryBean();
factory.setJpaVendorAdapter(vendorAdapter);
factory.setPackagesToScan("com.mypackage.forum.api.models");
factory.setDataSource(secureDataSource());
factory.afterPropertiesSet();
return factory.getObject();
}
#Bean
public JpaTransactionManager transactionManager() {
EntityManagerFactory factory = entityManagerFactory();
return new JpaTransactionManager(factory);
}
#Bean
public HibernateExceptionTranslator exceptionTranslator() {
return new HibernateExceptionTranslator();
}
#Bean
public DataSource secureDataSource() {
try {
Context ctx = new InitialContext();
return (DataSource) ctx.lookup("java:jboss/datasources/ForumDS");
} catch (Exception e) {
return null;
}
}
}
ForumGroup.java
#Entity
public class ForumGroup {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
#JsonIgnore
#OneToMany(mappedBy = "group")
private List<Forum> forums;
private String name;
private String description;
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public List<Forum> getForums() {
return forums;
}
public void setForums(List<Forum> forums) {
this.forums = forums;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getDescription() {
return description;
}
public void setDescription(String description) {
this.description = description;
}
#Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
ForumGroup that = (ForumGroup) o;
if (id != null ? !id.equals(that.id) : that.id != null) return false;
if (name != null ? !name.equals(that.name) : that.name != null) return false;
return true;
}
#Override
public int hashCode() {
int result = id != null ? id.hashCode() : 0;
result = 31 * result + (name != null ? name.hashCode() : 0);
return result;
}
#Override
public String toString() {
return "ForumGroup{" +
"id=" + id +
", name='" + name + '\'' +
", description='" + description + '\'' +
'}';
}
}
I presume that I need to configure some sort of Session Manager but not sure the best way to go about doing it in JavaConfig, I've tried various methods via Google with no joy.
Any help would be much appreciated.
Update
I've now managed to resolve this by 1) Adding the #Fetch() annotation to the relevant collections, which means that the collections get populated 2) I have stopped Entities/Models being returned as JSON objects and instead use separate DTOs
org.hibernate.LazyInitializationException happens when call lazy fields outside from session.
In your situation , it's seams you select ForumGroup entities and then, outside from session you trying call entity.getForums(). That's why happening LazyInitializationException.
If you need that collection, then load them eagerly. You can do that by calling:
entity.getForums().size();.Remember, call this inside the session
You can use anotation, to to load collection EAGERLY by default : #ManyToOne(fetch = FetchType.EAGER)
You just need to fetch the ForumGroup along with its forums list.
This can be achieved with a LEFT JOIN FETCH whenever you retrieve the ForumGroup you are interested in:
"from ForumGroup as fg " +
"left join fetch fg.forums "
You can assign a default fetching strategy, but this is a decision that will affect subsequent queries, so make sure you know the trade-offs:
Use a join fetch mode:
#JsonIgnore
#OneToMany(mappedBy = "group")
#Fetch(FetchMode.JOIN)
private List<Forum> forums = new ArrayList<Forum>();
Or a sub-select fetch mode:
#JsonIgnore
#OneToMany(mappedBy = "group")
#Fetch(FetchMode.SUBSELECT)
private List<Forum> forums = new ArrayList<Forum>();
And always initialize your collections, never let them assigned to null.
Related
I am trying to use the JPA Criteria API to filter the results and aggregate them using simple count, min, avg and max. I am using Spring Boot 2.7.8, so I am trying to use Interface-projections such that these aggregated results look the same as the simpler queries done automatically by the Spring repositories.
My domain entity (simplified for brevity) looks like this:
#Entity
#Table(name = "vehicle_stopped")
#IdClass(VehicleStopped.VehicleStoppedPK.class)
public class VehicleStopped implements Serializable {
#Id
#Column(name = "stopped_session_uuid", nullable = false)
private String stoppedSessionUuid;
#Id
#Column(name = "start_ts", nullable = false)
private OffsetDateTime startTs;
#Column(name = "end_ts", nullable = false)
private OffsetDateTime endTs;
#Column(name = "duration_seconds")
private Double durationSeconds;
#ManyToOne(fetch = FetchType.EAGER)
#JoinColumn(name = "zone_id")
private CameraZone cameraZone;
#Override
public VehicleStoppedPK getId() {
VehicleStopped.VehicleStoppedPK pk = new VehicleStopped.VehicleStoppedPK();
pk.setStartTs(this.getStartTs());
pk.setStoppedSessionUuid(this.getStoppedSessionUuid());
return pk;
}
public OffsetDateTime getEndTs() {
return endTs;
}
public void setEndTs(OffsetDateTime endTs) {
this.endTs = endTs;
}
public Double getDurationSeconds() {
return durationSeconds;
}
public void setDurationSeconds(Double durationSeconds) {
this.durationSeconds = durationSeconds;
}
public CameraZone getCameraZone() {
return cameraZone;
}
public void setCameraZone(CameraZone cameraZone) {
this.cameraZone = cameraZone;
}
public VehicleType getVehicleType() {
return vehicleType;
}
public void setVehicleType(VehicleType vehicleType) {
this.vehicleType = vehicleType;
}
public String getStoppedSessionUuid() {
return stoppedSessionUuid;
}
public void setStoppedSessionUuid(String stoppedSessionUuid) {
this.stoppedSessionUuid = stoppedSessionUuid;
}
//some details removed for brevity
#Override
public static class VehicleStoppedPK implements Serializable {
private OffsetDateTime startTs;
private String stoppedSessionUuid;
public VehicleStoppedPK() {
}
public OffsetDateTime getStartTs() {
return startTs;
}
public void setStartTs(OffsetDateTime startTs) {
this.startTs = startTs;
}
public String getStoppedSessionUuid() {
return stoppedSessionUuid;
}
public void setStoppedSessionUuid(String stoppedSessionUuid) {
this.stoppedSessionUuid = stoppedSessionUuid;
}
#Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
VehicleStoppedPK that = (VehicleStoppedPK) o;
return Objects.equals(startTs, that.startTs) && Objects.equals(stoppedSessionUuid, that.stoppedSessionUuid);
}
#Override
public int hashCode() {
return Objects.hash(startTs, stoppedSessionUuid);
}
#Override
public String toString() {
return "VehicleStoppedPK{" +
"startTs=" + startTs +
", stoppedSessionUuid='" + stoppedSessionUuid + '\'' +
'}';
}
}
}
#Entity
#Table(name = "camera_zone")
public class CameraZone implements Serializable {
#Id
#SequenceGenerator(name = "camera_zone_id_seq", sequenceName = "camera_zone_id_seq", allocationSize = 1)
#GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "camera_zone_id_seq")
#Column(name = "id", updatable=false)
private Integer id;
#Column(name = "uuid", unique = true)
private String uuid;
#Column(name = "type")
private String type;
#Column(name = "name")
private String name;
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getUuid() {
return uuid;
}
public void setUuid(String uuid) {
this.uuid = uuid;
}
public String getType() {
return type;
}
public void setType(String type) {
this.type = type;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
#Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
CameraZone that = (CameraZone) o;
return Objects.equals(id, that.id) && Objects.equals(uuid, that.uuid) && Objects.equals(camera, that.camera) && Objects.equals(type, that.type) && Objects.equals(name, that.name);
}
#Override
public int hashCode() {
return Objects.hash(id, uuid, camera, type, name);
}
}
The code that I have in my Repository implementation looks like this:
public class SpecificationVehicleStoppedRepositoryImpl
implements SpecificationVehicleStoppedRepository {
#Autowired private EntityManager em;
#Autowired ProjectionFactory projectionFactory;
#Override
public List<VehicleStoppedAggregate> getStoppedVehiclesCount(Specification<VehicleStopped> spec) {
CriteriaBuilder builder = em.getCriteriaBuilder();
CriteriaQuery<Tuple> query = builder.createTupleQuery();
Root<VehicleStopped> root = query.from(VehicleStopped.class);
Predicate predicate = spec.toPredicate(root, query, builder);
if (predicate != null) {
query.where(predicate);
}
Path<Number> duration = root.get("durationSeconds");
Path<CameraZone> zone = root.get("cameraZone");
query
.multiselect(zone,
builder.count(root).alias("totalVehicles"),
builder.min(duration).alias("minDuration"),
builder.avg(duration).alias("avgDuration"),
builder.max(duration).alias("maxDuration"))
.groupBy(zone);
List<Tuple> rawResultList = em.createQuery(query).getResultList();
return project(rawResultList, VehicleStoppedAggregate.class);
}
private <P> List<P> project(List<Tuple> results, Class<P> projectionClass) {
return results.stream()
.map(tuple -> {
Map<String, Object> mappedResult = new HashMap<>(tuple.getElements().size());
for (TupleElement<?> element : tuple.getElements()) {
String name = element.getAlias();
mappedResult.put(name, tuple.get(name));
}
return projectionFactory.createProjection(projectionClass, mappedResult);
})
.collect(Collectors.toList());
}
}
The interface-based projection I am trying to populate (using SpelAwareProxyProjectionFactory) is this:
public interface VehicleStoppedAggregate {
CameraZone getCameraZone();
Integer getTotalVehicles();
Double getMinDuration();
Double getAvgDuration();
Double getMaxDuration();
}
The call to getStoppedVehiclesCount() fails with the following error:
ERROR: column "camerazone1_.id" must appear in the GROUP BY clause or be used in an aggregate function
This error is coming from the PostgreSQL database, and rightly so because the SQL hibernate generates is incorrect:
select
vehiclesto0_.zone_id as col_0_0_,
count(*) as col_1_0_,
min(vehiclesto0_.duration_seconds) as col_2_0_,
avg(vehiclesto0_.duration_seconds) as col_3_0_,
max(vehiclesto0_.duration_seconds) as col_4_0_,
camerazone1_.id as id1_2_,
camerazone1_.name as name2_2_,
camerazone1_.type as type3_2_,
camerazone1_.uuid as uuid4_2_
from
vehicle_stopped vehiclesto0_
inner join
camera_zone camerazone1_
on vehiclesto0_.zone_id=camerazone1_.id cross
where
vehiclesto0_.start_ts>=?
and vehiclesto0_.start_ts<=?
and 1=1
and 1=1
and 1=1
group by
vehiclesto0_.zone_id
It is not grouping by the other fields it is requesting from the joined table.
If I had to use a normal class, instead of a Tuple, it would work, but it would mean I would have to create a class with a huge constructor for all fields for Hibernate to populate it.
Somehow, when I use Interface-based projections with Spring's repositories rather than my criteriaquery, the same scenario works. They manage to populate the one-to-many relationships just fine.
Is there a way to fix this and make Hibernate ask for the right fields?
I am using Hibernate 5.6.14.Final (as bundled with Spring Boot 2.7.8).
I believe the "solution" is two create two "independent" query roots and join them together:
CriteriaBuilder builder = session.getCriteriaBuilder();
CriteriaQuery<Tuple> query = builder.createTupleQuery();
Root<VehicleStopped> root = query.from(VehicleStopped.class);
// instead of Path<CameraZone> zone = root.get("cameraZone")
Root<CameraZone> zone = query.from(CameraZone.class);
query.where(builder.equal(zone, root.get("cameraZone")));
Path<Number> duration = root.get("durationSeconds");
query
.multiselect(zone,
builder.count(root).alias("totalVehicles"),
builder.min(duration).alias("minDuration"),
builder.avg(duration).alias("avgDuration"),
builder.max(duration).alias("maxDuration"))
.groupBy(zone);
session.createQuery(query).getResultList();
In that case Hibernate 5 produces following SQL (which actually looks weird from my perspective due to missing columns in group by clause):
select
naturalidc1_.id as col_0_0_,
count(*) as col_1_0_,
min(naturalidc0_.duration_seconds) as col_2_0_,
avg(naturalidc0_.duration_seconds) as col_3_0_,
max(naturalidc0_.duration_seconds) as col_4_0_,
naturalidc1_.id as id1_0_,
naturalidc1_.name as name2_0_,
naturalidc1_.type as type3_0_,
naturalidc1_.uuid as uuid4_0_
from
vehicle_stopped naturalidc0_ cross
join
camera_zone naturalidc1_
where
naturalidc1_.id=naturalidc0_.zone_id
group by
naturalidc1_.id
FYI. Your initial query does work in Hibernate 6 and produced SQL does look more correct but still weird:
select
c1_0.id,
c1_0.name,
c1_0.type,
c1_0.uuid,
count(*),
min(v1_0.duration_seconds),
avg(v1_0.duration_seconds),
max(v1_0.duration_seconds)
from
vehicle_stopped v1_0
join
camera_zone c1_0
on c1_0.id=v1_0.zone_id
group by
1,
2,
3,
4
I have multi module project (*.war).
It's an entry point in app.
#SpringBootConfiguration
#SpringBootApplication
#EnableJpaRepositories(basePackages = {"....dao.repository"})
#EntityScan(basePackages = {"....dao.model"})
#ComponentScan(basePackages = {"..."})
public class ApsTtsApplication
extends SpringBootServletInitializer
implements WebApplicationInitializer {
private static final Logger LOGGER = LoggerFactory.getLogger( ApsTtsApplication.class );
public static void main(String[] args) {
LOGGER.info("Start an application...");
SpringApplication.run(ApsTtsApplication.class, args);
}
#Override
protected SpringApplicationBuilder configure(SpringApplicationBuilder builder) {
LOGGER.info("There is building the web application!");
return builder.sources(ApsTtsApplication.class);
}
}
model
IdMainForEntities
#MappedSuperclass
public abstract class IdMainForEntities {
#Id
#GenericGenerator(name="system-uuid", strategy = "uuid")
#GeneratedValue(generator="system-uuid")
#Column(name = "id", nullable = false)
private String ID;
public IdMainForEntities() {
}
public String getID() {
return ID;
}
public void setID(String ID) {
this.ID = ID;
}
#Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
IdMainForEntities that = (IdMainForEntities) o;
return Objects.equals(ID, that.ID);
}
#Override
public int hashCode() {
return Objects.hash(ID);
}
#Override
public String toString() {
return "IdMainEntity{" +
"ID='" + ID + '\'' +
'}';
}
}
MessageLog
#Entity
#Table(name = "MESSAGESLOG")
public class MessageLog extends IdMainForEntities {
...
#Column(name = "MSG_OWNER_ID")
#Size(message = "MSG_OWNER_ID{MessagesLog.size}", max = 32)
private String msgOwnerId;
public MessageLog() {
}
public String getMsgOwnerId() {
return msgOwnerId;
}
public void setMsgOwnerId(String msgOwnerId) {
this.msgOwnerId = msgOwnerId;
}
....
}
MessagesLogReadRepository
public interface MessagesLogReadRepository extends CrudRepository <MessageLog, String> {
Optional <MessageLog> findByMsgOwnerId(String msgOwnerId);
MessageLog findMessageLogByMsgOwnerId(String msgOwnerId);
Optional <MessageLog> findByDocument(String documentId);
}
Only standard methods can be called from this repository : findById (), etc.
But named queries is not found.
18-03-2020 08:49:39.531 DEBUG 8660 o.s.d.j.r.query.JpaQueryFactory
: Looking up query for method findByMsgOwnerId 18-03-2020
08:49:39.547 DEBUG 8660 o.s.d.jpa.repository.query.NamedQuery :
Looking up named query MessageLog.findByMsgOwnerId 18-03-2020
08:49:39.547 DEBUG 8660 o.h.e.t.internal.TransactionImpl :
On TransactionImpl creation,
JpaCompliance#isJpaTransactionComplianceEnabled == false 18-03-2020
08:49:39.547 DEBUG 8660 o.s.d.jpa.repository.query.NamedQuery :
Did not find named query MessageLog.findByMsgOwnerId
#Override
public MessageLogDto getByMsgOwnerId(String msgOwnerId) {
if(msgOwnerId == null) throw new MessagesLogException(paramNotNull);
/*It's null*/
Optional<MessageLog> byMsgOwnerId = this.messagesLogReadRepository.findByMsgOwnerId("8a00844170d829040170d82c670b00");
MessageLog messageLog = byMsgOwnerId.orElse(new MessageLog());
/*It's OK*/
Optional<MessageLog> byId = this.messagesLogReadRepository.findById("8a00844170d829040170d82c670b0003");
return transformEntityToDto(messageLog);
}
Update
public interface MessagesLogReadRepository extends JpaRepository<MessageLog, String> {
#Query(value = "SELECT * from messageslog where MSG_OWNER_ID = ?1", nativeQuery = true )
Optional <MessageLog> findRowByMsgOwnerId(String msgOwnerId);
...
...extends JpaRepository
But, It doesn't solve the problem.
I don't have any mistakes. I only get null, but the requested row is in the table, that 's for sure.
does anyone have any ideas about this?
Why ?
Solution
The field we were working with had a data type CHAR(32 byte). We changed this type to VARCHAR(32 byte) into a table database of Oracle.
Now, Spring can build named query fine.
The project on older versions of Spring worked like this ...(CHAR(32 byte))
I don't understand why so.
I have a table with unique constraint on the one field and I want to setup Hibernate EntityManager so it would insert new record only if there is no such record already and will update otherways.
My POJO for the table looks looks this:
#Entity
#Table(name = "links", uniqueConstraints = {
#UniqueConstraint(columnNames = "link", name = "uk_link")
})
public class Link {
private long linkId;
private String link;
private String data;
private String metadata;
private List<Result> results;
#Id
#Column(name="link_id")
#GeneratedValue(strategy=GenerationType.IDENTITY)
public long getLinkId() {
return linkId;
}
public void setLinkId(long linkId) {
this.linkId = linkId;
}
public String getLink() {
return link;
}
public void setLink(String link) {
this.link = link;
}
public String getData() {
return data;
}
public void setData(String data) {
this.data = data;
}
public String getMetadata() {
return metadata;
}
public void setMetadata(String metadata) {
this.metadata = metadata;
}
#OneToMany(cascade=CascadeType.ALL, fetch = FetchType.LAZY, mappedBy = "link")
public List<Result> getResults() {
return results;
}
public void setResults(List<Result> results) {
this.results = results;
}
#Override
public String toString() {
return "Link [linkId=" + linkId + ", link=" + link + ", data=" + data + ", metadata=" + metadata + "]";
}
public boolean isDataEquals(String data) {
if (this.data == null) {
if (data != null)
return false;
} else if (!this.data.equals(data))
return false;
return true;
}
public boolean isMetadataEquals(String metadata) {
if (this.metadata == null) {
if (metadata != null)
return false;
} else if (!this.metadata.equals(metadata))
return false;
return true;
}
}
And I'm try to resolve the necessary changes through this code:
public Link selectByLink(String link) {
return entityManager
.createQuery("select l from Link l WHERE l.link = :link", Link.class)
.setParameter("link", link)
.getSingleResult();
}
public void insert(Link link) {
this.entityManager.persist(link);
}
public Link update(Link link) {
return this.entityManager.merge(link);
}
public void save(Link link) {
if (link.getLinkId() == 0) {
Link _existing = selectByLink(link.getLink());
if (null != _existing) {
link.setLinkId(_existing.getLinkId());
if (!_existing.isDataEquals(link.getData()) ||
!_existing.isMetadataEquals(link.getMetadata())) {
update(link);
}
} else
insert(link);
}
}
In the Spring log, I see one additional select:
Hibernate: select link0_.link_id as link_id1_0_, link0_.data as data2_0_, link0_.link as link3_0_, link0_.metadata as metadata4_0_ from links link0_ where link0_.link=?
Hibernate: select link0_.link_id as link_id1_0_1_, link0_.data as data2_0_1_, link0_.link as link3_0_1_, link0_.metadata as metadata4_0_1_, results1_.link_id as link_id1_1_3_, results1_.text_id as text_id2_1_3_, results1_.link_id as link_id1_1_0_, results1_.text_id as text_id2_1_0_, results1_.found as found3_1_0_, results1_.level as level4_1_0_ from links link0_ left outer join results results1_ on link0_.link_id=results1_.link_id where link0_.link_id=?
Hibernate: update links set data=?, link=?, metadata=? where link_id=?
I guess, it happens because I use merge function, but if I do not search for the object id prior merging it, the merge will try to insert object instead of updating it. Is there a way to just update the object without testing it first?
And unrelated question, the SQL's looks very messy. all these link0_.link_id as link_id1_0_, can they be suppressed?
The extra join comes as a result of your lazy one to many relationship. Apart from the select for the link itself, an extra select statement is performed to load the collection (that's the root of the famous N+1 problem).
And, for the selection performed before updating, it seems to be the way hibernate persists by default, when entities are detached. If you want to avoid it you should play with its configuration (there's a select-before-update property) or write an update query yourself. You could as well try to avoid Spring detaching Hibernate entities.
I'm using Spring Roo to generate an entity based on a table(created on the MS SQL Server). The following is my sql script:
CREATE TABLE [dbo].[JMSMessage](
[MessageID] [nvarchar](100) NOT NULL,
[Destination] [varchar](50) NOT
[Status] [varchar](15) NULL,
CONSTRAINT [PK_JMSMessage] PRIMARY KEY CLUSTERED
(
[MessageID] ASC
) WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF,ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY]
Java entity generated by Spring Roo is
#Entity
#Table(schema = "dbo",name = "JMSMessage")
#Configurable
public class Jmsmessage {
#Column(name = "Destination", length = 50)
#NotNull
private String destination;
#Column(name = "Status", length = 15)
private String status;
public String getDestination() {
return destination;
}
public void setDestination(String destination) {
this.destination = destination;
}
public String getStatus() {
return status;
}
public void setStatus(String status) {
this.status = status;
}
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
#Column(name = "MessageID", length = 100)
private String messageId;
public String getMessageId() {
return this.messageId;
}
public void setMessageId(String id) {
this.messageId = id;
}
#PersistenceContext
transient EntityManager entityManager;
public static final EntityManager entityManager() {
EntityManager em = new Jmsmessage().entityManager;
if (em == null) throw new IllegalStateException("Entity manager has not been injected (is the Spring Aspects JAR configured as an AJC/AJDT aspects library?)");
return em;
}
public static Jmsmessage findJmsmessage(String messageId) {
if (messageId == null || messageId.length() == 0) return null;
return entityManager().find(Jmsmessage.class, messageId);
}
#Transactional
public void persist() {
if (this.entityManager == null) this.entityManager = entityManager();
this.entityManager.persist(this);
}
#Transactional
public void remove() {
if (this.entityManager == null) this.entityManager = entityManager();
if (this.entityManager.contains(this)) {
this.entityManager.remove(this);
} else {
Jmsmessage attached = Jmsmessage.findJmsmessage(this.messageId);
this.entityManager.remove(attached);
}
}
#Transactional
public void flush() {
if (this.entityManager == null) this.entityManager = entityManager();
this.entityManager.flush();
}
#Transactional
public void clear() {
if (this.entityManager == null) this.entityManager = entityManager();
this.entityManager.clear();
}
#Transactional
public Jmsmessage merge() {
if (this.entityManager == null) this.entityManager = entityManager();
Jmsmessage merged = this.entityManager.merge(this);
this.entityManager.flush();
return merged;
}
public String toString() {
return ReflectionToStringBuilder.toString(this, ToStringStyle.SHORT_PREFIX_STYLE);
}
}
Then when i run the following code to create an entity instance and try to persistent it, the program throws an PersistentObjectException saying "detached entity passed to persist".
Jmsmessage message = new Jmsmessage();
message.setMessageId("m1");
message.setDestination("queue1");
message.setStatus("status1");
message.persist(); // If i use message.merge(), it will throws another Sql exception saying "Cannot insert the value NULL into column 'MessageID'"
How can i make the persistent work, any suggestions? Thanks in advance!
You should not set a value for messageId, because you have the strategy to #GeneratedValue(strategy = GenerationType.AUTO)
On another note, I notice that you have used the active record pattern that roo provides. I suggest that you change this to the DAO pattern since code like this in very uncommon in the Java/Spring/Hibernate world :)
You have ID generation strategy auto but you have assigned some id value in your entity. Meaning ORM engine will consider your fresh entity as detached entity since it got id and it will throw exception like trying to save detached entity.. if you try to merge the entity to overcome the problem , ORM engine will check the id you provided to the entity and it will not able to match any row against your entity since there is no such row with that primary key..
Bottom line of the error is, you are confusing the ORM engine with Id generation.
Hope This is helpful!
I thought that an entity found by em.find was automatically managed by em, even out a transaction, but this class below seems to show the contrary. Was I wrong or what is the mistake in that class?
#Stateful
#TransactionAttribute(NOT_SUPPORTED)
public class CustomerGateway {
#PersistenceContext(unitName = "customersPU", type = EXTENDED)
private EntityManager em;
private Customer customer;
public Customer find(Long id) {
// customer is not managed!
this.customer = em.find(Customer.class, id);
// Print false!
System.out.println("Method find: " + em.contains(customer));
// Print false too (2 is the id of an entity)!
System.out.println("Method find: " + em.contains(em.find(Customer.class, 2L));
// A workaround
customer = em.merge(customer);
// Print true.
System.out.println("Method find after merge: " + em.contains(customer));
return this.customer;
}
EDIT 1: code of the entity
#Entity
#NamedQuery(name = "Customer.all", query = "select c from Customer c")
public class Customer implements Serializable {
private static final long serialVersionUID = 1L;
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
private String name;
public Customer() {
}
public Customer(String name) {
this.name = name;
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
#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 Customer)) {
return false;
}
Customer other = (Customer) object;
if ((this.id == null && other.id != null) || (this.id != null && !this.id.equals(other.id))) {
return false;
}
return true;
}
#Override
public String toString() {
return "entity.Customer[ id=" + id + " ]";
}
}
Code of the stateful EJB:
#Stateful
#TransactionAttribute(NOT_SUPPORTED)
public class CustomerGateway {
#PersistenceContext(type = PersistenceContextType.EXTENDED)
private EntityManager em;
private Customer customer;
public Customer getCustomer() {
return customer;
}
public void create(Customer customer) {
em.persist(customer);
this.customer = customer;
}
public Customer find(Long id) {
this.customer = em.find(Customer.class, id);
System.out.println("customer managed ? " + em.contains(this.customer));
// Workaround :
// this.customer = em.merge(customer);
return customer;
}
public void remove(Long id) {
Customer cust = em.getReference(Customer.class, id);
em.remove(cust);
}
#TransactionAttribute(REQUIRES_NEW)
public void save() {
}
public List<Customer> findAllCustomers() {
Query query = em.createNamedQuery("Customer.all");
return query.getResultList();
}
#Remove
public void close() {
}
}
I work with NetBeans 7.4, GlassFish 4.0, EJB 3.2, Java DB.
All that you have experienced is according to the spec. The persistence context remains (and the entities keeps attached) while the transaction exists. So, in a extended persistence context and a NOT_SUPPORTED transaction the objects retrieved by calling find method are dettached. -Also, if your Customer object has lazy relationships and you try to access them, then, it is highly probable that you will get a runtime exception.-
Now, why the merge method is just ok?. Well, first remember that merge returns a managed entity and is attaching the customer to the persistence context.
Second, you have an EXTENDED persistence context, so, it wont go to update the database until you call the #Remove annotated method. When this call arrives, you will probably get a TransactionRequiredException.
EDIT 1 --------------------------------------------------------------------------------
According to your comments:
find is not required to be in a transaction, although, if you want managed object there must be one.
The paragraph is about EM the life cycle (3.3 section), in this case, tries to explain that at the end of a method for a transaction-scoped bean, the entities will be detached, but, in the case of extended EM the entities will remains attached.
There are 2 insightful paragraphs :
When an EM with an extended persistence context is used, the persist, remove, merge and refresh operations can be called regardless of whether a transaction is active. The effects of these operations will be committed to the database when the extended persistence context is enlisted in a transaction and the transaction commits.
The persistence context is closed by the container when the #Remove method of the stateful session bean completes (or the stateful session bean instance is otherwise destroyed).
Looks like the method that you initially omit in the question with #TransactionAttribute(REQUIRES_NEW) is the place where the merge occurs successfully. That's why you have no exception.
EDIT 2 --------------------------------------------------------------------------------
After some testing, GF4 has a bug and has been reported > https://java.net/jira/browse/GLASSFISH-20968
EDIT 3 ---------------------------------------------------------------------------------
20/May/2014 : The bug has been marked as : Must Fix for Glassfish 4.0.1.
According to Checkus, it seems to be a bug in GF4: https://java.net/jira/browse/GLASSFISH-20968