I have selection parameters on flat data, only don't know how to either omit a parameter entirely, or make it a complete wildcard. The search might use one or all parameters. How is this done? With ANY or ALL? Or, is there another way?
I would like to use one general query with all the paremeters, and pass in "all" or "any", something along those lines, for some of those parameters.
existing code:
package legacy.database;
import java.sql.Timestamp;
import java.util.List;
import java.util.logging.Logger;
import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.Persistence;
import javax.persistence.Query;
public class MyQueries {
private static final Logger log = Logger.getLogger(MyQueries.class.getName());
private final EntityManagerFactory emf = Persistence.createEntityManagerFactory("LegacyDatabasePU");
private final EntityManager em = emf.createEntityManager();
public MyQueries() {
}
public List<Clients> findAll() {
Query q = em.createQuery("select c from Clients c");
List<Clients> clients = q.getResultList();
return clients;
}
public List<Clients> selectWithParameters(Criteria c) {
log.info(c.toString());
String opener = c.getOpener();
String closer1 = c.getCloser1();
String status = c.getStatus();
Query q = em.createQuery(
"SELECT c FROM Clients c "
+ "WHERE c.status like :status "
+ "and c.closer1 like :closer1 "
+ "and c.opener like :opener");
q.setParameter("opener", opener);
q.setParameter("closer1", closer1);
q.setParameter("status", status);
log.info(q.toString());
List<Clients> clients = q.getResultList();
log.fine(clients.toString());
return clients;
}
public Clients findById(int id) {
Clients client = em.find(Clients.class, id);
return client;
}
public void send(int id) {
Clients c = em.find(Clients.class, id);
java.util.Date date = new java.util.Date();
Timestamp t = new Timestamp(date.getTime());
em.getTransaction().begin();
c.setDateUpdated(t.toString());
em.getTransaction().commit();
}
}
In case the parameters are optional, the criteria API provides some more flexibility.
If the selectWithParameters is called often, consider using parameters, since the DB can cache the parametrized query then.
selectWithParameters with optional parameters reads like this:
public List<Clients> selectWithParameters(Criteria criteria) {
log.info(criteria.toString());
String opener = criteria.getOpener();
String closer1 = criteria.getCloser1();
String status = criteria.getStatus();
CriteriaBuilder criteriaBuilder = em.getCriteriaBuilder();
CriteriaQuery<Clients> query = criteriaBuilder.createQuery(Clients.class);
Root<Clients> c = query.from(Clients.class);
List<Predicate> wherePredicates = new LinkedList<Predicate>();
if (null != status) {
wherePredicates.add(criteriaBuilder.like(c.get("status"), status));
}
if (null != closer1) {
wherePredicates.add(criteriaBuilder.like(c.get("closer1"), closer1));
}
if (null != opener) {
wherePredicates.add(criteriaBuilder.like(c.get("opener"), opener));
}
query.where(wherePredicates.toArray(new Predicate[0]));
List<Clients> clients = em.createQuery(query).getResultList();
log.fine(clients.toString());
return clients;
}
Thank you, Heiner. This worked, not sure why I had trouble with Heiner's code, but his sample put me in the right direction:
public List<Clients> selectByCriteria(Criteria criteria) {
CriteriaBuilder criteriaBuilder = em.getCriteriaBuilder();
CriteriaQuery<Clients> clientCriteriaQuery = criteriaBuilder.createQuery(Clients.class);
Root<Clients> clientRoot = clientCriteriaQuery.from(Clients.class);
clientCriteriaQuery.select(clientRoot);
List<Predicate> predicates = new ArrayList<>();
predicates.add(criteriaBuilder.like(clientRoot.get(Clients_.phone1), "%" + criteria.getPhone1() + "%"));
if (!criteria.getOpener().equalsIgnoreCase("all")) {
predicates.add(criteriaBuilder.like(clientRoot.get(Clients_.opener), "%" + criteria.getOpener() + "%"));
}
if (!criteria.getCloser1().equalsIgnoreCase("all")) {
predicates.add(criteriaBuilder.like(clientRoot.get(Clients_.closer1), "%" + criteria.getCloser1() + "%"));
}
if (!criteria.getStatus().equalsIgnoreCase("all")) {
predicates.add(criteriaBuilder.like(clientRoot.get(Clients_.status), "%" + criteria.getStatus() + "%"));
}
clientCriteriaQuery.where(predicates.toArray(new Predicate[0]));
List<Clients> clients = em.createQuery(clientCriteriaQuery).getResultList();
return clients;
}
There's probably no substantive difference (?) from what Heiner answered. JPA and JPQL are a bit murky. I can't believe it, but I almost prefer SQL! I'll have to adjust.
Related
i've come across a problem in these days, which would be simple for other languages, like php, but the project I'm doing is in Spring MVC.
The question is: In Spring MVC, how can i delete an entity with two attributes ids coming from this entity?
Example: "Delete from Entity Where id1 =: id1 and id2 =: id2" (This is the query that i want)
Thanks for the attention.
What i was trying ...
public boolean remover(int idUsuario, int idCategoria) {
EntityManagerFactory factory = Persistence.createEntityManagerFactory(PERSISTENCE_UNIT);
EntityManager manager = factory.createEntityManager();
String hqlStr = "delete from UsuarioEscolheCategoria where idUsuario = :idUsuario and idCategoria = :idCategoria";
Query query = null;
try {
query = manager.createQuery(hqlStr);
query.setParameter("idUsuario", idUsuario);
query.setParameter("idCategoria", idCategoria);
query.executeUpdate();
manager.close();
factory.close();
return true;
}catch(Exception e) {
return false;
}
}
If i take the exception, it gives me:
String hqlStr = "delete from UsuarioEscolheCategoria where usuario.idUsuario = :idUsuario and categoria.idCategoria = :idCategoria";
The important part is usuario.idUsuario and categoria.idCategoria. That way you're making a reference to the attribute type Usuario, which is on your model class.
you prblem is the session factory, check how you have created it, here a simple usefull example:
import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.springframework.beans.factory.annotation.Autowired;
#Autowired
private SessionFactory sessionFactory;
protected Session getSession() {
return sessionFactory.getCurrentSession();
}
public void deleteById(Integer id) {
Query query = getSession().createSQLQuery("delete from TABLE where id = :t_id");
query.setInteger("t_id", id);
query.executeUpdate();
}
I'm trying to use SpEL has in this document
https://spring.io/blog/2014/07/15/spel-support-in-spring-data-jpa-query-definitions to do a query filtering results by the ?#{principal.id}
The problem is that Spring returns an exception
org.hibernate.QueryException: Not all named parameters have been set:
[1] [select p , p.store, p.category from Product p JOIN p.store s
JOIN p.category c WHERE p.store.id = :id AND p.keywords LIKE :keyword
AND p.store.ownerId = ?1 ]; nested exception is
java.lang.IllegalArgumentException: org.hibernate.QueryException: Not
all named parameters have been set: [1] [select p , p.store,
p.category from Product p JOIN p.store s JOIN p.category c WHERE
p.store.id = :id AND p.keywords LIKE :keyword AND p.store.ownerId = ?1
]
I have setup the following code and it is being executed.
#Service
public class SecurityEvaluationContextExtension extends EvaluationContextExtensionSupport {
#Override
public String getExtensionId() {
return "security";
}
#Override
public SecurityExpressionRoot getRootObject() {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
System.out.println("SER >>>>> " + authentication.getPrincipal().toString() + " -- " );
return new SecurityExpressionRoot(authentication) {};
}
}
#Configuration
#EnableJpaRepositories
public class SecurityConfiguration {
#Bean
EvaluationContextExtension securityExtension() {
return new SecurityEvaluationContextExtension();
}
}
I'm using this server has a Spring Resource Server and so using the translation bellow to get the Id from the Authorization server. I confirm the code is executing and translating well but I'm getting the exception above.
#Service
public class myPrincipalExtractor implements PrincipalExtractor {
#Override
public UserInfo extractPrincipal(Map<String, Object> map) {
Map<String,Object> principal = null;
if (map.containsKey("principal")) {
principal = (Map<String, Object>) map.get("principal");
}
UserInfo user = new UserInfo();
if (principal != null ) {
if (principal.containsKey("id")) {
user.setId(Long.parseLong(principal.get("id").toString()));
}
if (principal.containsKey("username")) {
user.setUsername(principal.get("username").toString());
}
if (principal.containsKey("email")) {
user.setEmail(principal.get("email").toString());
}
}
System.out.println("----> " + user.getUsername() + " -> " + user.getId());
return user;
}
}
The query is...
#CrossOrigin
public interface StoreRepository extends CrudRepository<Store, Long>
{
#Query("select p , p.store, p.category from Product p JOIN p.store s " +
" JOIN p.category c " +
" WHERE p.store.id = :id AND p.keywords LIKE %:keyword% AND p.store.ownerId = ?#{principal.id} ")
List<Product> findByKeywordIgnoreCase(#Param("id") Long id , #Param("keyword") String keyword);
}
More Information:
I did this code bellow in the SecurityExpressionRoot so now I know this is really getting called when I place the SpEL and the ID and Username exist in the object. Also tried casting the returned object to Principal and the same problem happened.
AND p.store.ownerId = ?#{principal.id}
return new SecurityExpressionRoot(authentication) {
#Override
public UserInfo getPrincipal() {
System.out.println("Fetching the principal has user " + authentication.getPrincipal().toString());
return (UserInfo) authentication.getPrincipal();
}
};
In the website example (https://spring.io/blog/2014/07/15/spel-support-in-spring-data-jpa-query-definitions) they specify you can use ?#{principal.id} and that really gets called when executing the code above but it fails on the #Query binding for some reason.
However I tried running it with another example case
#Query("select p , p.store, p.category from Product p JOIN p.store s " +
" JOIN p.category c " +
" WHERE p.store.id = :id AND p.keywords LIKE %:keyword% AND p.store.ownerId = ?#{#security.principal.id} ")
And this worked.
I found this other example here:
https://github.com/spring-projects/spring-data-examples/blob/master/jpa/security/src/main/java/example/springdata/jpa/security/SecureBusinessObjectRepository.java
I have a Java Spring based web application and I want to insert a record to a table only if the table does not contain any rows that are "similar" (according to some specific, irrelevant criteria) to the new row.
Because this is a multi-threaded environment, I cannot use a SELECT+INSERT two-step combination as it would expose me to a race condition.
The same question was first asked and answered here and here several years ago. Unfortunately, the questions have got only a little attention and the provided answer is not sufficient to my needs.
Here's the code I currently have and it's not working:
#Component("userActionsManager")
#Transactional
public class UserActionsManager implements UserActionsManagerInterface {
#PersistenceContext(unitName = "itsadDB")
private EntityManager manager;
#Resource(name = "databaseManager")
private DB db;
...
#SuppressWarnings("unchecked")
#Override
#PreAuthorize("hasRole('ROLE_USER') && #username == authentication.name")
public String giveAnswer(String username, String courseCode, String missionName, String taskCode, String answer) {
...
List<Submission> submissions = getAllCorrectSubmissions(newSubmission);
List<Result> results = getAllCorrectResults(result);
if (submissions.size() > 0
|| results.size() > 0) throw new SessionAuthenticationException("foo");
manager.persist(newSubmission);
manager.persist(result);
submissions = getAllCorrectSubmissions(newSubmission);
results = getAllCorrectResults(result);
for (Submission s : submissions) manager.lock(s, LockModeType.OPTIMISTIC_FORCE_INCREMENT);
for (Result r : results ) manager.lock(r, LockModeType.OPTIMISTIC_FORCE_INCREMENT);
manager.flush();
...
}
#SuppressWarnings("unchecked")
private List<Submission> getAllCorrectSubmissions(Submission newSubmission) {
Query q = manager.createQuery("SELECT s FROM Submission AS s WHERE s.missionTask = ?1 AND s.course = ?2 AND s.user = ?3 AND s.correct = true");
q.setParameter(1, newSubmission.getMissionTask());
q.setParameter(2, newSubmission.getCourse());
q.setParameter(3, newSubmission.getUser());
return (List<Submission>) q.getResultList();
}
#SuppressWarnings("unchecked")
private List<Result> getAllCorrectResults(Result result) {
Query q = manager.createQuery("SELECT r FROM Result AS r WHERE r.missionTask = ?1 AND r.course = ?2 AND r.user = ?3");
q.setParameter(1, result.getMissionTask());
q.setParameter(2, result.getCourse());
q.setParameter(3, result.getUser());
return (List<Result>) q.getResultList();
}
...
}
According to the answer provided here I am supposed to somehow use OPTIMISTIC_FORCE_INCREMENT but it's not working. I suspect that the provided answer is erroneous so I need a better one.
edit:
Added more context related code. Right now this code still has a race condition. When I make 10 simultaneous HTTP POST requests approximately 5 rows will get erroneously inserted. Other 5 requests are rejected with HTTP error code 409 (conflict). The correct code would guarantee that only 1 row would get inserted to the database no matter how many concurrent requests I make. Making the method synchronous is not a solution since the race condition still manifests for some unknown reason (I tested it).
Unfortunately after several days of research I was unable to find a short and simple solution to my problem. Since my time budget is not unlimited I had to come up with a workaround. Call it a kludge if you may.
Since the whole HTTP request is a transaction, it will be rolled back at the sight of any conflicts. I am using this for my advantage by locking a special entity within the context of the whole HTTP request. Should multiple HTTP requests be received at the same time, all but one will result in some PersistenceException.
In the beginning of the transaction I am checking whether no other correct answers have been submitted yet. During that check the lock is already effective so no race condition could happen. The lock is effective until the answer is submitted. This basically simulates a critical section as a SELECT+INSERT two step query on the application level (in pure MySQL I would have used the INSERT IF NOT EXISTS construct).
This approach has some drawbacks. Whenever two students submit an answer at the same time, one of them will be thrown an exception. This is sort of bad for performance and bandwidth because the student who received HTTP STATUS 409 has to resubmit their answer.
To compensate the latter, I am automatically retrying to submit the answer on the server side a couple of times between randomly chosen time intervals. See the according HTTP request controller code is below:
#Controller
#RequestMapping("/users")
public class UserActionsController {
#Autowired
private SessionRegistry sessionRegistry;
#Autowired
#Qualifier("authenticationManager")
private AuthenticationManager authenticationManager;
#Resource(name = "userActionsManager")
private UserActionsManagerInterface userManager;
#Resource(name = "databaseManager")
private DB db;
.
.
.
#RequestMapping(value = "/{username}/{courseCode}/missions/{missionName}/tasks/{taskCode}/submitAnswer", method = RequestMethod.POST)
public #ResponseBody
Map<String, Object> giveAnswer(#PathVariable String username,
#PathVariable String courseCode, #PathVariable String missionName,
#PathVariable String taskCode, #RequestParam("answer") String answer, HttpServletRequest request) {
init(request);
db.log("Submitting an answer to task `"+taskCode+"` of mission `"+missionName+
"` in course `"+courseCode+"` as student `"+username+"`.");
String str = null;
boolean conflict = true;
for (int i=0; i<10; i++) {
Random rand = new Random();
int ms = rand.nextInt(1000);
try {
str = userManager.giveAnswer(username, courseCode, missionName, taskCode, answer);
conflict = false;
break;
}
catch (EntityExistsException e) {throw new EntityExistsException();}
catch (PersistenceException e) {}
catch (UnexpectedRollbackException e) {}
try {
Thread.sleep(ms);
} catch(InterruptedException ex) {
Thread.currentThread().interrupt();
}
}
if (conflict) str = userManager.giveAnswer(username, courseCode, missionName, taskCode, answer);
if (str == null) db.log("Answer accepted: `"+answer+"`.");
else db.log("Answer rejected: `"+answer+"`.");
Map<String, Object> hm = new HashMap<String, Object>();
hm.put("success", str == null);
hm.put("message", str);
return hm;
}
}
If for some reason the controller is unable to commit the transaction 10 times in a row then it will try one more time but will not attempt to catch the possible exceptions. When an exception is thrown on the 11th try then it will be processed by the global exception controller and the client will receive HTTP STATUS 409. The global exception controller is defined below.
#ControllerAdvice
public class GlobalExceptionController {
#Resource(name = "staticDatabaseManager")
private StaticDB db;
#ExceptionHandler(SessionAuthenticationException.class)
#ResponseStatus(value=HttpStatus.FORBIDDEN, reason="session has expired") //403
public ModelAndView expiredException(HttpServletRequest request, Exception e) {
ModelAndView mav = new ModelAndView("exception");
mav.addObject("name", e.getClass().getSimpleName());
mav.addObject("message", e.getMessage());
return mav;
}
#ExceptionHandler({UnexpectedRollbackException.class,
EntityExistsException.class,
OptimisticLockException.class,
PersistenceException.class})
#ResponseStatus(value=HttpStatus.CONFLICT, reason="conflicting requests") //409
public ModelAndView conflictException(HttpServletRequest request, Exception e) {
ModelAndView mav = new ModelAndView("exception");
mav.addObject("name", e.getClass().getSimpleName());
mav.addObject("message", e.getMessage());
synchronized (db) {
db.setUserInfo(request);
db.log("Conflicting "+request.getMethod()+" request to "+request.getRequestURI()+" ("+e.getClass().getSimpleName()+").", Log.LVL_SECURITY);
}
return mav;
}
//ResponseEntity<String> customHandler(Exception ex) {
// return new ResponseEntity<String>("Conflicting requests, try again.", HttpStatus.CONFLICT);
//}
}
Finally, the giveAnswer method itself utilizes a special entity with a primary key lock_addCorrectAnswer. I lock that special entity with the OPTIMISTIC_FORCE_INCREMENT flag which makes sure that no two transactions can have overlapping execution times for the giveAnswer method. The respective code can be seen below:
#Component("userActionsManager")
#Transactional
public class UserActionsManager implements UserActionsManagerInterface {
#PersistenceContext(unitName = "itsadDB")
private EntityManager manager;
#Resource(name = "databaseManager")
private DB db;
.
.
.
#SuppressWarnings("unchecked")
#Override
#PreAuthorize("hasRole('ROLE_USER') && #username == authentication.name")
public String giveAnswer(String username, String courseCode, String missionName, String taskCode, String answer) {
.
.
.
if (!userCanGiveAnswer(user, course, missionTask)) {
error = "It is forbidden to submit an answer to this task.";
db.log(error, Log.LVL_MAJOR);
return error;
}
.
.
.
if (correctAnswer) {
.
.
.
addCorrectAnswer(newSubmission, result);
return null;
}
newSubmission = new Submission(user, course, missionTask, answer, false);
manager.persist(newSubmission);
return error;
}
private void addCorrectAnswer(Submission submission, Result result) {
String var = "lock_addCorrectAnswer";
Global global = manager.find(Global.class, var);
if (global == null) {
global = new Global(var, 0);
manager.persist(global);
manager.flush();
}
manager.lock(global, LockModeType.OPTIMISTIC_FORCE_INCREMENT);
manager.persist(submission);
manager.persist(result);
manager.flush();
long submissions = getCorrectSubmissionCount(submission);
long results = getResultCount(result);
if (submissions > 1 || results > 1) throw new EntityExistsException();
}
private long getCorrectSubmissionCount(Submission newSubmission) {
Query q = manager.createQuery("SELECT count(s) FROM Submission AS s WHERE s.missionTask = ?1 AND s.course = ?2 AND s.user = ?3 AND s.correct = true");
q.setParameter(1, newSubmission.getMissionTask());
q.setParameter(2, newSubmission.getCourse());
q.setParameter(3, newSubmission.getUser());
return (Long) q.getSingleResult();
}
private long getResultCount(Result result) {
Query q = manager.createQuery("SELECT count(r) FROM Result AS r WHERE r.missionTask = ?1 AND r.course = ?2 AND r.user = ?3");
q.setParameter(1, result.getMissionTask());
q.setParameter(2, result.getCourse());
q.setParameter(3, result.getUser());
return (Long) q.getSingleResult();
}
}
It is important to note that the entity Global has to have a version annotation in its class for the OPTIMISTIC_FORCE_INCREMENT to work (see code below).
#Entity
#Table(name = "GLOBALS")
public class Global implements Serializable {
.
.
.
#Id
#Column(name = "NAME", length = 32)
private String key;
#Column(name = "INTVAL")
private int intVal;
#Column(name = "STRVAL", length = 4096)
private String strVal;
#Version
private Long version;
.
.
.
}
Such an approach can be optimized even further. Instead of using the same lock name lock_addCorrectAnswer for all giveAnswer calls, I could generate the lock name deterministically from the name of the submitting user. For example, if the student's username is Hyena then the primary key for the lock entity would be lock_Hyena_addCorrectAnswer. That way multiple students could submit answers at the same time without receiving any conflicts. However, if a malicious user spams the HTTP POST method for submitAnswer 10x in parallel they will be prevented by the this locking mechanism.
after generating Endpoint from a model class I edit the ClassnameEndpoint file to make a method that will return a list of Entities using a modified SELECT string.
Somehow this makes a clash and Generate Client Libraries fails like this:
INFO: Successfully processed C:\Users\1177\AndroidStudioProjects\N5\appN5-AppEngine\target/generated-sources/appengine-endpoints\WEB-INF/appengine-web.xml
[ERROR]
com.google.api.server.spi.config.validation.ApiConfigInvalidException: Multiple methods with same rest path "GET eventdata": "listUserEventData" and "listEventData"
here is the ClassnameEndpoint.java, which is edited by hand after being generated:
note listEventData and listUserEventData are identical except for SELECT string...
So how can I create an endpoint method to get ALL the entities and another method to get SOME entities without the clash?
package sic.example.appn5;
import com.google.api.server.spi.config.Api;
import com.google.api.server.spi.config.ApiMethod;
import com.google.api.server.spi.config.ApiNamespace;
import com.google.api.server.spi.response.CollectionResponse;
import com.google.appengine.api.datastore.Cursor;
import com.google.appengine.datanucleus.query.JPACursorHelper;
import java.util.List;
import javax.annotation.Nullable;
import javax.inject.Named;
import javax.persistence.EntityExistsException;
import javax.persistence.EntityNotFoundException;
import javax.persistence.EntityManager;
import javax.persistence.Query;
#Api(name = "eventdataendpoint", namespace = #ApiNamespace(ownerDomain = "example.sic", ownerName = "example.sic", packagePath = "appn5"))
public class EventDataEndpoint {
/**
* This method lists all the entities inserted in datastore.
* It uses HTTP GET method and paging support.
*
* #return A CollectionResponse class containing the list of all entities
* persisted and a cursor to the next page.
*/
#SuppressWarnings({"unchecked", "unused"})
#ApiMethod(name = "listEventData")
public CollectionResponse<EventData> listEventData(
#Nullable #Named("cursor") String cursorString,
#Nullable #Named("limit") Integer limit) {
EntityManager mgr = null;
List<EventData> execute = null;
try {
mgr = getEntityManager();
Query query = mgr.createQuery("select from EventData as EventData");
Cursor cursor;
if (cursorString != null && cursorString.trim().length() > 0) {
cursor = Cursor.fromWebSafeString(cursorString);
query.setHint(JPACursorHelper.CURSOR_HINT, cursor);
}
if (limit != null) {
query.setFirstResult(0);
query.setMaxResults(limit);
}
execute = (List<EventData>) query.getResultList();
cursor = JPACursorHelper.getCursor(execute);
if (cursor != null) cursorString = cursor.toWebSafeString();
// Tight loop for fetching all entities from datastore and accomodate
// for lazy fetch.
for (EventData obj : execute) ;
} finally {
if (mgr != null) {
mgr.close();
}
}
return CollectionResponse.<EventData>builder()
.setItems(execute)
.setNextPageToken(cursorString)
.build();
}
#SuppressWarnings({"unchecked", "unused"})
#ApiMethod(name = "listUserEventData")
public CollectionResponse<EventData> listUserEventData(
#Nullable #Named("username") String username,
#Nullable #Named("cursor") String cursorString,
#Nullable #Named("limit") Integer limit) {
EntityManager mgr = null;
List<EventData> execute = null;
try {
mgr = getEntityManager();
Query query = mgr.createQuery(String.format("select from EventData as EventData where BelongsTo = '%s'", username));
Cursor cursor;
if (cursorString != null && cursorString.trim().length() > 0) {
cursor = Cursor.fromWebSafeString(cursorString);
query.setHint(JPACursorHelper.CURSOR_HINT, cursor);
}
if (limit != null) {
query.setFirstResult(0);
query.setMaxResults(limit);
}
execute = (List<EventData>) query.getResultList();
cursor = JPACursorHelper.getCursor(execute);
if (cursor != null) cursorString = cursor.toWebSafeString();
// Tight loop for fetching all entities from datastore and accomodate
// for lazy fetch.
for (EventData obj : execute) ;
} finally {
if (mgr != null) {
mgr.close();
}
}
return CollectionResponse.<EventData>builder()
.setItems(execute)
.setNextPageToken(cursorString)
.build();
}
/**
* This method gets the entity having primary key id. It uses HTTP GET method.
*
* #param id the primary key of the java bean.
* #return The entity with primary key id.
*/
#ApiMethod(name = "getEventData")
public EventData getEventData(#Named("id") String id) {
EntityManager mgr = getEntityManager();
EventData eventData = null;
try {
eventData = mgr.find(EventData.class, id);
} finally {
mgr.close();
}
return eventData;
}
/**
* This inserts a new entity into App Engine datastore. If the entity already
* exists in the datastore, an exception is thrown.
* It uses HTTP POST method.
*
* #param eventData the entity to be inserted.
* #return The inserted entity.
*/
#ApiMethod(name = "insertEventData")
public EventData insertEventData(EventData eventData) {
EntityManager mgr = getEntityManager();
try {
if (containsEventData(eventData)) {
throw new EntityExistsException("Object already exists");
}
mgr.persist(eventData);
} finally {
mgr.close();
}
return eventData;
}
/**
* This method is used for updating an existing entity. If the entity does not
* exist in the datastore, an exception is thrown.
* It uses HTTP PUT method.
*
* #param eventData the entity to be updated.
* #return The updated entity.
*/
#ApiMethod(name = "updateEventData")
public EventData updateEventData(EventData eventData) {
EntityManager mgr = getEntityManager();
try {
if (!containsEventData(eventData)) {
throw new EntityNotFoundException("Object does not exist");
}
mgr.persist(eventData);
} finally {
mgr.close();
}
return eventData;
}
/**
* This method removes the entity with primary key id.
* It uses HTTP DELETE method.
*
* #param id the primary key of the entity to be deleted.
* #return The deleted entity.
*/
#ApiMethod(name = "removeEventData")
public EventData removeEventData(#Named("id") String id) {
EntityManager mgr = getEntityManager();
EventData eventData = null;
try {
eventData = mgr.find(EventData.class, id);
mgr.remove(eventData);
} finally {
mgr.close();
}
return eventData;
}
private boolean containsEventData(EventData eventData) {
EntityManager mgr = getEntityManager();
boolean contains = true;
try {
EventData item = mgr.find(EventData.class, eventData.getEventKey());
if (item == null) {
contains = false;
}
} finally {
mgr.close();
}
return contains;
}
private static EntityManager getEntityManager() {
return EMF.get().createEntityManager();
}
}
The end of the automatically generated path looks like this: _ah/api/endpoint_name/version_name/return_type/{named_parameter_1}/{named_parameter_2}/…
The problem you're facing is that both methods are in the same endpoint class, of the same version, have the same return type, and use GET. Therefore, the URLs will be identical and conflicting. In this case, they're both _ah/api/eventdataendpoint/v1/eventdata.
The solution is to add the path attribute to one of the classes, like so:
#ApiMethod(name = "listUserEventData", path="eventdata/user")
public CollectionResponse<EventData> listUserEventData(…
Now, the first method with have URL _ah/api/eventdataendpoint/v1/eventdata and the second one will have path _ah/api/eventdataendpoint/v1/eventdata/user.
As you increase the number of methods in your endpoint, you will come across conflicts like this a lot, so if you plan on making many new methods, it's a good idea to use the path attribute every time rather than rely on CE to auto-generate a path.
Edit: you'll find a lot of info pertaining to your API and each method's path at YOUR_APP_ID.appspot.com/_ah/api/discovery/v1/apis/ENDPOINT_NAME/v1/rest
The Answer given by #willlma is definately right !
But a very simple way of fixing this temporarily and avoid a lot of code change is to add another named parameter to be passed in the method.
public Event getFirstEvent(#Named("mainEventId") Long mainEventId,
#Named("useless") Boolean useless ,
User auth) throws UnauthorizedException {
if (auth!=null){
...
return event
} else throw new UnauthorizedException("Please authenticate first.");
}
public Event getEvent(#Named("eventID") Long eventID, User auth) throws UnauthorizedException {
if (auth != null) {
...
return event;
} else throw new UnauthorizedException("Please authenticate first.");
}
I have some complex queries to build with a number of optional filters, for which MyBatis seems like an ideal candidate for generating dynamic SQL.
However, I still want my query to execute in the same framework as the rest of the application (which is not using MyBatis).
So what I was hoping to do was use MyBatis strictly for generating the SQL, but from there using the rest of my app to actually execute it. Is this possible? If so, how?
Although MyBatis was designed to execute the query after it builds it, you can make use of it's configuration and a little bit of "inside knowledge" to get to what you need.
MyBatis is a very nice framework, unfortunately it lacks on the documentations side so the source code is you friend. If you dig around you should bump into these classes: org.apache.ibatis.mapping.MappedStatement and org.apache.ibatis.mapping.BoundSql which are key players into building the dynamic SQL. Here is a basic usage example:
MySQL table user with this data in it:
name login
----- -----
Andy a
Barry b
Cris c
User class:
package pack.test;
public class User {
private String name;
private String login;
// getters and setters ommited
}
UserService interface:
package pack.test;
public interface UserService {
// using a different sort of parameter to show some dynamic SQL
public User getUser(int loginNumber);
}
UserService.xml mapper file:
<mapper namespace="pack.test.UserService">
<select id="getUser" resultType="pack.test.User" parameterType="int">
<!-- dynamic change of parameter from int index to login string -->
select * from user where login = <choose>
<when test="_parameter == 1">'a'</when>
<when test="_parameter == 2">'b'</when>
<otherwise>'c'</otherwise>
</choose>
</select>
</mapper>
sqlmap-config.file:
<configuration>
<settings>
<setting name="lazyLoadingEnabled" value="false" />
</settings>
<environments default="development">
<environment id="development">
<transactionManager type="JDBC"/>
<dataSource type="POOLED">
<property name="driver" value="com.mysql.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://localhost/test"/>
<property name="username" value="..."/>
<property name="password" value="..."/>
</dataSource>
</environment>
</environments>
<mappers>
<mapper resource="pack/test/UserService.xml"/>
</mappers>
</configuration>
AppTester to show the result:
package pack.test;
import java.io.Reader;
import org.apache.ibatis.io.Resources;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;
public class AppTester {
private static String CONFIGURATION_FILE = "sqlmap-config.xml";
public static void main(String[] args) throws Exception {
Reader reader = null;
SqlSession session = null;
try {
reader = Resources.getResourceAsReader(CONFIGURATION_FILE);
session = new SqlSessionFactoryBuilder().build(reader).openSession();
UserService userService = session.getMapper(UserService.class);
// three users retreived from index
for (int i = 1; i <= 3; i++) {
User user = userService.getUser(i);
System.out.println("Retreived user: " + user.getName() + " " + user.getLogin());
// must mimic the internal statement key for the mapper and method you are calling
MappedStatement ms = session.getConfiguration().getMappedStatement(UserService.class.getName() + ".getUser");
BoundSql boundSql = ms.getBoundSql(i); // parameter for the SQL statement
System.out.println("SQL used: " + boundSql.getSql());
System.out.println();
}
} finally {
if (reader != null) {
reader.close();
}
if (session != null) {
session.close();
}
}
}
}
And the result:
Retreived user: Andy a
SQL used: select * from user where login = 'a'
Retreived user: Barry b
SQL used: select * from user where login = 'b'
Retreived user: Cris c
SQL used: select * from user where login = 'c'
Everyone knows how to use BoundSql.getSql() to get a paramaterized query string from MyBatis, like this:
// get parameterized query
MappedStatement ms = configuration.getMappedStatement("MyMappedStatementId");
BoundSql boundSql = ms.getBoundSql(parameters);
System.out.println("SQL" + boundSql.getSql());
// SELECT species FROM animal WHERE name IN (?, ?) or id = ?
But now you need the other half of the equation, the list of values that correspond to the question marks:
// get parameters
List<ParameterMapping> boundParams = boundSql.getParameterMappings();
String paramString = "";
for(ParameterMapping param : boundParams) {
paramString += boundSql.getAdditionalParameter(param.getProperty()) + ";";
}
System.out.println("params:" + paramString);
// "Spot;Fluffy;42;"
Now you can serialize it to send elsewhere to be run, or you can print it to a log so you can stitch them together and run the query manually.
*code not tested, might be minor type issues or the like
mybatis version is 3.4.5
Util Class
To convert mapper to sql, need mapper interface class,method name,paramters,and sqlSession.
package util;
import java.lang.reflect.Method;
import java.text.DateFormat;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Date;
import java.util.List;
import java.util.Locale;
import java.util.regex.Matcher;
import org.apache.ibatis.binding.MapperMethod.MethodSignature;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.ParameterMapping;
import org.apache.ibatis.reflection.MetaObject;
import org.apache.ibatis.session.Configuration;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.type.TypeHandlerRegistry;
import org.springframework.util.CollectionUtils;
/**
* #author zwxbest - 19-4-25
*/
public class SqlUtil {
public static String showSql(SqlSession sqlSession, Class mapperInterface, String methodName,
Object[] params) {
Configuration configuration = sqlSession.getConfiguration();
MappedStatement ms = configuration.getMappedStatement(
mapperInterface.getName() + "." + methodName);
Method sqlMethod = null;
//find method equals methodName
for (Method method : mapperInterface.getDeclaredMethods()) {
if (method.getName().equals(methodName)) {
sqlMethod = method;
break;
}
}
if (sqlMethod == null) {
throw new RuntimeException("mapper method is not found");
}
MethodSignature method = new MethodSignature(configuration, mapperInterface, sqlMethod);
Object paramObject = method.convertArgsToSqlCommandParam(params);
BoundSql boundSql = ms.getBoundSql(paramObject);
Object parameterObject = boundSql.getParameterObject();
List<ParameterMapping> parameterMappings = boundSql
.getParameterMappings();
String sql = boundSql.getSql().replaceAll("[\\s]+", " ");
if (!CollectionUtils.isEmpty(parameterMappings) && parameterObject != null) {
TypeHandlerRegistry typeHandlerRegistry = configuration
.getTypeHandlerRegistry();
if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {
sql = sql.replaceFirst("\\?",
Matcher.quoteReplacement(getParameterValue(parameterObject)));
} else {
MetaObject metaObject = configuration.newMetaObject(
parameterObject);
for (ParameterMapping parameterMapping : parameterMappings) {
String propertyName = parameterMapping.getProperty();
if (metaObject.hasGetter(propertyName)) {
Object obj = metaObject.getValue(propertyName);
sql = sql
.replaceFirst("\\?", Matcher.quoteReplacement(getParameterValue(obj)));
} else if (boundSql.hasAdditionalParameter(propertyName)) {
Object obj = boundSql.getAdditionalParameter(propertyName);
sql = sql
.replaceFirst("\\?", Matcher.quoteReplacement(getParameterValue(obj)));
} else {
sql = sql.replaceFirst("\\?", "missing");
}
}
}
}
return sql;
}
/**
* if param's type is `String`,add single quotation<br>
*
* if param's type is `datetime`,convert to string and quote <br>
*/
private static String getParameterValue(Object obj) {
String value = null;
if (obj instanceof String) {
value = "'" + obj.toString() + "'";
} else if (obj instanceof Date) {
DateFormat formatter = DateFormat
.getDateTimeInstance(DateFormat.DEFAULT, DateFormat.DEFAULT, Locale.CHINA);
value = "'" + formatter.format(new Date()) + "'";
} else if (obj instanceof LocalDateTime) {
value = "\'" + ((LocalDateTime) obj)
.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")) + "\'";
} else {
if (obj != null) {
value = obj.toString();
} else {
value = "";
}
}
return value;
}
}
call example
sqlSession is injected by Spring .
#Autowired
private SqlSession sqlSession;
String sql = SqlUtil
.showSql(sqlSession, PromotionCodeMapper.class, "selectByPromotionCodeForUpdate",
new Object[]{"111"});
log.warn(sql);
public static void main(String[] args) throws Exception {
String script = "<script>select * from table where 1 = 1<if test='id != null'>and id = ${id} </if></script>";
System.out.println(buildSql(script));
}
private static String buildSql(String script) {
LanguageDriver languageDriver = new XMLLanguageDriver();
Configuration configuration = new Configuration();
SqlSource sqlSource = languageDriver.createSqlSource(configuration, script, Object.class);
Map<String, String> parameters = new HashMap<>();
parameters.put("id", "1");
BoundSql boundSql = sqlSource.getBoundSql(parameters);
return boundSql.getSql();
}
use ${id} instead of #{id}
result is: select * from table where 1 = 1 and id = 1
Just to add to Bogdan's correct answer: You need to pass a JavaBean to getBoundSql() with getter's for your interface parameters, if you're interface has a more complex signature.
Let's assume you want to query the user based on the login number and/or the user name. Your interface might look like this:
package pack.test;
public interface UserService {
// using a different sort of parameter to show some dynamic SQL
public User getUser(#Param("number") int loginNumber, #Param("name") String name);
}
I'm leaving out the Mapper code since it's irrelevant for this discussion, but your code in AppTester should become:
[...]
final String name = "Andy";
User user = userService.getUser(i, name);
System.out.println("Retreived user: " + user.getName() + " " + user.getLogin());
// must mimic the internal statement key for the mapper and method you are calling
MappedStatement ms = session.getConfiguration().getMappedStatement(UserService.class.getName() + ".getUser");
BoundSql boundSql = ms.getBoundSql(new Object() {
// provide getters matching the #Param's in the interface declaration
public Object getNumber() {
return i;
}
public Object getName() {
return name;
}
});
System.out.println("SQL used: " + boundSql.getSql());
System.out.println();
[...]