I use following tecnologies:
TestNG(6.9.10)
Spring(4.3.2.RELEASE)
Hibernate(5.1.0.Final)
Java 8
I test some code with functionality by integration tests and i need to check the entity for correct save/update/delete or any other changes. There are sessionFactory configuration in my .xml :
<bean id="sessionFactory" class="org.springframework.orm.hibernate5.LocalSessionFactoryBean"
p:dataSource-ref="dataSource" p:hibernateProperties="jdbcProperties">
<property name="packagesToScan" value="my.package"/>
</bean>
and test class example:
#ContextConfiguration(locations = {"classpath:/applicationContext-test.xml",
"classpath:/applicationContext-dao.xml",
"classpath:/applicationContext-orm.xml"})
public class AccountServiceTest extends AbstractTransactionalTestNGSpringContextTests {
#Autowired
private SomeService someService;
#Autowired
private SessionFactory sessionFactory;
#Test
public void updateEntity() {
//given
Long entityId = 1L;
SomeClass expected = someService.get(entityId);
String newPropertyValue = "new value";
//when
someService.changeEntity(entity, newPropertyValue);
// Manual flush is required to avoid false positive in test
sessionFactory.getCurrentSession().flush();
//then
expected = someService.get(entityId);
Assert.assertEquals(expected.getChangedProperty() , newPropertyValue);
}
service method:
#Transactional
#Override
public int changeEntity(entity, newPropertyValue) {
return dao().executeNamedQuery(REFRESH_ACCESS_TIME_QUERY,
CollectionUtils.arrayToMap("id", entity.getId(), "myColumn", newPropertyValue));
}
dao:
#Override
public int executeNamedQuery(final String query, final Map<String, Object> parameters) {
Query queryObject = sessionFactory.getCurrentSession().getNamedQuery(query);
if (parameters != null) {
for (Map.Entry<String, Object> entry : parameters.entrySet()) {
NamedQueryUtils.applyNamedParameterToQuery(queryObject, entry.getKey(), entry.getValue());
}
}
return queryObject.executeUpdate();
}
But my entity property didn't change after flush()
as described here, change #Autowire SessionFactory with #PersistenceContext EntityManager , i should use EntityManager to flush() - but i can't do this - i can't transform sessionFactory to EntityManager, and i don't need in creation of EntityManager for my application - because i need to change my .xml config file and others.
Is there are any another solutions of this problem?
Your code is actually working as expected.
Your test method is transactional and thus your Session is alive during the whole execution of the test method. The Session is also the 1st level cache for hibernate and when loading an entity from the database it is put into the session.
So the line SomeClass expected = someService.get(entityId); will load the entity from the database and with it also put it in the Session.
Now this line expected = someService.get(entityId); first checks (well actually the dao method underneath) checks if the entity of the requested type with the id is already present in the Session if so it simply returns it. It will not query the database!.
The main problem is that you are using hibernate in a wrong way, you are basically bypassing hibernate with the way you are updating your database. You should update your entity and persist it. You should not write queries to update the database!
Annotated test method
#Test
public void updateEntity() {
//given
Long entityId = 1L;
SomeClass expected = someService.get(entityId); // load from db and put in Sesion
String newPropertyValue = "new value";
//when
someService.changeEntity(entity, newPropertyValue); // update directly in database bypass Session and entity
// Manual flush is required to avoid false positive in test
sessionFactory.getCurrentSession().flush();
//then
expected = someService.get(entityId); // return entity from Session
Assert.assertEquals(expected.getChangedProperty() , newPropertyValue);
}
To only fix the test add a call to clear() after the flush().
sessionFactory.getCurrentSession().clear();
However what you actually should do is stop writing code like that and use Hibernate and persistent entities in the correct way.
#Test
public void updateEntity() {
//given
Long entityId = 1L;
String newPropertyValue = "new value";
SomeClass expected = someService.get(entityId);
expected.setMyColumn(newPropertyValue);
//when
someService.changeEntity(entity);
sessionFactory.getCurrentSession().flush();
// now you should use a SQL query to verify the state in the DB.
Map<String, Object> dbValues = getJdbcTemplate().queryForMap("select * from someClass where id=?", entityId);
//then
Assert.assertEquals(dbValues.get("myColumn"), newPropertyValue);
}
Your dao method should look something like this.
public void changeEntity(SomeClass entity) {
sessionFactory.getCurrentSession().saveOrUpdate(entity);
}
Related
I am preparing notification system for API which I've build before.
Basically I have an aspect which listens on projectRepository.save method. What I want to achieve is check project status in an entity which is a parameter for save method with original status from database record. What I have notice is that when I search for the DB record by id it returns cached value so it is always the same as the object which is in save method even if database still have old value. Can I force Spring Data Jpa to return database record instead of cached entity?
#Aspect
#Component
#RequiredArgsConstructor
public class NotificationAspect {
private final UserService userService;
private final ProjectRepository projectRepository;
private final NotificationService notificationService;
#Pointcut("execution(* *com.stars.domain.project.ProjectRepository.save(..))")
public void projectSavePointcut() {}
#Before("projectSavePointcut()")
public void sendNotificationOnStatusChange(JoinPoint joinPoint) {
if(joinPoint.getArgs().length > 0 && joinPoint.getArgs()[0] instanceof Project) {
Project projectToUpdate = (Project) joinPoint.getArgs()[0];
Optional<Project> oldProject = projectRepository.findById(projectToUpdate.getProjectId());
if(oldProject.isPresent() && !oldProject.get().getStatus().equals(projectToUpdate.getStatus())) {
notificationService.saveNotification(
MessageFormat.format("Project: {} status has been changed from: {} to: {}",
projectToUpdate.getName(),
oldProject.get().getStatus(),
projectToUpdate.getStatus()),
List.of(userService.getUser(projectToUpdate.getCreatedBy())));
}
}
}
}
This line always returns true even if database record has different value.
oldProject.get().getStatus().equals(projectToUpdate.getStatus())
I can think of two ways.
First, if you're interested only in status field, you can create a custom native query in a repository, which will bypass EntityManager, for example like this:
#Query("SELECT p.status FROM projects p WHERE p.id = :id", nativeQuery = true)
String getProjectStatusById(#Param("id") String projectId);
Second looks like a bad idea, but it should work - you can make the entity manager's cache detach all managed entities, so it will be forced to make a DB call again.
For this inject EntityManager in your aspect bean and call its .clear() method right before calling projectRepository.findById method.
I have some uncatchable bug in my work.
For example, I have code that looks like this:
#Entity
public class Message {
#Id
#GeneratedValue(strategy = SEQUENCE, generator = "message_generator")
private long id;
private long massMessageId;
}
public class MessageDTO {
public final long id;
public final long massMessageId;
}
#Transactional
#Service
public class ExtendedMessageService {
private MessageService messageService;
public MessageDTO createMessage(MessageCreateDTO createDTO) {
var messageDTO = messageService.create();
return messageService.linkMassMessage(messageDTO.id, createDTO.massMessageId);
}
}
#Transactional
#Service
public class MessageService {
private final MessageRepository repository;
private final ObjectMapper mapper;
public MessageDTO create() {
var message = new Message();
var savedMessage = repository.save(message);
return mapper.map(savedMessage, MessageDTO.class);
}
public MessageDTO linkMassMessage(long messageId, long massMessageId) {
var message = repository.findById(messageId)
.orElseThrow(() -> new ObjectNotFoundException("Message with id " + id + " was not found"));
return mapper.map(repository.save(message.setMassMessageId(massMessageId)), MessageDTO.class);
}
}
What will happen in this situation? I have some bugs, when repository.findById(id) can't find entity and throws exception.
And i have no reason, why this bug is only on prod (i tried to repeat it on dev and nothing succeeded)
And when i try to find the reason of it, i get a question:
"Can i save entity and get it in one transaction in Spring?"
How saving works
repository.save() doesn't save anything to database, this method puts entity to the session (persistent context) in memory.
flush step — on this step actual SQL insert happens. It can be invoked manually repository.saveAndFlush(), repository.flush(). Hibernate can do flush in the background, before operations that can use saved to the database value, like JPQL statements.
Also flush happens when the end of #Transactional boundary is reached.
What can be an issue
You are using incorrect method. This method from the old version of Spring data and it doesn't perform search in the database. You have to use findById() method instead.
Hibernate: findById vs getbyId
The most simple way, if you want to use id after save — flush the data immediately.
Entity entity = new Entity(some_information);
repository.saveAndFlush(entity);
Entity findedEntity = repository.findById(entity.getId())
.orElseThrow(() -> new RuntimeException("Can't find id=" + entity.getId()));
Hibernate will not necessary perform SQL select to get findedEntity. It can get it from the session, if it happens in the same #Transactional boundaries.
So if the above code resides in the method with #Transaction SQL will not performed. if there is not #Transaction SQL will be performed.
About this question
"Can Spring or Hibernate find not flushed entity in transaction context? Or there are some other ways to do it?"
Hibernate can't find not flushed entity. if id is autogenerated, Hibernate needs to perform SQL INSERT (flush) to get the id from a database. Another option to set up an id manually. Probably in this case it will be possible to get an entity from the persistent context.
There is an Hibernate domain object that have 'not null' field which have annotation #Column(nullable=false), a DAO class method which is saving this object in DB.
I am mocking the create DAO call using PowerMockito, mock call is working fine but if i am passing null for the field, mock test is not throwing error that field is null.
Below is the code, tools/techs (java, spring, hibernate, sqlserver, junits, powermockit). Few code changes omitted related to spring context and hibernate session configurations.
public class Entity{
private String id;
#Column(nullable=false)
private String field;
//setters and getters goes here
}
public class HibernateDAO{
private SessionFactory sessionFactory;
public void create(Entity entity){
getSession().save(entity);
}
public void setSessionFactory(SessionFactory sf){
sessionFactory = sf;
}
}
public class HibernateDAOTest{
HibernateDAO hibernateDAO = new HibernateDAO();
public SessionFactory mockedSessionFactory;
public Session mockedSession;
public Query query;
public SQLQuery sqlQuery;
public Transaction mockedTransaction;
#Before
public void setup() {
mockedSessionFactory = PowerMockito.mock(SessionFactory.class);
mockedSession = PowerMockito.mock(Session.class);
mockedTransaction = PowerMockito.mock(Transaction.class);
query = PowerMockito.mock(Query.class);
sqlQuery = PowerMockito.mock(SQLQuery.class);
PowerMockito.when(mockedSessionFactory.openSession()).thenReturn(mockedSession);
PowerMockito.when(mockedSessionFactory.getCurrentSession()).thenReturn(mockedSession);
PowerMockito.when(mockedSession.beginTransaction()).thenReturn(mockedTransaction);
}
#Test
public void testCreate(){
Entity entityToSave = new Entity();
entityToSave.setId("123");
entityToSave.setField(null);
hibernateDAO.setSessionFactory(mockedSessionFactory);
hibernateDAO.create(entityToSave);//this call should throw error that "field" is null but its not.
}
}
Actually the validation (not-nullability check) is not done in your DAO. You do declare your field as not-null, but then your create(Entity entity) method calls:
getSession().save(entity);
and that is all it does. Now the validation would happen within the saving via HibernateSession, but all those classed are mocked. So they will not perform any validation.
Generally it is a good rule, that each time you accidentally mock the very same thing you tested, you step back and re-evaluate:
Did I write that code? Why do I want to test it?
In this case probably the answer is, that you should trust Hibernate taking care of things. And if you want to test your model definition, you need to setup a full Hibernate context and perform a real test with Hibernate not being mocked.
I am trying to implement a sharded Hibernate logic. All Databases have same table called MyTable which is mapped to MyClass through Hibernate POJO.
public class SessionFactoryList {
List<SessionFactory> factories;
int minShard;
int maxShard;
// getters and setters here.
}
In my Dao implementation, I have a method getAll which is following -
public class MyClassDao {
#Autowired // through Spring
private SessionFactoryList list;
List<MyClass> getAll() {
List<MyClass> outputList = new ArrayList<>();
for(SessionFactory s : list.getFactories()) {
Criteria c = s.getCurrentSession.createCriteria(MyClass.class);
outputList.addAll(c.list());
}
return outputList;
}
Here is my test for the corresponding getAll implementation -
public class MyClassTest {
#Autowired
SessionFactoryList list;
#Autowired
MyClassDao myClassDao;
#Test
void getAllTest() {
Session session1 = list.getFactories.get(0).getCurrentSession();
session1.beginTransaction();
session1.save(new MyClass(// some parameters here));
Session session2 = list.getFactories.get(1).getCurrentSession();
session2.beginTransaction();
session2.save(new MyClass(// some parameters here));
//Set up done.
assert myClassDao.getAll().size() == 2
}
}
I am using HSQL in-memory database for the test cases.
I have verified that DB connections are correctly setup, but the Assert statement is failing.
'getAll' method of MyClassDao is returning 3 rows. MyClass object inserted in SessionFactory1's session is getting duplicated.
Is there anything I am missing out here?
I found it. The 2 sessionFactory configurations which I used for the test had same Database URL. Hence the same database was queried twice which caused the duplicates.
I was tasked with creating an Annotation for Custom Validation. This was due to some problems with handling database constraint violations nicely. What I did in response to this was relatively simple. I created a class-level CustomConstraint specifically for the one domain-class that required it. What I got as my current result is the following:
#UniqueLocation Annotation:
#Target({ TYPE, ANNOTATION_TYPE })
#Retention(RUNTIME)
#Constraint(validatedBy = UniqueLocationValidator.class)
#Documented
public #interface UniqueLocation {
String message() default "must be unique!";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
This is not spectacular, in fact it's copied almost verbatim from the hibernate documentation.
I proceeded to create my UniqueLocationValidator and ran into a problem with using the persistence context in there. I wanted to run a defensive select, and thusly tried to Inject my application wide #Produces #PersistenceContext EntityManager.
Therefor I included JBoss Seam to use it's InjectingConstraintValidatorFactory configuring my validation.xml as follows:
<?xml version="1.0" encoding="UTF-8"?>
<validation-config
xmlns="http://jboss.org/xml/ns/javax/validation/configuration" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://jboss.org/xml/ns/javax/validation/configuration validation-configuration-1.0.xsd">
<constraint-validator-factory>
org.jboss.seam.validation.InjectingConstraintValidatorFactory
</constraint-validator-factory>
</validation-config>
After running into some issues with Creating Constraint Violations this is how my Validator actually looks:
#ManagedBean
public class UniqueLocationValidator implements
ConstraintValidator<UniqueLocation, Location> {
// must not return a result for name-equality on the same Id
private final String QUERY_STRING = "SELECT * FROM Location WHERE locationName = :value AND id <> :id";
#Inject
EntityManager entityManager;
private String constraintViolationMessage;
#Override
public void initialize(final UniqueLocation annotation) {
constraintViolationMessage = annotation.message();
}
#Override
public boolean isValid(final Location instance,
final ConstraintValidatorContext context) {
if (instance == null) {
// Recommended, instead use explicit #NotNull Annotation for
// validating non-nullable instances
return true;
}
if (duplicateLocationExists(instance)) {
createConstraintViolations(context);
return false;
} else {
return true;
}
}
private void createConstraintViolations(
final ConstraintValidatorContext context) {
context.disableDefaultConstraintViolation();
context.buildConstraintViolationWithTemplate(constraintViolationMessage)
.addNode("locationName").addConstraintViolation();
}
private boolean duplicateLocationExists(final Location location) {
final String checkedValue = location.getLocationName();
final long id = location.getId();
Query defensiveSelect = entityManager.createNativeQuery(QUERY_STRING)
.setParameter("value", checkedValue).setParameter("id", id);
return !defensiveSelect.getResultList().isEmpty();
}
}
So much for my current configuration, now to the real beef, the problem:
When I run following code after recieving an action from a user, the thing works wonderfully and correctly marks a duplicate location name as invalid.. Also persisting works just fine when the locationName is not duplicated.
public long add(#Valid final Location location) {
entityManager.persist(location);
return location.getId();
}
Mind that the entityManager here and the entityManager in the UniqueLocationValidator are both injected via Weld CDI from the aforementioned #PersistenceContext EntityManager.
What does not work is the following:
public long update(#Valid final Location location){
entityManager.merge(location);
return location.getId();
}
When calling this code, I get a relatively short stacktrace, that has a ConcurrentModificationException as root-cause.
I neither understand why that's the case, nor how I would go about fixing this. I have nowhere attempted to explicitly multithread my application, so this should have been managed by the JBoss 7.1.1-Final I am using as application server..
What you're trying to do is not possible via the EntityManager. Well, not normally.
Your validator is called during the processing of the updates. Queries sent via the EntityManager affect the internal storage, the ActionQueue of the EntityManager. This is what causes the ConcurrentModificationException: the results from your query alter the list that the EntityManager is iterating through when flushing changes.
A workaround for this would be to bypass the EntityManager.
How can we do this?
Well, ... it's a bit dirty, since you're effectively adding a dependency on the hibernate implementation, but you can get the connection from the Session or EntityManager in various ways. And once you have a java.sql.Connection object, well, you can use something like a PreparedStatement to execute your query anyway.
Example fix:
Session session = entityManager.unwrap(Session.class);
SessionFactoryImplementor sessionFactoryImplementation = (SessionFactoryImplementor) session.getSessionFactory();
ConnectionProvider connectionProvider = sessionFactoryImplementation.getConnectionProvider();
try {
connection = connectionProvider.getConnection();
PreparedStatement ps = connection.prepareStatement("SELECT 1 FROM Location WHERE id <> ? AND locationName = ?");
ps.setLong(1, id);
ps.setString(2, checkedValue);
ResultSet rs = ps.executeQuery();
boolean result = rs.next();//found any results? if we can retrieve a row: yes!
rs.close();
return result;
}//catch SQLException etc...
//finally, close resources (only the resultset!)