I'm using spring-data's repositories - very convenient thing but I faced an issue. I easily can update whole entity but I believe it's pointless when I need to update only a single field:
#Entity
#Table(schema = "processors", name = "ear_attachment")
public class EARAttachment {
private Long id;
private String originalName;
private String uniqueName;//yyyy-mm-dd-GUID-originalName
private long size;
private EARAttachmentStatus status;
to update I just call method save. In log I see the followwing:
batching 1 statements: 1: update processors.ear_attachment set message_id=100,
original_name='40022530424.dat',
size=506,
status=2,
unique_name='2014-12-16-8cf74a74-e7f3-40d8-a1fb-393c2a806847-40022530424.dat'
where id=1
I would like to see some thing like this:
batching 1 statements: 1: update processors.ear_attachment set status=2 where id=1
Spring's repositories have a lot of facilities to select something using name conventions, maybe there is something similar for update like updateForStatus(int status);
You can try something like this on your repository interface:
#Modifying
#Query("update EARAttachment ear set ear.status = ?1 where ear.id = ?2")
int setStatusForEARAttachment(Integer status, Long id);
You can also use named params, like this:
#Modifying
#Query("update EARAttachment ear set ear.status = :status where ear.id = :id")
int setStatusForEARAttachment(#Param("status") Integer status, #Param("id") Long id);
The int return value is the number of rows that where updated. You may also use void return.
See more in reference documentation.
Hibernate offers the #DynamicUpdate annotation. All we need to do is to add this annotation at the entity level:
#Entity(name = "EARAttachment ")
#Table(name = "EARAttachment ")
#DynamicUpdate
public class EARAttachment {
//Code omitted for brevity
}
Now, when you use EARAttachment.setStatus(value) and executing "CrudRepository" save(S entity), it will update only the particular field. e.g. the following UPDATE statement is executed:
UPDATE EARAttachment
SET status = 12,
WHERE id = 1
You can update use databind to map #PathVariable T entity and #RequestBody Map body. And them update body -> entity.
public static void applyChanges(Object entity, Map<String, Object> map, String[] ignoreFields) {
map.forEach((key, value) -> {
if(!Arrays.asList(ignoreFields).contains(key)) {
try {
Method getMethod = entity.getClass().getMethod(getMethodNameByPrefix("get", key));
Method setMethod = entity.getClass().getMethod(getMethodNameByPrefix("set", key), getMethod.getReturnType());
setMethod.invoke(entity, value);
} catch (IllegalAccessException | NoSuchMethodException | InvocationTargetException e) {
e.printStackTrace();
}
}
});
}
Related
I've read the question Custom method for update query with spring data MongoRepository and my result would be the very same that it is in que previously mentioned question, the only difference would be that I want to do it by using a #Query annotated method. Is it possible? If so, how?
I have a entity and I what to update all documents with a value if a determined criteria has been match.
My entity:
#Document("accrual")
public class Accrual extends AbstractEntity {
#Serial
private static final long serialVersionUID = 1175443967269096002L;
#Indexed(unique = true)
private Long numericUserId;
#Indexed(unique = true)
private Long orderIdentifier;
private boolean error;
// sets, gets, equals, hashCode and toString methods
}
I would like to update the boolean error in every document found by searching for them using a list of Longs matching orderIdentifier attribute.
If it was in a relational database I would have a method like:
#Query(value = "update accrual set error = 0 where order_identifier in :list", nativeQuery = true)
updateErrorByOrderIdentifier(#Param("list") List<Long> orderIdentifiers)
You can try to use #Query annotation for filtering documents to be updated and #Update for providing actual update query.
#Query("{ 'orderIdentifier' : { '$in': ?0 } }")
#Update("{ '$set' : { 'error' : 'false' } }")
void updateAllByOrderIdentifier(List<Long> orderIdentifiers);
More details can be found here
I am trying to implement Delete query in Spring Boot, however the parameters are optional. How do I write JPA query for same.
Here is how I have implemented for mandate Request Params:
#Transactional
#Repository
public interface ABCRepo extends CrudRepository<ABC, Long>{
public List<ABC> findByABCIdAndStartYrAndStartMonth(String pilotId, int startYr, int startMonth);
public long deleteABCByABCId(String pilotId);
}
Controller.class
#RequestMapping(value="", method= RequestMethod.DELETE)
public Response delete(#PathVariable("abc-id")String pilotId)
{
LOGGER.info("Trying to delete pilot bank using abc id : "+ abcId);
long deletedRecords=abcBiz.deleteABCByABCId(abcId);
if(deletedRecords==0)
{
throw new PilotNotFoundException("Entity not found "+abcId);
}
return Response.status(Response.Status.NO_CONTENT).entity(deletedRecords).build();
}
My new Controller.class after adding optional params
#RequestMapping(value="", method= RequestMethod.DELETE)
public Response delete(#PathVariable("abc-id")String abcId, #RequestParam(name = "bid-yr", required = false)
int bidYr, #RequestParam(name = "bid-month", required = false) int bidMonth)
{
LOGGER.info("Trying to delete pilot bank using abc id : "+ abcId);
long deletedRecords=abcBiz.deleteABCByABCId(a);bcId
if(deletedRecords==0)
{
throw new PilotNotFoundException("Entity not found "+abcId);
}
return Response.status(Response.Status.NO_CONTENT).entity(deletedRecords).build();
}
How do I handle this at JPA?
For optional parameters, you need to write the query. Something like below:
#Modifying
#Query("DELETE FROM ABC WHERE abcId=:pilotId AND (:otherOptionalParam IS NULL OR otherField=:otherOptionalParam)")
public long deleteABCByABCId(String pilotId, String otherOptionalParam);
If you want to create a complex query, with lot of optional parameters, then you can create custom repository, and develop native queries. Here I have already answered to how we can create custom repositories in Spring data JPA - https://stackoverflow.com/a/68721142/3709922
On Top of what Jignesh has said, don't forget to mark your parameters with Param annotation. Also jpa modification will return int/Integer but not long so I had to change return type too.
#Modifying
#Query("DELETE FROM ABC WHERE abcId=:pilotId AND (:otherOptionalParam IS NULL OR
otherField=:otherOptionalParam)")
public long deleteABCByABCId(#Param("pilotId")String pilotId, #Param("otherOptionalParam")String
otherOptionalParam);
I've been having this issue where I am unable to properly filter on a table using querydsl which has a nullable foreign key. I stripped down my use case into a very simple scenario.
Say we have 2 entities, MyEntity and TimeRangeEntity. My Entity only has an ID and a foreign key to the TimeRangeEntity. The TimeRangeEntity only has a start and an end time and an ID. BaseEntity, that these both extend from, only has the ID set with the #Id annotation.
#Entity
#Table(name = "TEST_OBJECT")
public class MyEntity extends BaseEntity {
#OneToOne(cascade = { CascadeType.MERGE, CascadeType.PERSIST })
private TimeRangeEntity actionTime;
public TimeRangeEntity getActionTime() {
return actionTime;
}
public void setActionTime(TimeRangeEntity actionTime) {
this.actionTime = actionTime;
}
}
#Entity
#DiscriminatorValue("static")
public class TimeRangeEntity extends BaseEntity {
#Column(name = "START_TIME")
private Instant startTime;
#Column(name = "END_TIME")
private Instant endTime;
public Instant getStartTime() {
return startTime;
}
public void setStartTime(Instant startTime) {
this.startTime = startTime;
}
public Instant getEndTime() {
return endTime;
}
public void setEndTime(Instant endTime) {
this.endTime = endTime;
}
}
I've constructed a default method in my repository to run a findAll with a predicate using querydsl to build the SQL syntax
#Repository
public interface MyRepository extends JpaRepository<MyEntity, Long>, QueryDslPredicateExecutor<MyEntity> {
default Page<MyEntity> paginateFilter(PaginationInfo info, String filter){
int page = info.getOffset() > 0 ? info.getOffset() / info.getLimit() : 0;
PageRequest pageRequest = new PageRequest(page, info.getLimit(), new Sort(new Sort.Order(info.getSortDirection(), info.getSortProperty())));
return findAll(createFilterPredicate(filter, myEntity), pageRequest);
}
default Predicate createFilterPredicate(String filter, QMyEntity root){
BooleanBuilder filterBuilder = new BooleanBuilder();
filterBuilder.or(root.id.stringValue().containsIgnoreCase(filter));
filterBuilder.or(root.actionTime.startTime.isNotNull());
return filterBuilder.getValue();
}
}
I also wrote a test that should work given the code presented. What I'm trying to do is just filter based on ID. The caveat is that the FK to the TimeRange can be null. I'll note that this a contrived example to get my point across and the solution can't really be "just enforce the FK is not null."
#RunWith(SpringRunner.class)
#DataJpaTest(showSql = false)
#ContextConfiguration(classes = TestConfig.class)
#ActiveProfiles("test")
public class MyRepositoryTest {
#Autowired
private MyRepository sut;
private static final int count = 3;
#Before
public void setup(){
for (int i = 0; i < count; i++){
sut.save(new MyEntity());
}
}
#Test
public void testPaginationWithStringFilter(){
PaginationInfo info = new PaginationInfo();
info.setOffset(0);
info.setLimit(10);
info.setSortDirection(Sort.Direction.ASC);
info.setSortProperty("id");
Page<MyEntity> page = sut.paginateFilter(info, "1");
assertEquals(1, page.getTotalElements());
page = sut.paginateFilter(info, "10");
assertEquals(0, page.getTotalElements());
}
}
The problem that I'm running into is that it isn't filtering on the ID if the FK is null. All I'm doing when I save is setting the ID. I know the problem is because I can see the filtering work properly when I comment out the line filterBuilder.or(root.actionTime.startTime.isNotNull()); but it doesn't work when I have that in.
This generates the following queries. The first is for the "working" filtering where I can filter based on ID (line commented out). The second is for the filtering with the actionTime included.
select myentity0_.id as id2_38_, myentity0_.action_time_id as action_t3_38_ from test_object myentity0_ where lower(cast(myentity0_.id as char)) like ? escape '!' order by myentity0_.id asc limit ?
select myentity0_.id as id2_38_, myentity0_.action_time_id as action_t3_38_ from test_object myentity0_ cross join time_range_entity timerangee1_ where myentity0_.action_time_id=timerangee1_.id and (lower(cast(myentity0_.id as char)) like ? escape '!' or timerangee1_.start_time is not null) order by myentity0_.id asc limit ?
Looking at this, I'm almost certain that this is due to the snipper cross join time_range_entity timerangee1_ where myentity0_.action_time_id=timerangee1_.id since it validates that the entities match, which they cannot if the range foreign key is null.
I've been pulling my hair out trying to get this conditional working that only checks the time range's table properties IF the FK is not null but I cannot find a way using querydsl. Any advice/guidance/code snippets would be stellar.
EDIT: Just translating to straight SQL, I got this query for the generated JPQL(translated to this example since I used it with real data):
select * from test_object cross join time_range range where test_object.action_time_id=range.id and lower(cast(test_object.id as char)) like '%1%';
With a null FK, that didn't return a row as expected. Changing this to a left join from a cross join ended up working properly.
select * from test_object left join time_range on test_object.action_time_id=time_range.id where lower(cast(test_object.id as char)) like '%1%';
With that, is there any way to specify a left join with the querydsl predicate executor? This seems like it'd be the solution to my problem!
Try to use Specification instead of Predicate
private Specification<QMyEntity> createFilterPredicate(final String filter, final QMyEntity root) {
return new Specification<QMyEntity>() {
#Nullable
#Override
public Predicate toPredicate(Root<QMyEntity> root, CriteriaQuery<?> query,
CriteriaBuilder criteriaBuilder) {
Join<Object, Object> actionTime = root.join("actionTime", JoinType.LEFT);
return criteriaBuilder.or(criteriaBuilder.like(criteriaBuilder.lower(root.get("id")), "%" + filter + "%"), criteriaBuilder.isNotNull(actionTime.get("startTime")));
}
};
}
I have an Enum class which has some values.
We've decided to remove one of these values and its all implementation from the code.
We dont want to delete any records from DB.
My Enum class is something like this:
public enum CourseType {
VIDEO("CourseType.VIDEO"),
PDF("CourseType.PDF"),
QUIZ("CourseType.QUIZ"),
SURVEY("CourseType.SURVEY"),
POWERPOINT("CourseType.POWERPOINT") //*this one will be removed*
...
}
My Course Entity:
#Entity
#Table(name = "CRS")
public class Course {
#Column(name = "COURSE_TYPE")
#Enumerated(EnumType.STRING)
private CourseType courseType;
#Column(name = "AUTHOR")
private String author;
....
#Override
public CourseType getCourseType() {
return courseType;
}
#Override
public void setCourseType(CourseType courseType) {
this.courseType = courseType;
}
....
}
After I removed the Powerpoint type from the Java Class and tried to fetch some values from the DB,
I get a mapping error for the removed type.
I have a code like this:
Course course = courseService.get(id);
If I gave a course id which its type is 'POWERPOINT' in the database,
the method gets the following error:
java.lang.IllegalArgumentException: Unknown name value [POWERPOINT]
for enum class [com.tst.enums.CourseType] at
org.hibernate.type.EnumType$NamedEnumValueMapper.fromName(EnumType.java:461)
at
org.hibernate.type.EnumType$NamedEnumValueMapper.getValue(EnumType.java:449)
at org.hibernate.type.EnumType.nullSafeGet(EnumType.java:107) at
org.hibernate.type.CustomType.nullSafeGet(CustomType.java:127) at
org.hibernate.type.AbstractType.hydrate(AbstractType.java:106) at
org.hibernate.persister.entity.AbstractEntityPersister.hydrate(AbstractEntityPersister.java:2912)
at org.hibernate.loader.Loader.loadFromResultSet(Loader.java:1673)
Is there any way when I try to retrieve a query result from DB,
hibernate will not fetch if that records' course_type column doesn't match with the any of the enum values in the code?
Do I have to use some kind of filter?
You can try use annotation #filter
#Filter(name = "myFilter", condition = "courseType <> 'CourseType.POWERPOINT'")
and enable it
session.enableFilter("myFilter")
If you can't use filters,
something like the following should work:
Add POWERPOINT back into the enum.
Add a deleted flag to the POWERPOINT enum value.
After the course list is loaded, remove courses that have a deleted courseType value.
New CourseType enum:
public enum CourseType
{
VIDEO("CourseType.VIDEO", false),
POWERPOINT("CourseType.POWERPOINT", true);
private boolean deletedFlag;
public CourseType(
existingParameter, // do whatever you are currently doing with this parameter
deletedFlagValue)
{
// code to handle existing parameter
deletedFlag = deletedFlagValue;
}
I have found the need to limit the size of a child collection by a property in the child class.
I have the following after following this guide:
#FilterDef(name="dateFilter", parameters=#ParamDef( name="fromDate", type="date" ) )
public class SystemNode implements Serializable {
#Getter
#Setter
#Builder.Default
// "startTime" is a property in HealthHistory
#Filter(name = "dateFilter", condition = "startTime >= :fromDate")
#OneToMany(mappedBy = "system", targetEntity = HealthHistory.class, fetch = FetchType.LAZY)
private Set<HealthHistory> healthHistory = new HashSet<HealthHistory>();
public void addHealthHistory(HealthHistory health) {
this.healthHistory.add(health);
health.setSystem(this);
}
}
However, I don't really understand how to toggle this filter when using Spring Data JPA. I am fetching my parent entity like this:
public SystemNode getSystem(UUID uuid) {
return systemRepository.findByUuid(uuid)
.orElseThrow(() -> new EntityNotFoundException("Could not find system with id " + uuid));
}
And this method in turn calls the Spring supported repository interface:
public interface SystemRepository extends CrudRepository<SystemNode, UUID> {
Optional<SystemNode> findByUuid(UUID uuid);
}
How can I make this filter play nicely together with Spring? I would like to activate it programatically when I need it, not globally. There are scenarios where it would be viable to disregard the filter.
I am using Spring Boot 1.3.5.RELEASE, I cannot update this at the moment.
Update and solution
I tried the following as suggested to me in the comments above.
#Autowired
private EntityManager entityManager;
public SystemNode getSystemWithHistoryFrom(UUID uuid) {
Session session = entityManager.unwrap(Session.class);
Filter filter = session.enableFilter("dateFilter");
filter.setParameter("fromDate", new DateTime().minusHours(4).toDate());
SystemNode systemNode = systemRepository.findByUuid(uuid)
.orElseThrow(() -> new EntityNotFoundException("Could not find system with id " + uuid));
session.disableFilter("dateFilter");
return systemNode;
}
I also had the wrong type in the FilterDef annotation:
#FilterDef(name="dateFilter", parameters=#ParamDef( name="fromDate", type="timestamp" ) )
I changed from date to timestamp.
This returns the correct number of objects, verified against the database.
Thank you!