I want to implement soft deletion, but still be able to delete permanently. Is there any way to ignore a declared #SQLDelete() annotation, or maybe say:
#SQLDelete("IF expression THEN UPDATE statement ELSE delete statement")
EDIT:
This is the entity in question.
#Indexed
#Entity
#DynamicUpdate
#FilterDefs({
#FilterDef(name = "tenantFilter", parameters = {#ParamDef(name = "tenantId", type = "string")}),
#FilterDef(name = "deleteFilter")
})
#Filters({
#Filter(name = "tenantFilter", condition = "tenant_id = :tenantId"),
#Filter(name = "deleteFilter", condition = "deleted = false")
})
#SQLDelete(sql ="UPDATE Antwort SET deleted = true, date_modified = NOW() WHERE ID = ?; DELETE FROM Antwort WHERE deleted = true AND kundenentwurf_id IS NOT NULL", check = ResultCheckStyle.NONE)
public class Antwort implements TenantSupport, ISoftDeleteModel{
#Id
#Column(name = "ID")
#GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
private String tenantId;
#Column(nullable = false)
private Boolean deleted;
private Date dateModified;
#ManyToOne(fetch = FetchType.LAZY)
private Organisation organisation;
#ManyToOne(fetch = FetchType.LAZY)
private Projekt projekt;
#ManyToOne(fetch = FetchType.LAZY)
private Kundenentwurf kundenentwurf;
private Integer nummer;
private String frage;
private String antwort;
//getters, setters and contructors...
}
I explained this in more details in one of my Hibernate Tips. Here is the short version of it:
When you call the remove method on your EntityManager, Hibernate will execute the SQL statement defined in the #SQLDelete operation.
You can’t deactivate the #SQLDelete annotation. So, if you want to remove the record from the database permanently, you can’t use the remove method of your EntityManager. You need to execute a SQL DELETE statement using a JPQL, Criteria or native query.
Here is an example of a JPQL DELETE statement:
EntityManager em = emf.createEntityManager();
em.getTransaction().begin();
// do something ...
// add this if you fetched the Book entity in this session
em.flush();
em.clear();
Query query = em.createQuery("DELETE Book b WHERE id = :id");
query.setParameter("id", 1L);
query.executeUpdate();
em.getTransaction().commit();
em.close();
In case you fetched the Book entity you want to remove within your current Hibernate Session, you need to call the flush and clear methods on your EntityManager before you execute the DELETE statement. This ensures that all pending changes are written to the database before you remove the record.
Related
Lets say I have two objects, say one is a User object and the other is a State Object. The state object is basically the 50 states of America so it doesn't ever have to change. The user object however has a Collection of States where the user has been. So like this:
#Entity
#Table(name="tbl_users")
class User {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name="id", unique=true, nullable = false)
private int id;
#Column(name="user_name")
private String name;
#OneToMany(targetEntity=State.class, orphanRemoval = false)
#Column(name="states")
private Collection<State> states;
//getters and setters
}
and the States entity looks like this:
#Entity
#Table(name="tbl_states")
class State {
#Id
#Column(name="id", unique=true, nullable=false)
private int id;
#Column(name="state")
private String state;
// getters and setters
}
Code for adding user (using hibernate):
public int addUser(User user) {
em.persist(user);
em.flush();
return user.getId();
}
Code for getting state by id:
public State getStateById(int id) {
return em.createQuery("SELECT s FROM State s WHERE s.id =:id, State.class)
.setParameter("id", id)
.getSingleResult();
}
but when I try to create a User and pick several states, I get a PSQLException:
org.postgresql.util.PSQLException: ERROR: duplicate key value violates unique constraint "uk_g6pr701i2pcq7400xrlb0hns"
2017-06-21T22:54:35.959991+00:00 app[web.1]: Detail: Key (states_id)=(5) already exists.
I tried looking up the Cascade methods to see if I could use any, but Cascade.MERGE and Cascade.PERSIST seem to do the same thing, and the rest I don't think I need (REMOVE, DETACH, etc). My question is:
How do I add states to the User object without having that error?
This code works:
class Example {
#Test
public void workingTest() {
EntityManagerFactory emf = Persistence.createEntityManagerFactory("testPU");
EntityManager em = emf.createEntityManager();
// Creating three states
State alabama = new State(state: 'Alabama');
State louisiana = new State(state: 'Louisiana');
State texas = new State(state: 'Texas');
em.getTransaction().begin();
em.persist(alabama);
em.persist(louisiana);
em.persist(texas);
em.getTransaction().commit();
List<State> states = em.createQuery('FROM State').getResultList();
// Assert only three states on DB
assert states.size() == 3;
User userFromAlabama = new User();
User userFromAlabamaAndTexas = new User();
em.getTransaction().begin();
State alabamaFromDB = em.find(State, alabama.getId());
State texasFromDB = em.find(State, texas.getId());
userFromAlabama.getStates().add(alabamaFromDB);
userFromAlabamaAndTexas.getStates().add(alabamaFromDB);
userFromAlabamaAndTexas.getStates().add(texasFromDB);
em.persist(userFromAlabama);
em.persist(userFromAlabamaAndTexas);
em.getTransaction().commit();
states = em.createQuery('FROM State').getResultList();
// Assert only three states on DB again
assert states.size() == 3;
// Assert one user
User userFromDB = em.find(User, userFromAlabama.getId());
assert userFromDB.getStates().size() == 1;
userFromDB = em.find(User, userFromAlabamaAndTexas.getId());
assert userFromDB.getStates().size() == 2;
}
}
#Entity
#Table(name="tbl_users")
class User {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private int id
#Column(name="user_name")
private String name
#ManyToMany
private Collection<State> states = Lists.newArrayList()
// Getters and setters
}
#Entity
#Table(name="tbl_states")
class State {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private int id;
#Column(name="state")
private String state;
// Getters and setters
}
You should change your mapping to #ManyToMany!
And you must have 3 tables on DB like this:
TBL_USERS, TBL_STATES and TBL_USERS_TBL_STATES
The TBL_USERS_TBL_STATES table is the default table name that Hibernate uses when a property is annotated with #ManyToMany. If you want to change the tablename of TBL_USERS_TBL_STATES, use the #JoinTable annotation too. See the docs here
With this configuration, you should be able to fetch a State from database, add it to a new User and then persist it. I made a unit test and It works!
In your case it might be better to use a manytomany association with manytomany hibernate dont generate unicity constraint.
Hibernate auto generation scheme behavior is a little bit strange with onetoMany but you can use this workaround.
Try this:
#ManyToMany
#JoinTable(name = "user_state")
private List<State> states;
I have 2 entities CallRecords and CallRecordOperators with one-to-many relation as given below
public class CallRecords {
#Id
#Column(name = "id", unique = true)
private String id;
#Column(columnDefinition = "varchar(255) default ''")
private String callerNumber = "";
#OneToMany(mappedBy="callrecord")
private List<CallRecordOperators> callRecordOperators = new ArrayList<CallRecordOperators>();
//getter setters
}
public class CallRecordOperators {
#Id
#Column(name = "id", length = 50, unique = true, nullable = false, insertable = false, updatable = false)
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
#JsonIgnore
#ManyToOne
#JoinColumn(name = "callRecordId")
private CallRecords callrecord;
#ManyToOne
#JoinColumn(name = "operatorId")
private Operator operator;
#Formats.DateTime(pattern = "yyyy-MM-dd HH:mm:yy")
#Column(columnDefinition = "TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP")
private Date startTime = new Date();
#Column(columnDefinition = "varchar(100) default ''")
private String dialStatus;
//getter setter
}
So if the user ask for all "CallRecords" data I also have to give "CallRecordOperators" as they are related.
Current code for Mapper and DTOs
#Mapper(unmappedTargetPolicy = ReportingPolicy.IGNORE)
public interface CallRecordsMapper {
CallRecordsMapper INSTANCE = Mappers.getMapper(CallRecordsMapper.class);
#Mapping(source="callRecordOperators",target = "operators")
CallRecordsDto callRecordsToCallRecordsDto(CallRecords callRecords);
public abstract CallRecordOperatorsDto toTarget(CallRecordOperators source);
List<CallRecordsDto> callRecordsToCallRecordsDtos(List<CallRecords> callRecords);
}
public class CallRecordsDto {
private String callerNumber;
private List<CallRecordOperatorsDto> operators;
//getter setters
}
public class CallRecordOperatorsDto {
private String callRecordsId;
private String operatorId;
private String operatorName;
private String currentTime;
// getter setter
}
But for above code I am getting
{
"callerNumber": "9898989898",
"operators": [{
"callRecordsId": null,
"operatorId": null,
"operatorName": null,
"currentTime": null
}, {
"callRecordsId": null,
"operatorId": null,
"operatorName": null,
"currentTime": null
}]
}
the values of operator array are null. what could be he issue?
It seems your are lacking the mappings from CallRecordOperators to CallRecordOperatorsDto:
#Mapper
public interface CallRecordsMapper {
CallRecordsMapper INSTANCE = Mappers.getMapper(CallRecordsMapper.class);
#Mapping(source="callRecordOperators",target = "operators")
CallRecordsDto callRecordsToCallRecordsDto(CallRecords callRecords);
#Mapping(target = "callRecordsId", source = "callrecord.id")
#Mapping(target = "operatorId", source = "operator.id")
#Mapping(target = "operatorName", source = "operator.name")
#Mapping(target = "currentTime", source = "startTime")
CallRecordOperatorsDto callRecordOperatorsToDto(CallRecordOperators source);
}
When you do a Hibernate query of A elements, you can fetch the related B elements of the bs collection using different strategies. Some of them are:
If you use HQL to construct your queries, you can do a JOIN FETCH or LEFT JOIN FETCH to populate the bs collection:
String hql = "SELECT DISTINCT a FROM " + A.class.getName()
+ " a LEFT JOIN FETCH a.bs WHERE ...";
This query will load all data using a single SQL query.
Use eager fetching of the bs collection, changing the #OneToMany annotation:
#OneToMany(fetch=FetchType.EAGER)
private List<B> bs;
In this case, when you run a query of A elements, a SQL query will be launched to retrieve the A data, and for each A object in the result, a SQL query will be executed to load the corresponding bs collection.
If you use Criteria to build the query, you can change the fetch mode of the bs collection in a way similar to the HQL JOIN FETCH:
Criteria c = session.createCriteria(A.class);
c.setFetchMode("bs", FetchMode.JOIN);
c.setResultTransformer(Criteria.DISTINCT_ROOT_ENTITY);
How about switching to a slightly different approach that also performs better? By using Blaze-Persistence Entity Views you can define your mapping directly on the DTO classes and apply that onto a query builder to generate efficient queries that perfectly fit your DTO structure.
#EntityView(CallRecords.class)
public interface CallRecordsDto {
// The id of the CallRecords entity
#JsonIgnore
#IdMapping("id") String getId();
String getCallerNumber();
#Mapping("callRecordOperators")
List<CallRecordOperatorsDto> getOperators();
}
#EntityView(CallRecordOperators.class)
public interface CallRecordOperatorsDto {
// The id of the CallRecordOperators entity
#JsonIgnore
#IdMapping("id") Long getId();
#Mapping("callrecord.id")
String getCallRecordId();
#Mapping("operator.id")
String getOperatorId();
#Mapping("operator.name")
String getOperatorName();
#Mapping("startTime")
String getCurrentTime();
// Whatever properties you want
}
See how you can map the entity attributes right in your DTOs? And here comes the code for querying
EntityManager entityManager = // jpa entity manager
CriteriaBuilderFactory cbf = // query builder factory from Blaze-Persistence
EntityViewManager evm = // manager that can apply entity views to query builders
CriteriaBuilder<User> builder = cbf.create(entityManager, CallRecords.class)
.where("callerNumber").eq("123456789");
List<CallRecordsDto> result = evm.applySetting(
builder,
EntityViewSetting.create(CallRecordsDto.class)
).getResultList();
Note that this will roughly generate the following optimized query
SELECT
c.id,
c.callerNumber,
o.callrecord.id,
o.id,
o.startTime,
op.id,
op.name
FROM CallRecords c
LEFT JOIN c.callRecordOperators o
LEFT JOIN o.operator op
WHERE c.callerNumber = :param_1
This is my Entity configuration
#Entity
#NamedQuery(name = "Payment.findByEmail", query = "SELECT p FROM Payment p JOIN p.additionalAuthData a " +
"WHERE KEY(a) = 'email' AND VALUE(a) = ?1 AND (p.paymentType = 4 OR p.paymentType = 10)")
public class Payment {
#Id
#GeneratedValue(strategy = GenerationType.SEQUENCE)
private Long id;
#Column(name = "payment_type")
private Integer paymentType;
/** other properties, getters and setters */
#ElementCollection
#CollectionTable(name = "additional_auth_data")
#MapKeyJoinColumn(name = "id", referencedColumnName = "id")
#MapKeyColumn(name = "field")
#Column(name = "data_value")
private Map<String, String> additionalAuthData;
}
The NamedQuery findByEmail("test#example.com") generates the following SQL
select -- all fields ...
from payment payment0_ inner join additional_auth_data additional1_ on payment0_.id=additional1_.id
where
additional1_.field='email' and (select additional1_.data_value from additional_auth_data additional1_ where payment0_.id=additional1_.id)='test#example.com' and (payment0_.payment_type=4 or payment0_.payment_type=10)
which is wrong: it may work if you have only one row but it blows up otherwise. H2 complains Scalar subquery contains more than one row and PostgreSQL more than one row returned by a subquery used as an expression. In fact, query's where condition compares a scalar value ('test#example.com') with a subquery.
The correct SQL should be:
select -- all fields
from payment payment0_ inner join additional_auth_data additional1_ on payment0_.id=additional1_.id
where additional1_.field='payerEmail' and additional1_.data_value='test#example.com' and (payment0_.payment_type=4 or payment0_.payment_type=10)
Is the HSQL correct? Is there a way to instruct Hibernate to generates a clever, better SQL? Is this a Hibernate bug?
Note: Hibernate shipped with Spring Boot Starter 1.3.7.RELEASE
Edit:
Using an #Embeddable class
#ElementCollection
#JoinTable(name = "additional_auth_data", joinColumns = #JoinColumn(name = "id"))
#MapKeyColumn(name = "field")
#Column(name = "data_value")
private Set<AdditionalData> additionalAuthData;
#Embeddable
public static class AdditionalData {
#Column(name = "field", nullable = false)
private String field;
#Column(name = "data_value")
private String dataValue;
protected AdditionalData() {
}
public AdditionalData(String field, String dataValue) {
this.field = field;
this.dataValue = dataValue;
}
/** Getters, setters; equals and hashCode on "field" */
}
#NamedQuery(name = "Payment.findByEmail", query = "SELECT p FROM Payment p JOIN p.additionalAuthData a " +
"WHERE a.field = 'email' AND a.dataValue = ?1 AND (p.paymentType = 4 OR p.paymentType = 10)")
solves the problem, and the SQL is correct, but it looks just plain wrong, like shooting a fly with a bazooka...
It generates correct SQL without value().
Use just a=?1
But I would expect is should generate it simple also with it.
I have a SQL query like this:
SELECT h.name, h.created_date, tbl.*
FROM my_table tbl
LEFT JOIN
(SELECT name, max(created_date) created_date FROM my_table GROUP BY name) h
ON tbl.name = h.name;
It returns the row from my_table (which has multiple for name="") along with the maximum created_date for that name.
Is there a way to replicate this in a JPQL query?
Here is the gist of the Entity class, it's quite simple:
#Entity
#Table(name = "MY_TABLE")
#XmlRootElement
public class MyTable implements Serializable {
private BigDecimal tableId;
private String name;
private Date createdDate;
// ...
#Id
#Basic(optional = false)
#Column(name = "TABLE_ID")
#GeneratedValue(generator = "TBL_ID_SEQ")
public BigDecimal getTableId() {
return tableId;
}
#Basic(optional = false)
#Column(name = "NAME")
public String getName() {
return name;
}
#Basic(optional = false)
#Column(name = "CREATED_DATE", insertable = false)
#Temporal(TemporalType.TIMESTAMP)
public Date getCreatedDate() {
return createdDate;
}
// ... getters/setters
}
Just reading your question I guess you do not need another entity. Entities in JPA are the same like tables in SQL. Usually there is a 1:1 relationship between entities and tables. You just have to know how to invoke a query using JPQ. You need a entity manager, which invokes your statement.
EntityManagerFactory emf = Persistence.createEntityManagerFactory("PersistenceUnit");
EntityManager em = emf.createEntityManager();
You have to define your persistence unit, i.e. in a pom file, or a config java file. So done you can go on coding something like this:
Query q = em.createQuery( "Your query in sql syntax as a string object" );
In respect to your entities and invoked query you will receive a List using
List<object> resultOfMyQuery = q.getResultList();
This is only one short example. But hopefully you got some buzzwords to look for ;)
I have 2 tables in database:
ads - represent user defined advertisements
ad_categories - represent categories for advertisements
every advertisement must belong to exactly one category, so in ads table I defined a foreign key pointing to ad_categories with ON UPDATE NO ACTION ON DELETE NO ACTION.
In my application, user must be able to delete any category, but if that category contains advertisements, they must be moved to another category before category is deleted.
em.getTransaction().begin();
// get currentNode
AdCategories currentNode = em.find(AdCategories.class, currentNodeId);
// get ads
List<Ads> resultList = em.createQuery("SELECT a from Ads a WHERE a.adCategoryId = :categoryId").setParameter("categoryId", currentNode).getResultList();
// get their new location
AdCategories newLocation = em.find(AdCategories.class, newLocationId);
// set their new location
for(Ads a: resultList)
a.setAdCategoryId(newLocation);
em.remove(currentNode);
em.getTransaction().commit();
I expected, that affected advertisements will have ad_category_id changed and then the empty category will be removed. But affected advertisements are deleted too!!
I enabled logging in EclipseLink to FINEST level and found out, that when transaction is commited, firstly, UPDATE query is sent to database, which changes ad_category_id for affected advertisements and then category is deleted, but delete is cascaed to advertisements! I dont understand why, because advertisements should have updated ad_category_ids before remove occours.
I know, one simple workaround is to call em.flush() before removing the category, but I dont think it is optimal solution. I think, I need to understand this behaviour.
I am using EclipseLink with NetBeans and PostgreSQL.
Table definitions:
AdCategories
#Entity
#Table(name = "ad_categories")
#XmlRootElement
#NamedQueries({
#NamedQuery(name = "AdCategories.findAll", query = "SELECT a FROM AdCategories a"),
#NamedQuery(name = "AdCategories.findById", query = "SELECT a FROM AdCategories a WHERE a.id = :id"),
#NamedQuery(name = "AdCategories.findByParentId", query = "SELECT a FROM AdCategories a WHERE a.parentId = :parentId"),
#NamedQuery(name = "AdCategories.findByCategoryOrder", query = "SELECT a FROM AdCategories a WHERE a.categoryOrder = :categoryOrder"),
#NamedQuery(name = "AdCategories.findByCategoryDepth", query = "SELECT a FROM AdCategories a WHERE a.categoryDepth = :categoryDepth"),
#NamedQuery(name = "AdCategories.findByName", query = "SELECT a FROM AdCategories a WHERE a.name = :name"),
#NamedQuery(name = "AdCategories.findByGrandParentId", query = "SELECT a FROM AdCategories a WHERE a.grandParentId = :grandParentId"),
#NamedQuery(name = "AdCategories.findByParentName", query = "SELECT a FROM AdCategories a WHERE a.parentName = :parentName"),
#NamedQuery(name = "AdCategories.findByGrandParentName", query = "SELECT a FROM AdCategories a WHERE a.grandParentName = :grandParentName")})
public class AdCategories implements Serializable {
#OneToMany(cascade = CascadeType.ALL, mappedBy = "adCategoryId")
private Collection<Ads> adsCollection;
private static final long serialVersionUID = 1L;
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Basic(optional = false)
#Column(name = "id")
private Integer id;
#Basic(optional = false)
#Column(name = "parent_id")
private int parentId;
#Basic(optional = false)
#Column(name = "category_order")
private short categoryOrder;
#Basic(optional = false)
#Column(name = "category_depth")
private short categoryDepth;
#Basic(optional = false)
#Column(name = "name")
private String name;
#Column(name = "grand_parent_id")
private Integer grandParentId;
#Column(name = "parent_name")
private String parentName;
#Column(name = "grand_parent_name")
private String grandParentName;
...
Ads
#Entity
#Table(name = "ads")
#XmlRootElement
#NamedQueries({
#NamedQuery(name = "Ads.findAll", query = "SELECT a FROM Ads a"),
#NamedQuery(name = "Ads.findByAdId", query = "SELECT a FROM Ads a WHERE a.adId = :adId"),
#NamedQuery(name = "Ads.findByName", query = "SELECT a FROM Ads a WHERE a.name = :name"),
#NamedQuery(name = "Ads.findByDescriptionShort", query = "SELECT a FROM Ads a WHERE a.descriptionShort = :descriptionShort"),
#NamedQuery(name = "Ads.findByDescriptionLong", query = "SELECT a FROM Ads a WHERE a.descriptionLong = :descriptionLong")})
public class Ads implements Serializable {
private static final long serialVersionUID = 1L;
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Basic(optional = false)
#Column(name = "ad_id")
private Integer adId;
#Basic(optional = false)
#Column(name = "name")
private String name;
#Basic(optional = false)
#Column(name = "description_short")
private String descriptionShort;
#Basic(optional = false)
#Column(name = "description_long")
private String descriptionLong;
#JoinColumn(name = "ad_category_id", referencedColumnName = "id")
#ManyToOne(optional = false)
private AdCategories adCategoryId;
...
The problem here is that you defined a bi-directional relationship, which needs to be manually managed (The JPA provider will not do it for you). In your calling code, you break the link between ads and their category, from the point of view of the ads.
for(Ads a: resultList)
a.setAdCategoryId(newLocation);
But, your category is still holding on to a collection of ads that it believes its related too, and when you delete it, those ads get deleted as well (because of the CascadeType.ALL annotation). There are two ways you can go about fixing this.
Keep the bidirectional relationship
If you really need to, you can leave the relationship bidirectional, but then you would have to properly disassociate the relationship on both sides, when you want to break it. It's normal to manage the relationship entirely from the 'owning' side, so I would do something like this:
public class Ads implements Serializable {
public void setAdCategoryId(AdCategories category) {
this.category.removeAd(this);
this.category = category;
this.category.addAd(this);
}
}
Very rough pseudocode, you will need to flesh it out
Remove the birectional relationship
Does a category really need to maintain a list of all ads that use it? Conceptually, I don't think it should. The list will get very large over time, and you could always query for it dynamically instead of storing it with each category. But that's a decision you have to make from a business point of view.
If you declare a cascade of type REMOVE (or ALL) on the collection of ads in AdCategory, you tell JPA: when I call remove() on an AdCategory, also call remove() on all the ads in this collection. So that's what JPA does.
You have a bidirectional association, it's your responsibility to make sure both sides of the association are in a coherent state. So if you change the category of an ad, you should also remove this ad from the set of ads in its category, and you should also add the ad to its new category. It's not absolutely mandatory in all the cases, but in yours, it is.
Also, your naming is really bad. An instance of AdCategories is a single category. So the entity should be named AdCategory. Same for Ads, which should be named Ad. The field adCategoryId doesn't contain a category ID, but a category. It should be named adCategory of category and not adCategoryId. Why name the field adId? It's the ID in the class Ad, so it's already obviously the ID of an Ad. It should thus be named id. descriptionLong should be named longDescription. That might seem like details, but those are the details that make code look good and be readable.