I have the following Entity
#Entity
#Table(name = "APP_ITEM")
public class AppItem implements Serializable {
private static final long serialVersionUID = 1L;
#EmbeddedId
private AppItemPK AppItemPK;
public AppItemPK getAppItemPK() {
return appItemPK;
}
public void setAppItemPK(
AppItemPK appItemPK) {
this.appItemPK = appItemPK;
}
}
#Embeddable
public class AppItemPK implements Serializable {
private static final long serialVersionUID = 1L;
#Column(name = "app_id")
private Long appId;
#Column(name = "item_id")
private Long itemId;
public Long getAppId() {
return appId;
}
public void setAppId(Long appId) {
this.appId = appId;
}
public Long getItemId() {
return itemId;
}
public void setItemId(Long itemId) {
this.itemId = itemId;
}
public boolean equals(Object obj) {
if (obj instanceof AppItemPK) {
AppItemPK appItemPK = (AppItemPK) obj;
if (appItemPK.getItemId().equals(this.itemId)
&& appItemPK.getAppId().equals(
this.appId)) {
return true;
}
}
return false;
}
public int hashCode() {
return this.itemId.hashCode() + this.applicationId.hashCode();
}
}
Using below code to insert record into app_item table
#Transactional(readOnly = false)
public boolean saveItemSelection(PageHeaderViewData pageHeaderViewData, Map<Long, Boolean> selectedItems,String savedItems){
long millisSaveStart = Calendar.getInstance().getTimeInMillis();
log.debug("Inside saveItemSelection appId:"+pageHeaderData.getAppId());
boolean saveStatus = false;
List<AppItem> appItemInsList = new ArrayList<SavedApplicationItem>();
if (pageHeaderData.getAppId() != null) {
for (Entry<Long, Boolean> idEntry : selectedItems.entrySet() ) {
if (idEntry!= null){
if (idEntry.getValue() && !savedItems.contains(idEntry.getKey().toString())){
//log.debug("Inside saveItemSelection SAVED itemId:"+idEntry.getKey()+" , Value:"+idEntry.getValue());
AppItem appItem = new AppItem();
AppItemPK appItemPK = new AppItemPK();
appItemPK.setAppId(pageHeaderData.getAppId());
appItemPK.setItemId(idEntry.getKey());
appItem.setAppItemPK(appItemPK);
appItem.setUpdateInd(ToggleEnum.Y);
appItemInsList.add(appItem);
//appItemRepository.saveAndFlush(appItem);
}
}
}
}
if (appItemInsList.size() != 0){
long millisJPASaveStart = Calendar.getInstance().getTimeInMillis();
appItemRepository.save(appItemInsList);
long millisJPASaveEnd = Calendar.getInstance().getTimeInMillis();
log.debug("JPA save time:"+(millisJPASaveEnd-millisJPASaveStart));
}
saveStatus = true;
long millisSaveEnd = Calendar.getInstance().getTimeInMillis();
log.debug("Total save time:"+(millisSaveEnd-millisSaveStart));
}
return saveStatus;
}//end of saveItemSelection
For inserting 5000 records it is taking 13826 milliseconds.
Can someone please let me know, how to improve the performance in above JPA code. We are using hibernate for jpa implementation.
To improve the performance your inserts, you should implement batch insert using custom code. The method below will make sure that batches are flushed to the database. Tweak the batch size based on your performance tests. Generally 50 is a good number to start.
#Transactional
public void bulkPersist(List<Entity> entities) {
int i = 0;
for (Entity entity : entities) {
em.persist(entity);
i++;
if (i % batchSize == 0) {
flush();
clear();
}
}
}
Above change will create multiple insert statements. You can further optimize the insert query by setting up the hibernate configuration.
<prop key="hibernate.order_inserts">true</prop>
<prop key="hibernate.order_updates">true</prop>
Tip: Enable Debug log to see 1 insert query is being generated per batch.
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 about 300 JPA entities where the getters are annotated with persistence annotations. I would like to find a way to move all such annotations to the properties instead and remove all getters and setters. I did this manually for about 100 of these classes but it's very time consuming and mind numbing work.
I'm looking at source code transformation tools like Spoon but still not sure it can do what I need it to do.
More specifically, how can I transform this code:
#Entity
#Table(name = "crm_ticket")
public class CrmTicket implements Serializable {
private static final long serialVersionUID = -902718555957517699L;
private CrmAccount crmAccount;
private ItsType subType;
#ManyToOne(fetch = FetchType.EAGER)
#JoinColumn(name = "account")
public CrmAccount getCrmAccount() {
return crmAccount;
}
public void setCrmAccount(CrmAccount crmAccount) {
this.crmAccount = crmAccount;
}
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "sub_type")
public ItsType getSubType() {
return subType;
}
public void setSubType(ItsType type) {
this.subType = type;
}
}
To this:
#Entity
#Table(name = "crm_ticket")
#Data
public class CrmTicket implements Serializable {
private static final long serialVersionUID = -902718555957517699L;
#ManyToOne(fetch = FetchType.EAGER)
#JoinColumn(name = "account")
private CrmAccount crmAccount;
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "sub_type")
private ItsType subType;
}
Spoon would work well for this, you would use aField.addAnnotation and aSetter.delete.
I ended up using Spoon. It wasn't that painful. I configured their maven plugin to run my processor and it transformed the code of my Entity classes. I then copied the generated code back to my project and removed the plugin configuration.
Here's my processor code:
public class JpaAnnotationMover extends AbstractProcessor<CtMethod> {
Pattern p1 = Pattern.compile("return.*this\\.(.*?)$");
Pattern p2 = Pattern.compile("return(.*?)$");
#Override
public boolean isToBeProcessed(CtMethod method) {
return isInEntity(method) && isAGetter(method) && hasOneStatement(method) && !isTransient(method);
}
#Override
public void process(CtMethod ctMethod) {
CtType parentClass = ctMethod.getParent(CtType.class);
String fieldName = getFieldName(ctMethod);
if (fieldName == null) {
log.warn(String.format("expected field name for %s not found.", ctMethod.getSimpleName()));
return;
}
CtField field = parentClass.getField(fieldName);
if (field == null) {
log.warn(String.format("Expected field %s not found.", fieldName));
return;
}
for (CtAnnotation<? extends Annotation> annotation : ctMethod.getAnnotations()) {
field.addAnnotation(annotation);
}
parentClass.removeMethod(ctMethod);
log.info(String.format("Processed method %s:%s", parentClass.getSimpleName(), ctMethod.getSimpleName()));
// find corresponding setter
String setterName = "set" + WordUtils.capitalize(fieldName);
#SuppressWarnings("unchecked") CtMethod setter = parentClass
.getMethod(getFactory().Type().createReference("void"), setterName, ctMethod.getType());
if (setter == null) {
log.warn(String.format("Unable to find setter for %s", fieldName));
return;
}
parentClass.removeMethod(setter);
if (!parentClass.hasAnnotation(Data.class)) {
parentClass.addAnnotation(getFactory().createAnnotation(getFactory().Type().createReference(Data.class)));
}
}
private Boolean isInEntity(CtMethod method) {
CtType parentClass = method.getParent(CtType.class);
return parentClass.hasAnnotation(Entity.class);
}
private Boolean isAGetter(CtMethod method) {
return method.getSimpleName().contains("get");
}
private Boolean hasOneStatement(CtMethod method) {
return method.getBody().getStatements().size() == 1;
}
private Boolean isTransient(CtMethod method) {
return method.hasAnnotation(Transient.class);
}
private String getFieldName(CtMethod method) {
String statement = method.getBody().getLastStatement().toString();
Matcher m = p1.matcher(statement);
Matcher m2 = p2.matcher(statement);
return m.matches() ? m.group(1).trim() : m2.matches() ? m2.group(1).trim() : null;
}
}
I am using Spring REST with Hibernate and i am fetching a particular record from database table passing id into my method. The method is working properly but if there is no record in table then i want false in a variable and if record exist then i want true in the variable in my json object.
Here is my Entity class Subscribe.java
#Entity
#Table(name="subscribe")
#JsonIgnoreProperties({"hibernateLazyInitializer", "handler"})
public class Subscribe implements Serializable {
private static final long serialVersionUID = 1L;
#Id
#GeneratedValue
#Column(name="id")
private long id;
#Column(name="subscribed_id")
private String subID;
#Column(name="subscriber_id")
private long subrID;
public long getSubrID() {
return subrID;
}
public void setSubrID(long subrID) {
this.subrID = subrID;
}
public String getSubID() {
return subID;
}
public void setSubID(String subID) {
this.subID = subID;
}
public long getId() {
return id;
}
public void setId(long id) {
this.id = id;
}
}
Here is my DAO class
#SuppressWarnings({ "unchecked", "rawtypes" })
public List<Subscribe> getSubscribeById(long id) throws Exception {
session = sessionFactory.openSession();
Criteria cr = session.createCriteria(Subscribe.class);
cr.add(Restrictions.eq("subrID", id));
List results = cr.list();
tx = session.getTransaction();
session.beginTransaction();
tx.commit();
return results;
}
And here is my Controller
#RequestMapping(value = "/subscribe/{id}", method = RequestMethod.GET)
public #ResponseBody
List<Subscribe> getSubscriber(#PathVariable("id") long id) {
List<Subscribe> sub = null;
try {
sub = profileService.getSubscribeById(id);
} catch (Exception e) {
e.printStackTrace();
}
return sub;
}
Please suggest me how can can i do this
Given the way your code is structured ( where you effectively pass the database object directly to the REST client ) there is no clean way that you can do this.
I think a more RESTful approach would be to return an HTTP code that indicates that the requested record could not be found. HTTP 404 would be the appropriate code to use.
So, in pseudo-code,
#RequestMapping(value = "/subscribe/{id}", method = RequestMethod.GET)
public #ResponseBody
List<Subscribe> getSubscriber(#PathVariable("id") long id) {
List<Subscribe> sub = null;
try {
sub = profileService.getSubscribeById(id);
} catch (Exception e) {
e.printStackTrace();
}
if ( sub.isEmpty() ){
return HTTP 404 result code
} else {
return sub;
}
}
Your client side code will need to be altered to respond to the HTTP result code rather than a boolean value, but otherwise would remain unchanged.
I'm trying to render like/dislike buttons using JPA and JSF
#Entity
public class APost implements Serializable {
...
#ManyToMany(fetch = FetchType.EAGER)
protected Collection<User> likes;
#ManyToMany(fetch = FetchType.EAGER)
protected Collection<User> dislikes;
#Transient
public Integer getLikesNumber() {
if (likes == null) {
return 0;
}
return likes.size();
}
public Collection<User> getLikes() {
return likes;
}
public void setLikes(Collection<User> likes) {
this.likes = likes;
}
public void addLikes(User user) {
if (likes == null) {
likes = new HashSet<>();
}
likes.add(user);
}
public void removeLikes(User user) {
if (likes != null) {
likes.remove(user);
}
}
#Transient
public Integer getDislikesNumber() {
if (dislikes == null) {
return 0;
}
return dislikes.size();
}
public Collection<User> getDislikes() {
return dislikes;
}
public void setDislikes(Collection<User> dislikes) {
this.dislikes = dislikes;
}
public void addDislikes(User user) {
if (dislikes == null) {
dislikes = new HashSet<>();
}
dislikes.add(user);
}
public void removeDislikes(User user) {
if (dislikes != null) {
dislikes.remove(user);
}
}
}
The User Class :
#Entity
public class User implements Serializable {
...
#Id
private String email;
#ManyToMany(fetch = FetchType.EAGER, mappedBy = "likes")
protected Collection<APost> likes;
#ManyToMany(fetch = FetchType.EAGER, mappedBy = "dislikes")
protected Collection<APost> dislikes;
public Collection<APost> getLikes() {
return likes;
}
public void addLikes(APost post) {
if (likes == null) {
likes = new HashSet<>();
}
likes.add(post);
}
public void removeLikes(APost post) {
if (likes != null) {
likes.remove(post);
}
}
public Collection<APost> getDislikes() {
return dislikes;
}
public void addDislikes(APost post) {
if (dislikes == null) {
dislikes = new HashSet<>();
}
dislikes.add(post);
}
public void removeDislikes(APost post) {
if (dislikes != null) {
dislikes.remove(post);
}
}
#Override
public boolean equals(Object obj) {
if (obj == null)
return false;
User uObj = (User) obj;
return getEmail().equals(uObj.getEmail());
}
}
Facelet : post.xhtml
...
<h:commandLink
action="#{bean.addLike(post.id)}"
<h:graphicImage library="images" name="thumb-up-24x31.png"></h:graphicImage>
</h:commandLink>
...
Bean.java
#ManagedBean
#ConversationScoped
public class OnePostManager implements Serializable {
private static final long serialVersionUID = 1L;
#EJB
private IPostFacade postFacade;
#EJB
private IUserFacade userFacade;
#Inject
private LoginManager loginManager;
...
public String addLike(Long postId) {
if (loginManager.getConnected().equals(false)) {
return "login?redirect=post&&faces-redirect=true";
}
if (postId != null) {
APost post = postFacade.find(postId);
User user = userFacade.find(loginManager.getEmail());
post.addLikes(user);
postFacade.edit(post);
}
return null;
}
}
Now, when I click on the "like" button, I got an exception :
javax.faces.el.EvaluationException: javax.ejb.EJBException: Transaction aborted
...
Caused by: Exception [EclipseLink-4002] (Eclipse Persistence Services - 2.5.0.v20130507- 3faac2b): org.eclipse.persistence.exceptions.DatabaseException
Internal Exception: com.mysql.jdbc.exceptions.jdbc4.MySQLIntegrityConstraintViolationException: Cannot add or update a child row: a foreign key constraint fails (`medianovens`.`APOST_USER`, CONSTRAINT `FK_APOST_USER_likes_EMAIL` FOREIGN KEY (`likes_EMAIL`) REFERENCES `USER` (`EMAIL`))
Error Code: 1452
Call: INSERT INTO APOST_USER (dislikes_EMAIL, dislikes_ID) VALUES (?, ?)
bind => [2 parameters bound]
Query: DataModifyQuery(name="dislikes" sql="INSERT INTO APOST_USER (dislikes_EMAIL, dislikes_ID) VALUES (?, ?)")
...
When I look at my database, I've got one table called APOST_USER with 4 columns : dislikes_EMAIL, dislikes_ID, likes_EMAIL, likes_ID
So I suppose that when it tries to add an entry that defines which post a user likes, it tries to fill likes_EMAIL and likes_ID but also expects some values for dislikes_EMAIL and dislikes_ID ...
How can I solve this ?
Note :
When I remove all code concerning dislike functions, the code works OK (My table APOST_USER only has 2 columns, likes_EMAIL and likes_ID) and an entry can be added, but everything goes wrong if I add all code regarding dislike function.
It sounds like your JPA provider is trying to be smart and combine the two references to APost into one. What you should probably do is look into the #JoinTable annotation, which would allow you to definitively specify different tables should be used for your Like and Dislike features.
http://blog.jbaysolutions.com/2012/12/17/jpa-2-relationships-many-to-many/ shows how this is used in practice.
Here's what I think it should look like to fix your mapping problem:
#Entity
public class APost implements Serializable {
...
#JoinTable(name="post_likes")
#ManyToMany(fetch = FetchType.EAGER)
protected Collection<User> likes;
#JoinTable(name="post_dislikes")
#ManyToMany(fetch = FetchType.EAGER)
protected Collection<User> dislikes;
You need to assign both sides of the relationship:
post.addLikes(user);
user.addLikes(post);
I have a weird problem. I'm using play 2.1-SNAPSHOT with ebeans (=> mysql). I have a very small (test) setup and for some reason database updates and deletions don't work. Items are created in the DB... but updating them does not work.
Here's my bean (which extends a superclass that adds the timestamps (created and modified date)):
AbstractTimestamp (superclass):
#MappedSuperclass
public abstract class AbstractTimestampedBean extends AbstractIdentifiableBean {
/** The date this item has been created. */
#CreatedTimestamp
public Timestamp createdTime;
}
Project Bean (removed unimportant stuff) - hashCode and equals have been created by eclipse - here we overwrite the methods of play.db.ebean.Model:
#Entity
#Table(name = "Projects")
public class Project extends AbstractTimestampedBean {
private static final long serialVersionUID = -6160140283947231026L;
#NotNull
public String title;
#NotNull
#ManyToOne(cascade = CascadeType.ALL, fetch = FetchType.EAGER)
public User owner;
#NotNull
#ManyToOne(cascade = CascadeType.ALL, fetch = FetchType.EAGER)
public User creator;
#ManyToMany(cascade = CascadeType.ALL, fetch = FetchType.EAGER)
public Set<User> participants;
#EnumMapping(nameValuePairs = "ACTIVE=A,INACTIVE=I,EXPIRED=E")
public enum Status {
ACTIVE, INACTIVE, EXPIRED
}
public Project() {
}
public Project(final String title, final User creator) {
this.title = title;
this.creator = creator;
this.owner = creator;
}
/*
* (non-Javadoc)
*
* #see play.db.ebean.Model#hashCode()
*/
#Override
public int hashCode() {
final int prime = 31;
int result = super.hashCode();
result = prime * result
+ (this.creator == null ? 0 : this.creator.hashCode());
result = prime * result
+ (this.owner == null ? 0 : this.owner.hashCode());
result = prime * result
+ (this.participants == null ? 0 : this.participants
.hashCode());
result = prime * result
+ (this.title == null ? 0 : this.title.hashCode());
return result;
}
/*
* (non-Javadoc)
*
* #see play.db.ebean.Model#equals(java.lang.Object)
*/
#Override
public boolean equals(final Object obj) {
if (this == obj) {
return true;
}
if (this.getClass() != obj.getClass()) {
return false;
}
final Project other = (Project) obj;
if (this.creator == null) {
if (other.creator != null) {
return false;
}
} else if (!this.creator.equals(other.creator)) {
return false;
}
if (this.owner == null) {
if (other.owner != null) {
return false;
}
} else if (!this.owner.equals(other.owner)) {
return false;
}
if (this.participants == null) {
if (other.participants != null) {
return false;
}
} else if (!this.participants.equals(other.participants)) {
return false;
}
if (this.title == null) {
if (other.title != null) {
return false;
}
} else if (!this.title.equals(other.title)) {
return false;
}
return true;
}
Here's the very simple test case:
First run creates a projects - checks that it's there (nothing fails here)
Then we update some stuff - store it - and assert again... and here I can see that the db entries have not been updated.
http://pastebin.com/7zdzWGXw
Here's the superclass that we are using here:
public abstract class AbstractPersistableTestCase {
#Transactional
void saveBean(final Model bean) {
Ebean.save(bean);
}
#Transactional
void deleteBean(final Model bean) {
Ebean.delete(bean);
}
#Transactional
<T extends Model> void deleteBeans(final List<T> beans) {
Ebean.delete(beans);
}
}
Error message from jUnit4:
This is the assertion of the title in the update case => See: db entry has not been updated:
[error] Test test.models.ProjectTest.createAndUpdateProject failed: expected:<'Project_[NEW_]1350681993608'> but was:<Project_[]1350681993608'>
This happens when I try to delete the project:
[error] Test test.models.ProjectTest.deleteProjects failed: Data has changed. updated [0] rows sql[delete from user where id=? and name is null and email is null and created_time is null] bind[null]
Do you guys have an idea why this is happening? I'm really frustrated here...
Regards,
Sascha
It seems to me that you are not adding an Id to your classes.
Try to add this to your superclass:
#MappedSuperclass
public abstract class AbstractModel extends play.db.ebean.Model
{
#Id
public Long id;
public Long getId()
{
return id;
}
// ... here your other attributes
}