I have a Product model object like this -
class ProductDTO {
int id;
String code;
String description;
//getters and setters go here
}
I am writing a service (code below) that looks up products by id or code and returns their description. I am using Spring 4 and ehcache to cache the results.
I have 2 methods - one for lookup by id and one for lookup by code - they are getProductByCode and getProductById. Both return the description as a string. They do so by calling getAllProducts() which returns a list of all products. The callers then search the list for a product matching the id or code and return the description.
getAllProducts() also calls 2 methods with #CachePut for each product - to save the description Strings in cache - by key code and id.
Caching works properly if the same arguments are passed for code or id to to the getProductByCode and getProductById methods. But if I pass a different argument, getAllProducts() is called again.
How do I achieve the desired behavior - where every time a call is made to getAllProducts(), all descriptions get cached and a subsequent call looks up the cache rather than going to the repository?
public class ProductServiceImpl implements ProductService {
#Autowired
ProductsRepository ProductRepo;
#Override
public List<ProductDTO> getAllProducts() {
List<ProductDTO> products = ProductRepo.getAllProducts();
for(ProductDTO prodDTO : products) {
String desc = prodDTO.getDescription();
String code = prodDTO.getCode();
int id = prodDTO.getId();
putDescriptionInCache(desc, code);
putDescriptionInCache(desc, id);
}
return products;
}
#CachePut(value = "products", key = "#id")
public String putDescriptionInCache(String description, int id){
return description;
}
#CachePut(value = "products", key = "#code")
public String putDescriptionInCache(String description, String code){
return description;
}
#Override
#Cacheable(value="products", key="#id")
public String getProductById(Integer id) throws NullPointerException {
String dtoDesc = null;
List<ProductDTO> products = getAllProducts();
for(ProductDTO currDTO : products) {
int currId = currDTO.getId();
if(id.equals(new Integer(currId))) {
dtoDesc = currDTO.getDescription();
}
}
return dtoDesc;
}
#Override
#Cacheable(value="products", key="#code")
public String getProductByCode(String code) throws NullPointerException {
String dtoDesc = null;
List<ProductDTO> products = getAllProducts();
for(ProductDTO currDTO : products) {
String currCode = currDTO.getCode();
if(currCode.equals(code)) {
dtoDesc = currDTO.getDescription();
}
}
return dtoDesc;
}
}
As it was commented by M. Deinum, the problem comes from the annotations, like CachePut or Cacheable, being transformed into an aspect at runtime. And the main limitation with that approach is that calls from the same class are not properly captured.
As you replied yourself in the comments section, moving the annotated methods to another type that is injected in the current one solves the problem.
Related
I'm trying to get the id of the last inserted object into a database using Room with Android. I can fetch the last object using an SQL query and can call other methods to get the various properties of that object which the user has set when saving the object. But getId() always returns 0. When I examine the table contents in Android Studio's app inspector, I can clearly see that Room is generating a unique primary key for each row, but I just can't get at it. Can anyone suggest what the problem might be?
Here's the Dao query:
#Query("SELECT * FROM gamebooks_table WHERE gamebookId=gamebookId ORDER BY gamebookId DESC LIMIT 1")
LiveData<Gamebook> getSingleGamebookByID();
And here's the annotated entity class:
#Entity(tableName = "gamebooks_table")
public class Gamebook {
#PrimaryKey(autoGenerate = true)
private long gamebookId;
private String gamebookName;
private String gamebookComment;
private String gamebookPublisher;
private float gamebookStarRating;
public Gamebook(String gamebookName, String gamebookComment, String gamebookPublisher, float gamebookStarRating) {
this.gamebookName = gamebookName;
this.gamebookComment = gamebookComment;
this.gamebookPublisher = gamebookPublisher;
this.gamebookStarRating = gamebookStarRating;
}
public long getGamebookId() {
return gamebookId;
}
public String getGamebookName() {
return gamebookName;
}
public String getGamebookComment() {
return gamebookComment;
}
public String getGamebookPublisher() {
return gamebookPublisher;
}
public float getGamebookStarRating(){
return gamebookStarRating;
}
public void setGamebookId(long gamebookId) {
this.gamebookId = gamebookId;
}
}
SOLVED
Finally sorted this by adding an Observer to my DAO method which returns a single gamebook. Within the Observer's onChanged() method, I can loop through all Gamebooks in the LiveData List (even though there's only one because I'm limiting it to one in the SQL query) and call getId() to get their respective IDs.
mainViewModel.getSingleGamebook().observe(this, new Observer<List<Gamebook>>() {
#Override
public void onChanged(List<Gamebook> gamebooks) {
int i=0;
for(Gamebook gamebook : gamebooks){
gamebookId= gamebook.getGamebookId();
Log.d(TAG, "Gamebook Name: "+gamebook.getGamebookName()+ " Database ID: " +gamebookId);
i++;
}
}
});
I believe that your issue is due to the only constructor being available not setting the id so the LiveData uses the default value of 0 for a long.
I'd suggest having a default constructor and thus all setters/getters and (optionally) using #Ignore annotation for one of the constructors..
without #Ignore you get warnings Gamebook.java:8: warning: There are multiple good constructors and Room will pick the no-arg constructor. You can use the #Ignore annotation to eliminate unwanted constructors. public class Gamebook {
e.g. :-
#Entity(tableName = "gamebooks_table")
public class Gamebook {
#PrimaryKey(autoGenerate = true)
private long gamebookId;
private String gamebookName;
private String gamebookComment;
private String gamebookPublisher;
private float gamebookStarRating;
public Gamebook(){} /*<<<<< ADDED */
#Ignore /*<<<<< ADDED - is not required - could be on the default constructor but not both*/
public Gamebook(String gamebookName, String gamebookComment, String gamebookPublisher, float gamebookStarRating) {
this.gamebookName = gamebookName;
this.gamebookComment = gamebookComment;
this.gamebookPublisher = gamebookPublisher;
this.gamebookStarRating = gamebookStarRating;
}
public long getGamebookId() {
return gamebookId;
}
public String getGamebookName() {
return gamebookName;
}
public String getGamebookComment() {
return gamebookComment;
}
public String getGamebookPublisher() {
return gamebookPublisher;
}
public float getGamebookStarRating(){
return gamebookStarRating;
}
public void setGamebookId(long gamebookId) {
this.gamebookId = gamebookId;
}
/* ADDED setters */
public void setGamebookName(String gamebookName) {
this.gamebookName = gamebookName;
}
public void setGamebookComment(String gamebookComment) {
this.gamebookComment = gamebookComment;
}
public void setGamebookPublisher(String gamebookPublisher) {
this.gamebookPublisher = gamebookPublisher;
}
public void setGamebookStarRating(float gamebookStarRating) {
this.gamebookStarRating = gamebookStarRating;
}
}
You also probably want to be able to pass the respective id to the getSingleGamebookByID, so you may wish to change this to:-
#Query("SELECT * FROM gamebooks_table WHERE gamebookId=:gamebookId /*<<<<< ADDED to use id passed */ ORDER BY gamebookId DESC LIMIT 1")
LiveData<Gamebook> getSingleGamebookByID(long gamebookId /*<<<<< ADDED to use id passed */);
you would probably want to remove the comments.
Note the LiveData aspect has not been tested and is conjecture.
Example
This example shows that room is fine with your original code but that the issues is on the LiveData/Viewmodel side :-
public class MainActivity extends AppCompatActivity {
TheDatabase db;
GamebookDao dao;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
/* Note The Database has .allowMainThreadQueries */
db = TheDatabase.getInstance(this);
dao = db.getGamebookDao();
long gb1id = dao.insert(new Gamebook("Gamebook1","blah","Gamebook1 Publisher", 10.1F));
long gb2id = dao.insert(new Gamebook("Gamebook2","blah","Gamebook2 Publisher", 6.1F));
long gb3id = dao.insert(new Gamebook("Gamebook3","blah","Gamebook3 Publisher", 10.1F));
logGameBook(dao.getSingleGamebookByID());
logGameBook(dao.getSingleGamebookByID());
logGameBook(dao.getSingleGamebookByID());
/* Alternative that allows the ID to be specified */
logGameBook(dao.getSingleGamebookByIDAlternative(gb1id));
logGameBook(dao.getSingleGamebookByIDAlternative(gb2id));
logGameBook(dao.getSingleGamebookByIDAlternative(gb3id));
}
void logGameBook(Gamebook gb) {
Log.d("GAMEBOOKINFO","Gamebook is " + gb.getGamebookName() + " id is " + gb.getGamebookId());
}
}
The above uses your original code, the TheDatabase is a basic #Database annotated class BUT with .allowMainThreadQueries so it is run on the main thread.
The log, after running, includes:-
2022-03-12 08:16:12.556 D/GAMEBOOKINFO: Gamebook is Gamebook3 id is 3
2022-03-12 08:16:12.558 I/chatty: uid=10132(a.a.so71429144javaroomidreturnedaszero) identical 1 line
2022-03-12 08:16:12.561 D/GAMEBOOKINFO: Gamebook is Gamebook3 id is 3
2022-03-12 08:16:12.568 D/GAMEBOOKINFO: Gamebook is Gamebook1 id is 1
2022-03-12 08:16:12.572 D/GAMEBOOKINFO: Gamebook is Gamebook2 id is 2
2022-03-12 08:16:12.574 D/GAMEBOOKINFO: Gamebook is Gamebook3 id is 3
Note how the first just returns the same object and thus id.
I have to create an application with Jhipster but i never use it before.
When a user send a GET request to the address http://localhost:8080/api/newmesure/{mac-address}/{value}
I want to insert a new mesure in my database.
First i created 3 entity "Plantes", "Capteurs" and "Mesures" with this format :
Image here : https://i.stack.imgur.com/zJqia.png (I'm not allowed to post)
I activated the JPA Filtering to create a #Query to insert data in my database but i read that was not possible.
In /src/main/java/com/mycompany/myapp/web/rest/MesuresRessources.java :
/**
* REST controller for managing {#link com.mycompany.myapp.domain.Mesures}.
*/
#RestController
#RequestMapping("/api")
public class MesuresResource {
private final Logger log = LoggerFactory.getLogger(MesuresResource.class);
private static final String ENTITY_NAME = "mesures";
#Value("${jhipster.clientApp.name}")
private String applicationName;
private final MesuresService mesuresService;
private final MesuresQueryService mesuresQueryService;
public MesuresResource(MesuresService mesuresService, MesuresQueryService mesuresQueryService) {
this.mesuresService = mesuresService;
this.mesuresQueryService = mesuresQueryService;
}
#GetMapping("/newMesure/{mac}/{value}")
public String newMesure(#PathVariable String mac,#PathVariable int value) {
log.debug("Adresse MAC : "+mac);
log.debug("Valeur : "+value);
#Query("SELECT valeur FROM Mesures WHERE id = 1") //not working
Mesures getValeur(); //not working
return "Mesure ajoutée";
}
}
In /src/main/java/com/mycompany/myapp/domain/Mesures.java :
/**
* A Mesures.
*/
#Entity
#Table(name = "mesures")
public class Mesures implements Serializable {
private static final long serialVersionUID = 1L;
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
#Column(name = "valeur")
private Integer valeur;
#ManyToOne(optional = false)
#NotNull
#JsonIgnoreProperties("macs")
private Capteurs mac;
// jhipster-needle-entity-add-field - JHipster will add fields here, do not remove
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public Integer getValeur() {
return valeur;
}
public Mesures valeur(Integer valeur) {
this.valeur = valeur;
return this;
}
public void setValeur(Integer valeur) {
this.valeur = valeur;
}
public Capteurs getMac() {
return mac;
}
public Mesures mac(Capteurs capteurs) {
this.mac = capteurs;
return this;
}
public void setMac(Capteurs capteurs) {
this.mac = capteurs;
}
// jhipster-needle-entity-add-getters-setters - JHipster will add getters and setters here, do not remove
#Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (!(o instanceof Mesures)) {
return false;
}
return id != null && id.equals(((Mesures) o).id);
}
#Override
public int hashCode() {
return 31;
}
#Override
public String toString() {
return "Mesures{" +
"id=" + getId() +
", valeur=" + getValeur() +
"}";
}
}
Louan
Learning java with JHipster is probably not a wise idea, it uses a very rich technology stack which might lose you unless you invest enough time to learn the basics.
There are many things wrong in your code and approach:
You can't use #Query annotation inside the body of method a of your REST controller, it must be used in your #Repository interface, this code can't compile. See https://www.baeldung.com/spring-data-jpa-query for a quick introduction
JPA filtering is not related to inserting into database
In HTTP/REST, GET method is supposed to be idempotent. For making changes in your database you should use POST or PUT methods. See What is idempotency in HTTP methods?
Your entity naming convention is not consistent: use singular for entity classes because each entity object represents one single instance of Mesure. Here you have Plantes (plural), Capteur (singular) and Mesures (plural). For table names, JHipster uses singular but plural is quite common too because a table holds many rows. Of course, this is just a convention and you or your team may decide to apply another (like a prefix for table names) but the key point is to be consistent.
I have this simple REST API that i created with Spring Boot.
In this app, I have a a POJO called Expense with 4 fields. I have a no Argument constructor and another constructor that takes only two inputs. One String value "item" and one Integer value "amount". The date is set using the LocalData.now() method and the id is set automatically in a MySql db running in the server.
Here's my Entity class
#Entity
public class Expense {
#Id
#GeneratedValue (strategy = GenerationType.AUTO)
private Integer id;
private String date;
private String item;
private Integer amount;
//No Arg Construction required by JPA
public Expense() {
}
public Expense(String item, Integer amount) {
this.date = LocalDate.now().toString();
this.item = item;
this.amount = amount;
}
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getDate() {
return date;
}
public void setDate(String date) {
this.date = date;
}
public String getItem() {
return item;
}
public void setItem(String item) {
this.item = item;
}
public Integer getAmount() {
return amount;
}
public void setAmount(Integer amount) {
this.amount = amount;
}
}
I have another class with RestController annotation where i have set a method to post Expense object with a post method using Request Mapping annotation.
#RestController
public class ExpController {
private ExpService expService;
private ExpenseRepo expenseRepo;
#Autowired
public ExpController(ExpService expService, ExpenseRepo expenseRepo) {
this.expService = expService;
this.expenseRepo = expenseRepo;
}
#RequestMapping(path = "/addExp", method=RequestMethod.POST)
public void addExp(Expense expense){
expenseRepo.save(expense);
}
}
Now finally i am using PostMan to make the HTTP Post Request. I have made a simple Json Format text to send Item and Amount
{
"item":"Bread",
"amount": 75
}
After I make the post request, all i can see is that a new Entry is created but all values are set to null.
I have done some experimentation and found out that the expenseRepo.save(expense) method is only using the default no Arg constructor to save the data. But it's not using the second constructor that takes the two parameters that I am passing through Postman
How to solve this issue. Please help
Change your controller method like this
#RequestMapping(path = "/addExp", method=RequestMethod.POST)
public void addExp(#RequestBody Expense expense){
expenseRepo.save(expense);
}
You need to use #RequestBody
I have a requirement to provide functionality which will allow user to search through many different domain elements and see results as combined list. So in UI he will have to fill only one text-field and than retrive results.
To visualize lets assume i have 3 entities in domain:
#Document(indexName="car")
public class Car {
private int id;
private String type;
}
#Document(indexName="garage")
public class Garage{
private int id;
private String address;
}
#Document(indexName="shop")
public class Shop{
private int id;
private String name;
}
Now i thought i could achieve requirement like this:
...
#Inject
private ElasticsearchTemplate elasticsearchTemplate;
...
#RequestMapping(value = "/_search/all/{query}",
method = RequestMethod.GET,
produces = MediaType.APPLICATION_JSON_VALUE)
#Timed
public List<?> search(#PathVariable String query) {
SearchQuery searchQuery = new NativeSearchQueryBuilder()
.withQuery(queryString(query))
.withIndices("car", "garage", "shop")
.build();
//THIS WORKS
elasticsearchTemplate.queryForIds(searchQuery);
//THIS THROWS ERROR ABOUT WRONG INDEXES
return elasticsearchTemplate.queryForPage(searchQuery, GlobalSearchDTO.class, new GlobalSearchResultMapper()).getContent();
}
...
class GlobalSearchDTO {
public Long id;
public String type;
public Object obj;
}
...
but when calling 2nd function - the one which is responsible for returning actual documents, the following exception is thrown:
Unable to identify index name. GlobalSearchDTO is not a Document. Make
sure the document class is annotated with #Document(indexName="foo")
I've tried with passing any domain entity as a class argument, but than i am retriving only elements from the corresponding index, not all of them. For instance calling:
return elasticsearchTemplate.queryForPage(searchQuery, Shop.class, new GlobalSearchResultMapper()).getContent();
Results in retrivng elements only from 'shop' index. It seems like for some reason dynamically provided indicies are not used.
So the question is: Is it possible to retrive data like that? Why specifying '.withIndices("car", "garage", "shop")' is not enough?
Maybe i should consider other solutions like:
search through indexes in loop(one bye one), join results and order them by score
create separate GlobalSearch entity with 'globalsearch' index
and duplicate data there
Thanks in advance!
Krzysztof
I have managed to find suitable workaround for my problem. It turned out that when using 'scroll' and 'scan' functionality dynamically provided indicies are used which means that query works as expected. Code for solution:
#RequestMapping(value = "/_search/all/{query}",
method = RequestMethod.GET,
produces = MediaType.APPLICATION_JSON_VALUE)
#Timed
public List<?> search(#PathVariable String query) {
SearchQuery searchQuery = new NativeSearchQueryBuilder()
.withQuery(queryString(query))
.withIndices("car", "garage", "shop")
.withPageable(new PageRequest(0,1))
.build();
String scrollId = elasticsearchTemplate.scan(searchQuery, 1000, false);
List<GlobalSearchDTO> sampleEntities = new ArrayList<GlobalSearchDTO>();
boolean hasRecords = true;
while (hasRecords){
Page<GlobalSearchDTO> page = elasticsearchTemplate.scroll(scrollId, 5000L , new ResultMapper());
if(page != null) {
sampleEntities.addAll(page.getContent());
hasRecords = page.hasNext();
}
else{
hasRecords = false;
}
}
return sampleEntities;
}
}
and in the ResultMapper class:
...
for (SearchHit hit : response.getHits()) {
switch(hit.getIndex()) {
case "car": //map to DTO
case "shop": //map to DTO
case "garage": //map to DTO
}
}
...
I'm investigating Neo4j and have a question with regards to object eager/lazy loading. Lets say I have class Trolley with has Set<Item> (with getters/setters). If I do the following:
Trolley t = new Trolley(...); // create empty trolley
t.addItem(f); // add one item to the trolley
t.persist(); // persist the object
I then later find the trolley based on the nodeId:
repo.findOne(xxx); // returns the trolley successfully
When I do something like:
trolley.getItems().size()
this is empty. I guess this is the intended behaviour. Is there any mechanism similar to JPA where is the session/tx is open to load the collection dynamically.
Code:
#NodeEntity
public class Trolley
{
#Indexed
private String name;
#RelatedTo
private Set<Item> items;
public Trolley(){}
public Trolley(String name)
{
this.name = name;
}
public void addItem(Item item)
{
this.items.add(item);
}
public Set<Item> getItems()
{
return items;
}
}
#NodeEntity
public class Item
{
private String name;
public Item(){}
public Item(String name)
{
this.name = name;
}
public String getName()
{
return name;
}
}
#Test
public void trolleyWithItemPersist()
{
Trolley trolley = new Trolley("trolley1").persist();
// Persisting - however I would've expected a cascade to
// occur when adding to the collection.
Item item = new Item("item1").persist();
// now add to the trolley
trolley.addItem(item);
// persist
trolley.persist();
// Now use repo to get trolley
Trolley loadedTrolley = trolleyRepository.findOne(trolley.getNodeId());
// should have one item
assertEquals(1, loadedTrolley.getItems().size());
}
Afaik, in Spring Data Jpa, to populate an lazy loaded field you need to annotate the method which call the findOne(xxx) with
#Transactional
from (org.springframework.transaction.annotation.Transactional)
Maybe it works also with neo4j...
I'm not really an skilled developper on Spring Data but this is the only way I know to retrieve lazy loaded fields. If someone has a better solution, feel free to write it!