I have a web service DocGenerationServiceImpl that inserts (for every format) a record in the table using DocRepository and object representing the record as DocFileDO. In the for-loop, I can get the id of the record that was created in the table. For each record, I will call the executor's execute method where DocGenTask will search for the record given the id. However, for example, there are 3 formats, the DocGenTask is able to get only the last record. The first 2 it cannot find. Although it's using hibernateTemplate. Can please advise?
#RestfulService
#Controller
#RequestMapping("/docs")
public class DocGenerationServiceImpl {
#Autowired
private TaskExecutor taskExecutor;
#Autowired
private DocRepository docRepository;
#RequestMapping(value = "/generate", method = RequestMethod.POST)
#ResponseBody
public String generatedDocFile(DOCParam param) {
for(String format : param.getFormatList()) {
DocFileDO docFileDO = new DocFileDO();
...
docRepository.saveDocFile(docFileDO);
log.debug("docFileDO id = " + docFileDO.getId());
DocGenTask task = new DocGenTask(docFileDO.getId());
task.setDocRepository(docRepository);
taskExecutor.execute(task);
}
}
}
#Repository
public class DocRepository {
#Autowired
private HibernateTemplate hibernateTemplate;
public DocFileDO saveDocFile(DocFileDO docFile) {
hibernateTemplate.save(docFile);
hibernateTemplate.flush();
return docFile;
}
public DocFileDO getDocFile(Long docFileId) {
return hibernateTemplate.get(DocFileDO.class, docFileId);
}
}
public class DocGenTask implements Runnable {
public void run() {
generate();
}
private void generate() {
DocFileDO docFileObj = docRepository.getDocFile(docFileId);
}
}
A couple of things
Don't use HibernateTemplate it should be considered deprecated as of Hibernate 3.0.1 (which was released somewhere in 2006). Use the SessionFactory directly and use the getCurrentSession() method to get a hibernate Session to operate on.
You don't have transactions setup (judging from the snippets), to work with a databse you need proper transaction setup.
Your controller is doing much, all of this should be inside a service.
The first refactor your repository
#Repository
public class DocRepository {
#Autowired
private SessionFactory sf;
public DocFileDO saveDocFile(DocFileDO docFile) {
Session session = sf.getCurrentSession();
session.save(docFile);
return docFile;
}
public DocFileDO getDocFile(Long docFileId) {
return sf.getCurrentSession().get(DocFileDO.class, docFileId);
}
}
Now your code will probably fail due to improper transaction setup. Add #Transactional to all the methods (or class) that need a transaction (like the saveDocFile method).
As mentioned you probably should move the code found in the controller to a service. The controller should be nothing more then a thin integration layer converting from the web to an internal representation of something and then kick off a service/business method somewhere. This service-/business-method is also your transactional unit-of-work it either all succeeds or all fails.
Related
I have a service class like this.
#Service
public class ServiceAImpl implements ServiceA {
#Autowired
private ServiceARepository repo;
#Autowired
private Publisher publisher;
#Transactional
public String save(Model model) {
//entity list creation code goes here
List<EntityA> entityList = repo.save(entityList);
repo.flush();
...
//savedIdList is comma separated list of entityList EntityA.id
publisher.publish(savedIdList);
return responseObject.toString();
}
}
When controller call to this service its create the Entity list and save. After that its call to publish method in another class with the saved ids. This save method annotated with #Transactional.
#Service
public class Publisher {
#Autowired
private AsyncPublisher asyPublisher;
#Autowired
PublishedDataRepository repo;
public void publish(String savedIdList) throws Exception {
savePublishData(..., savedIdList);
}
#Transactional
private void savePublishData(..., String savedIdList) throws Exception {
SearchPublishedData publishData = new SearchPublishedData();
...
publishData.setIdList(savedIdList);
publishData = repo.saveAndFlush(publishData);
asyPublisher.callPublisher(..., savedIdList, publishData.getId());
}
}
In publisher class its save a record to the publisher table and again it call to the async publisher class. In this class there is a method with #Async and its implemented with ThreadPoolTaskExecutor. In this async method what it going to do is get the previously saved data from its ids using EntityManager native query.
This is a sample java class code. Actually in this native query there are few tables join with this previously saved table and getting the data.
#Service
public class AsyncPublisher {
#PersistenceContext
private EntityManager entityManager;
#Async("threadPoolTaskExecutor") //This is created as ThreadPoolTaskExecutor
public void callPublisher(..., String ids, Long savedId) {
try {
String query = getQuery(ids);
List<Object[]> results = entityManager.createNativeQuery(query).getResultList();
... ///Other codes goes here, but results is empty
} catch (Exception e) {
logg error
}
}
private String getQuery(String ids) throws Exception {
StringBuilder query = new StringBuilder();
query.append("SELECT * FROM EntityA_table WHERE id IN (").append(ids).append(" ) ");
//This is a sample query, actually in this query there are 2 or more tables joined
return query.toString();
}
}
My problem is when I retrieve data from EntityManager native query time to time its not getting the data. But when I check the database with those ids those data available in database.
Anyone can give me the reason for this.
I think this saving block is annotated with #Transactional and it going to commit data to the Database at the end of the method execution and but before it save to the database EntityManager native query execution happens in another thread and its execute before the data commit. Is this correct? If not please explain someone what is happening here.
Also is there any way to avoid this data not retrieve scenario?
Thank you.
I just hit a really strange case which I can't explain to myself. I have have the following scenario:
Hibernate version: 5.4.9
Spring data version: 2.2.3
So the following method is wrapped in a transaction and it only saves the entity
#Transactional
public Bookmark create(Entity entity) {
return repository.save(entity);
}
Here I registered a PostInsertEventListener. Based on some logic it uses the same repository to query the underlying table. I removed the logic in order to make the example more readable.
#Component
public class EntityListener implements PostInsertEventListener {
#Autowired
private EntityRepository repository;
#Autowired
private EntityManagerFactory entityManagerFactory;
#PostConstruct
private void init() {
final EventListenerRegistry registry = ((SessionFactoryImplementor) entityManagerFactory.unwrap(SessionFactory.class)).getServiceRegistry()
.getService(EventListenerRegistry.class);
registry.appendListeners(EventType.POST_INSERT, this);
}
#Override
public void onPostInsert(PostInsertEvent event) {
if (event.getEntity() instanceof Entity) {
repository.findByFieldOneAndFieldTwoIsNotNull(event.getEntity().fieldOne());
}
}
#Override
public boolean requiresPostCommitHanding(EntityPersister persister) {
return false;
}
}
So when I invoke the create(Entity entity) method the onPostInsert(PostInsertEvent event) is triggered(as expected) but when this line is invoked repository.findByFieldOneAndFieldTwoIsNotNull(event.getEntity().fieldOne());
then another insert is executed and the onPostInsert(PostInsertEvent event) is triggered again. And of course at some point this leads to StackOverflowException.
Can someone come up with an idea why another insert is executed when I'm reading data using findBy query?
So i have a progress on that issue. When I execute repository.findByFieldOneAndFieldTwoIsNotNull(event.getEntity().fieldOne()); in a new separate transaction then everything is fine. So it seems that executing queries in the entity listener in the same transaction that the insert was executed on is leading to an infinite recursion which leads to a StackOverflowException. But I can't figure it out why is this happening.
CONTEXT:
I process reports with #Scheduled annotation and when invoke Component from Service property not getting initialized with #Value annotation even it physically exists in .properties and printed out in #PostConstruct.
DESCRIPTION:
ReportProcessor interface and InventoryReportProcessor implementation:
#FunctionalInterface
interface ReportProcessor {
public void process(OutputStream outputStream);
}
#Component
public class InventoryReportProcessor implement ReportProcessor {
#Value("${reportGenerator.path}")
private String destinationFileToSave;
/*
#PostConstruct
public void init() {
System.out.println(destinationFileToSave);
}
*/
#Override
public Map<String, Long> process(ByteArrayOutputStream outputStream) throws IOException {
System.out.println(destinationFileToSave);
// Some data processing in here
return null;
}
}
I use it from
#Service
public class ReportService {
#Value("${mws.appVersion}")
private String appVersion;
/* Other initialization and public API methods*/
#Scheduled(cron = "*/10 * * * * *")
public void processReport() {
InventoryReportProcessor reportProcessor = new InventoryReportProcessor();
Map<String, Long> skus = reportProcessor.process(new ByteArrayOutputStream());
}
}
My confusion comes from the fact that #Value in Service works fine but in #Component it returns null unless call in #PostConstruct. Also, if call #PostConstruct the value is still remains null in the rest of the class code.
I found similar Q&A and I did research in Srping docs but so far no single idea why it works this way and what can be a solution?
You need to Autowire the component to make your spring application aware of the component.
#Service
public class ReportService {
#Value("${mws.appVersion}")
private String appVersion;
/* Other initialization and public API methods*/
#Autowired
private ReportProcessor reportProcessor;
#Scheduled(cron = "*/10 * * * * *")
public void processReport() {
//InventoryReportProcessor reportProcessor = new InventoryReportProcessor();
Map<String, Long> skus = reportProcessor.process(new ByteArrayOutputStream());
}
}
Field injection is done after objects are constructed since obviously the container cannot set a property of something which doesn't exist.
at the time System.out.println(destinationFileToSave); triggers values are not being injected;
if you want to see it working try something like this
#Autowired
InventoryReportProcessor pross;
pross.process(ByteArrayOutputStream outputStream);
#PostConstruct works as it is being called after the object creation.
Spring will only parse #Value annotations on beans it knows. The code you use creates an instance of the class outside the scope of Spring and as such Spring will do nothing with it.
One thing you can do is to create the instance explictly or use Autowire:
#Autowired
private ReportProcessor reportProcessor;
tl:dr If you have configured your application context correctly then a #Value cannot be null as that will stop the correct startup of your application.
Change your Code from
#Value("${reportGenerator.path}")
private String destinationFileToSave;
to
#Value("${reportGenerator.path}")
public void setDestinationFileToSave(String destinationFileToSave) {
SendMessageController.destinationFileToSave = destinationFileToSave;
}
I'm writing a simple library API for a college project. I have a database with books, each with it's own ID. I'm using Spring Boot to make the service. I have a BookRepository which extends JpaRepository<Book, Long>, and a service implementation.
#Service
public class BookServiceImpl implements BookService{
#Autowired
private BookRepository bookRepository;
#Async
#Override
public void delete (Long id){
bookRepository.delete(id);
}
}
Later on, a REST controller handles the request:
#RestController
public class BookServiceController{
#Autowired
private BookService bookService;
#RequestMapping(value="books/{id}", method = RequestMethod.DELETE)
public ResponseEntity<Book> deleteBook (#PathVariable("id") Long id){
bookService.delete(id);
return new ResponseEntity<Book>(HttpStatus.OK);
}
}
Now, if I were to delete a Book which is not in the database, for example with an ID of 123, I'd get a EmptyResultDataAccessException thrown.
My question is, how and where do I handle the exception, and how do I avoid casting a NullPointerException that way?
Thanks in advance.
In the case of a DELETE operation, you aren't really returning an entity anymore; you're simply confirming that the resource is gone. As DELETE is idempotent (you could delete the record multiple times), you can either return the same status code regardless of whether the record exists or return a 404 if the record isn't found. You can also simplify the handler method:
#DeleteMapping("/books/{id}")
#ResponseStatus(HttpStatus.NO_CONTENT) // because you deleted it
public void deleteBook(#PathVariable Long id) {
try {
bookService.delete(id); // or just use the repository directly
} catch (EmptyResultDataAccessException ex) {
// either do nothing to return a 204, or
throw new NotFoundException();
}
}
where you have an exception that indicates the status:
#ResponseStatus(HttpStatus.NOT_FOUND)
public class NotFoundException extends RuntimeException {}
It's plausible that EmptyResultDataAccessException should already be annotated with a status of NOT_FOUND; this is a potential enhancement request.
The most elegant way to avoid the EmptyResultDataAccessException is to define an specific method in your Repository interface to delete by your id field.
Let's supppose that your id field is named bookId:
public interface BookRepository extends JpaRepository<Book, Long> {
Long deleteByBookId(Long bookId);
}
In this way the exception won't be throwed.
I haven't tested the solution if your id field is simply called id.
I'm currently having the issue that the #Transactional annotation doesn't seem to start a transaction for Neo4j, yet (it doesn't work with any of my #Transactional annotated methods, not just with the following example).
Example:
I have this method (UserService.createUser), which creates a user node in the Neo4j graph first and then creates the user (with additional information) in a MongoDB. (MongoDB doesn't support transactions, thus create the user-node first, then insert the entity into MongoDB and commit the Neo4j-transaction afterwards).
The method is annotated with #Transactional yet a org.neo4j.graphdb.NotInTransactionException is thrown when it comes to creating the user in Neo4j.
Here is about my configuration and coding, respectively:
Code based SDN-Neo4j configuration:
#Configuration
#EnableTransactionManagement // mode = proxy
#EnableNeo4jRepositories(basePackages = "graph.repository")
public class Neo4jConfig extends Neo4jConfiguration {
private static final String DB_PATH = "path_to.db";
private static final String CONFIG_PATH = "path_to.properties";
#Bean(destroyMethod = "shutdown")
public GraphDatabaseService graphDatabaseService() {
return new GraphDatabaseFactory().newEmbeddedDatabaseBuilder(DB_PATH)
.loadPropertiesFromFile(CONFIG_PATH).newGraphDatabase();
}
}
Service for creating the user in Neo4j and the MongoDB:
#Service
public class UserService {
#Inject
private UserMdbRepository mdbUserRepository; // MongoRepository
#Inject
private Neo4jTemplate neo4jTemplate;
#Transactional
public User createUser(User user) {
// Create the graph-node first, because if this fails the user
// shall not be created in the MongoDB
this.neo4jTemplate.save(user); // NotInTransactionException is thrown here
// Then create the MongoDB-user. This can't be rolled back, but
// if this fails, the Neo4j-modification shall be rolled back too
return this.mdbUserRepository.save(user);
}
...
}
Side-notes:
I'm using spring version 3.2.3.RELEASE and spring-data-neo4j version 2.3.0.M1
UserService and Neo4jConfig are in separate Maven artifacts
Starting the server and SDN reading operations work so far, I'm just having troubles with writing operations
I'm currently migrating our project from the tinkerpop-framework to SDN-Neo4j. This user creation-process has worked before (with tinkerpop), I just have to make it work again with SDN-Neo4j.
I'm running the application in Jetty
Does anyone have any clue why this is not working (yet)?
I hope, this information is sufficient. If anything is missing, please let me know and I'll add it.
Edit:
I forgot to mention that manual transaction-handling works, but of course I'd like to implement it the way "as it's meant to be".
public User createUser(User user) throws ServiceException {
Transaction tx = this.graphDatabaseService.beginTx();
try {
this.neo4jTemplate.save(user);
User persistantUser = this.mdbUserRepository.save(user);
tx.success();
return persistantUser;
} catch (Exception e) {
tx.failure();
throw new ServiceException(e);
} finally {
tx.finish();
}
}
Thanks to m-deinum I finally found the issue. The problem was that I scanned for those components / services in a different spring-configuration-file, than where I configured SDN-Neo4j. I moved the component-scan for those packages which might require transactions to my Neo4jConfig and now it works
#Configuration
#EnableTransactionManagement // mode = proxy
#EnableNeo4jRepositories(basePackages = "graph.repository")
#ComponentScan({
"graph.component",
"graph.service",
"core.service"
})
public class Neo4jConfig extends Neo4jConfiguration {
private static final String DB_PATH = "path_to.db";
private static final String CONFIG_PATH = "path_to.properties";
#Bean(destroyMethod = "shutdown")
public GraphDatabaseService graphDatabaseService() {
return new GraphDatabaseFactory().newEmbeddedDatabaseBuilder(DB_PATH)
.loadPropertiesFromFile(CONFIG_PATH).newGraphDatabase();
}
}
I still will have to separate those components / services which require transactions from those which don't, though. However, this works for now.
I assume that the issue was that the other spring-configuration-file (which included the component-scan) was loaded before Neo4jConfig, since neo4j:repositories has to be put before context:component-scan. (See Note in Example 20.26. Composing repositories http://static.springsource.org/spring-data/data-neo4j/docs/current/reference/html/programming-model.html#d0e2948)