Spring JPA Transaction ID - spring-boot

I have added an attribute to all my entities - transaction id - which is a sequence generated value that I bump up once in each transaction.
I also store the transaction id with user and start/end times so I have an audit trail for every change in the database.
What is the best way to handle storing a complete graph, where I basically only want to apply the transaction id to those entities that are actually dirty?
I can put a #PrePersist and #PreUpdate on the transaction id column, but how do I retrieve the value for the current transaction id? Is there a way to store and retrieve a value on the transaction object or other JPA controller? Do I need to use a ThreadLocal solution?

Ok, here is what I did. It seems to work in all of the use cases, though I have not done any performance testing, etc. If anyone sees anything that may be non-optimal or may fail in certain situations, please point it out.
Here is the base service class that all #Service implementations must extend:
public class BaseService
{
private final ActivityService activityService;
private final ApplicationEventPublisher applicationEventPublisher;
public static ThreadLocal<Activity> transaction = new ThreadLocal<>();
public BaseService(ActivityService activityService, ApplicationEventPublisher applicationEventPublisher)
{
this.activityService = activityService;
this.applicationEventPublisher = applicationEventPublisher;
}
Object executeWithinActivity(Updater updater)
{
boolean startedLocally = false;
try
{
if (transaction.get() == null)
{
startedLocally = true;
Activity activity = activityService.startTransaction();
transaction.set(activity);
}
return updater.execute(transaction.get());
}
finally
{
if (startedLocally)
{
applicationEventPublisher.publishEvent(new TransactionEvent());
Activity activity = transaction.get();
activityService.endTransaction(activity);
}
}
}
protected interface Updater
{
Object execute (Activity activity);
}
static class TransactionEvent
{
}
}
Activity is the entity that represents the stored transaction id:
#Entity
#Getter #Setter
#Table(name = "transactions", schema = "public", catalog = "euamdb")
public class Activity
{
#Id
#Column(name = "transaction_id", nullable = false)
#GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "tx_generator")
#SequenceGenerator(name = "tx_generator", sequenceName = "transaction_seq", allocationSize = 1)
private long transactionId;
#Basic
#Column(name = "user_id", length = 24)
private String userId;
#Basic
#Column(name = "transaction_start")
#CreationTimestamp
private Date transactionStart;
#Basic
#Column(name = "transaction_end")
#UpdateTimestamp
private Date transactionEnd;
#Override
public boolean equals(Object o)
{
if (this == o) return true;
if (!(o instanceof Activity)) return false;
Activity that = (Activity) o;
return transactionId == that.transactionId;
}
#Override
public int hashCode()
{
return Long.hashCode(transactionId);
}
}
ActivityService (which does not extend BaseService):
#Service
public class ActivityService
{
private final ActivityRepository activityRepository;
private final AuthUserService authService;
#Autowired
public ActivityService(ActivityRepository activityRepository, AuthUserService authService)
{
this.activityRepository = activityRepository;
this.authService = authService;
}
#Transactional
public Activity startTransaction()
{
Activity activity = new Activity();
activity.setTransactionStart(new Date());
activity.setUserId(authService.getAuthenticatedUserId());
activityRepository.save(activity);
return activity;
}
#Transactional
public void endTransaction(Activity activity)
{
activity.setTransactionEnd(new Date());
activityRepository.save(activity);
}
}
The base entity class for all entities (excepting Activity):
#MappedSuperclass
#Getter #Setter
public class BaseEntity
{
#Basic
#Column(name = "transaction_id")
private Long transactionId;
#PrePersist
#PreUpdate
public void setupTransaction ()
{
ThreadLocal<Activity> transaction = BaseService.transaction;
Activity activity = transaction.get();
long transactionId = activity.getTransactionId();
setTransactionId(transactionId);
}
}
An example of a service:
#Service
public class OrganizationService extends BaseService
{
private final OrgUserRepository orgUserRepository;
private final UserService userService;
#Autowired
public OrganizationService(ActivityService activityService,
OrgUserRepository orgUserRepository,
UserService userService,
ApplicationEventPublisher applicationEventPublisher)
{
super(activityService, applicationEventPublisher);
this.orgUserRepository = orgUserRepository;
this.userService = userService;
}
#Transactional
public OrgUser save(User user, OrgUser orgUser)
{
return (OrgUser) executeWithinActivity(activity ->
{
orgUser.setUser(userService.save(user));
return orgUserRepository.save(orgUser);
});
}
}
UserService also will extend BaseService and the save(OrgUser) method will also executeWithinActivity.
Finally, the commit listener:
#Component
public class AfterCommitListener
{
#TransactionalEventListener(phase = TransactionPhase.AFTER_COMPLETION)
public void doAfterTxComplete(BaseService.TransactionEvent event)
{
BaseService.transaction.remove();
}
}

Related

Spring boot JPARepository doesn't execute save

Im building a simple app in spring boot and having problems with saving an entity to the db. The bank account is saved normally, but the transaction is not being saved.
The weird thing is that there are no errors or anything. On the first transactionRepository call the method is exited and the app continues normally. I don't get any message in the console or and didn't find any problems using debugging.
Here is the makeTransaction() method, which executes normally until transactionRepository.save(...)
public void makeTransaction(Iban sourceIban, Iban destinationIban, double amount)
throws Exception {
BankAccountEntity sourceAccount = getBankAccountByIban(sourceIban);
BankAccountEntity destinationAccount = getBankAccountByIban(destinationIban);
Date currentDate = new Date();
TransactionEntity sourceTransaction = new TransactionEntity(sourceAccount, sourceIban, destinationIban, currentDate, amount);
TransactionEntity destinationTransaction = new TransactionEntity(destinationAccount, sourceIban, destinationIban, currentDate, amount);
if (sourceAccount.getAmountDeductible() < amount || amount < 0) { // TODO amount checker
throw new Exception(ILLEGAL_TRANSACTION_AMOUNT_TEXT); // TODO change exception type
}
sourceAccount.reduceBalance(amount);
sourceAccount.addTransaction(sourceTransaction);
destinationAccount.increaseBalance(amount);
destinationAccount.addTransaction(destinationTransaction);
bankAccountRepository.save(sourceAccount);
bankAccountRepository.save(destinationAccount);
transactionRepository.save(sourceTransaction);
transactionRepository.saveAndFlush(destinationTransaction);
}
The class has the repositories initialized as follows
private final BankAccountRepository bankAccountRepository;
private final TransactionRepository transactionRepository;
public BankAccountService(#Autowired BankAccountRepository bankAccountRepository,
#Autowired TransactionRepository transactionRepository) {
this.bankAccountRepository = bankAccountRepository;
this.transactionRepository = transactionRepository;
}
The BankAccountRepository looks like this
public interface BankAccountRepository extends JpaRepository<BankAccountEntity, UUID> {
Optional<BankAccountEntity> findByIban(String iban);
}
And the TransactionRepository like this
public interface TransactionRepository extends JpaRepository<TransactionEntity, UUID> {}
The TransactionEntity looks like this
#Entity
#Table(name = "transactions")
public class TransactionEntity {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
#Column(name = "transaction_id", nullable = false)
#Type(type = "uuid-char")
private Long transactionId;
private String sourceIban;
private String destinationIban;
private long dateExecuted;
private double amount;
#ManyToOne
#JoinColumn(name = "bank_account_id")
private BankAccountEntity bankAccount;
public TransactionEntity() {
}
public TransactionEntity(BankAccountEntity bankAccount, Iban sourceIban, Iban destinationIban, Date dateExecuted, double amount) {
this.bankAccount = bankAccount;
this.sourceIban = sourceIban.toString();
this.destinationIban = destinationIban.toString();
this.amount = amount;
this.dateExecuted = dateExecuted.getTime();
}
public BankAccountEntity getBankAccount() {
return bankAccount;
}
public void setBankAccount(BankAccountEntity bankAccount) {
this.bankAccount = bankAccount;
}
public Long getTransactionId() {
return transactionId;
}
public void setTransactionId(Long transactionId) {
this.transactionId = transactionId;
}
}

Spring Controller Returns Object Incompletely

There are three classes (Course, Lesson, User).
#EqualsAndHashCode(callSuper = true)
#Entity
#Table(name = "usr")
#Data
public class User extends RepresentationModel<User> implements UserDetails {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String firstname;
private String lastname;
private String username;
private String password;
#ElementCollection(targetClass = ERole.class, fetch = FetchType.EAGER)
#CollectionTable(name = "user_role", joinColumns = #JoinColumn(name = "user_id"))
#Enumerated(EnumType.STRING)
private Set<ERole> roles;
}
#Data
#Entity
#NoArgsConstructor
public class Lesson extends RepresentationModel<Lesson> {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String startTime;
private String endTime;
private String dayOfWeek;
#ManyToOne
private User teacher;
}
#EqualsAndHashCode(callSuper = true)
#Data
#Entity
public class Course extends RepresentationModel<Course> {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private Date startDate;
private Date endDate;
private String name;
#ManyToMany(cascade = CascadeType.ALL, fetch = FetchType.EAGER)
private Set<User> teachers;
#ManyToMany(cascade = CascadeType.ALL, fetch = FetchType.EAGER)
private Set<User> students;
private String description;
#ManyToMany(cascade = CascadeType.ALL)
private Set<Lesson> lessons;
}
And also RestController (CoursesController). When accessing the server at /courses, I get the correct server response with all fields
.
#RestController
#RequestMapping("/courses")
public class CoursesController {
private final CourseService courseService;
private final UserService userService;
private final LessonService lessonService;
#Autowired
public CoursesController(CourseService courseService, UserService userService, LessonService lessonService) {
this.courseService = courseService;
this.userService = userService;
this.lessonService = lessonService;
}
#GetMapping
#Operation(
summary = "getAllCourses",
description = "Returns all available courses"
)
public ResponseEntity<Page<Course>> getAllCourses(#PageableDefault(sort = "id", size = 5) Pageable pageable) {
try {
Page<Course> coursePage = courseService.findAll(pageable);
for (Course course : coursePage.getContent())
course.add(linkTo(methodOn(CoursesController.class).getCourse(course.getId().toString())).withSelfRel());
return ResponseEntity.ok(courseService.findAll(pageable));
}
catch (Exception e) {
return new ResponseEntity<>(HttpStatus.BAD_REQUEST);
}
}
#GetMapping("/{course-id}")
#Operation(
summary = "getCourse",
description = "Returns course by ID"
)
public ResponseEntity<Course> getCourse(#PathVariable ("course-id") String courseId) {
try {
Course course = courseService.getCourseById(courseId);
course.add(linkTo(methodOn(CoursesController.class).getCourse(courseId)).withSelfRel());
return ResponseEntity.ok(course);
} catch (Exception e) {
return new ResponseEntity<>(HttpStatus.NOT_FOUND);
}
}
}
Why, when requesting a course by ID (GET /courses/{id}), does Spring return an incomplete object (despite the fact that I manually added several teachers, students and lessons)?
I need to get all the fields of my object.
My CourseRepository below.
#Repository
#Transactional
public interface CourseRepository extends JpaRepository<Course, Long> {
}
My CourseService below.
#Service
public class CourseService {
private final CourseRepository courseRepository;
private final LessonRepository lessonRepository;
private final UserRepository userRepository;
#Autowired
public CourseService(CourseRepository courseRepository, LessonRepository lessonRepository, UserRepository userRepository) {
this.courseRepository = courseRepository;
this.lessonRepository = lessonRepository;
this.userRepository = userRepository;
}
public Page<Course> findAll(Pageable pageable) {
return courseRepository.findAll(pageable);
}
public Course createCourse(CourseDto courseDto) {
Course course = new Course(courseDto.getStartDate(), courseDto.getEndDate(), courseDto.getName(), courseDto.getDescription());
return courseRepository.saveAndFlush(course);
}
public Optional<Course> getCourseById(String id) {
return courseRepository.findById(Long.parseLong(id));
}
public Course updateCourse(CourseDto courseDto, String id) {
Course course = courseRepository.findById(Long.parseLong(id)).get();
course.setStartDate(courseDto.getStartDate());
course.setEndDate(courseDto.getEndDate());
course.setName(courseDto.getName());
course.setDescription(courseDto.getDescription());
return courseRepository.saveAndFlush(course);
}
public Page<Lesson> getLessonsByCourse(String courseId, Pageable pageable) {
Course course = courseRepository.findById(Long.parseLong(courseId)).get();
return new PageImpl<>(new ArrayList<>(course.getLessons()), pageable, course.getLessons().size());
}
public Course addLesson(String courseId, LessonDto lessonDto) {
Course course = courseRepository.findById(Long.parseLong(courseId)).get();
Lesson lesson = new Lesson();
lesson.setStartTime(lessonDto.getStartTime());
lesson.setEndTime(lessonDto.getFinishTime());
lesson.setDayOfWeek(lessonDto.getDayOfWeek());
lesson.setTeacher(userRepository.getUserById(lessonDto.getTeacherId()));
lessonRepository.saveAndFlush(lesson);
System.out.println(lesson);
course.getLessons().add(lesson);
return courseRepository.saveAndFlush(course);
}
public void deleteCourse(String id) {
courseRepository.deleteById(Long.parseLong(id));
}
}
Which I would (or might) expect as well. I would links to be generated for those additional relationshps (at least normally with Spring Data RESt handling this is what would happen). I wonder what happens if you ditch the RepresentationModel from your JPA model and just expose Course then. As stated you don't really want your JPA and HATEOAS stuff to be intertwined. You want to have a specialized projection/dto to expose. WHy does it work for your findAll. well you aren't adding links to it (although you think it does but your findAll executes twice!).
Removed RepresentationModel from User class.
Thx to #M.Deinum

How to combine two DAOs in generic way with only one method in Service layer in Spring Boot

Is it possible to combine two DAOs into one Service method?
I want to create a generic method which will choose correct DAO based on the input parameter. What for now I came up with is the method which will accept Dao from the outside the service object. But this requires to initialize appropriate Dao in the Controller which is a little bit ugly...
Measurement is just an interface for Temperature.java and Humidity.java entities with separate tables on PostgreSQL.
#Service
public class MeasurementService {
#Autowired
private TemperatureDao temperatureDao;
#Autowired
private HumidityDao humidityDao;
public<T extends PagingAndSortingRepository<Measurement, Long>> void insertMeasurementForUser(String username, List<Measurement> measurements, T dao) {
dao.saveAll(measurements);
}
}
TemperatureDao.java
#Repository
public interface TemperatureDao extends PagingAndSortingRepository<Temperature, Long> {
#Query("select u from Temperature u where u.owner = ?1 order by u.id desc")
List<Temperature> findLatestTemperatureForUser(User user, Pageable pageable);
}
HumidityDao.java
#Repository
public interface HumidityDao extends PagingAndSortingRepository<Humidity, Long> {
#Query("select u from Humidity u where u.owner = ?1 order by u.id desc")
List<Humidity> findLatestHumidityForUser(User user, Pageable pageable);
}
Temperature.java
#Entity
#Table(name = "temperature")
public class Temperature implements Measurement {
#Id
#Column(name = "id")
#GeneratedValue(strategy = GenerationType.SEQUENCE)
private Long id;
#Column(name = "th1value")
private Float th1Value;
#Column(name = "timestamp")
#NotNull
private LocalDateTime timestamp;
#ManyToOne
#JoinColumn(name = "user_id")
#NotNull
private User owner;
public Temperature() {
}
public Temperature(Float th1Value, LocalDateTime timestamp, User owner) {
this.th1Value = th1Value;
this.timestamp = timestamp;
this.owner = owner;
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
#JsonDeserialize(using = LocalDateTimeDeserializer.class)
public LocalDateTime getTimestamp() {
return timestamp;
}
#JsonSerialize(using = LocalDateTimeSerializer.class)
public void setTimestamp(LocalDateTime timestamp) {
this.timestamp = timestamp;
}
#Override
public User getOwner() {
return owner;
}
#Override
public void setOwner(User owner) {
this.owner = owner;
}
}
Humidity.java
#Entity
#Table(name = "humidity")
public class Humidity {
#Id
#Column(name = "id")
#GeneratedValue(strategy = GenerationType.SEQUENCE)
private Long id;
#Column(name = "hum1value")
private Float hum1Value;
#Column(name = "timestamp")
#NotNull
private LocalDateTime timestamp;
#ManyToOne
#JoinColumn(name = "user_id")
#NotNull
private User owner;
public Humidity() {
}
public Humidity(Float hum1Value, LocalDateTime timestamp, User owner) {
this.hum1Value = hum1Value;
this.timestamp = timestamp;
this.owner = owner;
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
#JsonDeserialize(using = LocalDateTimeDeserializer.class)
public LocalDateTime getTimestamp() {
return timestamp;
}
#JsonSerialize(using = LocalDateTimeSerializer.class)
public void setTimestamp(LocalDateTime timestamp) {
this.timestamp = timestamp;
}
public User getOwner() {
return owner;
}
public void setOwner(User owner) {
this.owner = owner;
}
}
Any ideas?
You could write a Resolver pattern to return needed dao based on your conditions. You service will use the resolver to get the correct dao.
public HellDao implements BaseDao {
public void save();
}
public ByeDao implements BaseDao {
public void save();
}
public DaoResolver {
#Autowired
private helloDao;
#Autowired
private byeDao;
public BaseDao resolve(Object input) {
//based on input return the correct dao
BaseDao resolvedDao = null;
switch(input.enum) {
case Hello:
resolvedDao = helloDao;
break;
case Hello:
resolvedDao = byeDao;
break;
default:
//decide something for default
}
return resolvedDao;
}
}
public class MyService {
#Autowired
private DaoResolver daoResolver;
public Object doSomething() {
BaseDao dao = daoResolver.resolve(someObject);
//you will get HelloDao or ByeDao based on the input
dao.save();
}
}
You can check for the type of measurements using instanceof so you could do it without generics.
public void insertMeasurementForUser(String username, List<Measurement> measurements) {
if(measurements.get(0) instanceof Temperature)
temperatureDao.saveAll(measurements);
else if(measurements.get(0) instanceof Humidity)
humidityDao.saveAll(measurements);
}

Hibernate transaction and session with multiple save

Thanks, let me completely change it.
Using:
Spring Boot, Hibernate JPA
I have created a link table with a composite primary key across all 3 columns(event_attendee_link_program)
I used the JPA tools in STS IDE to generate Entities from my tables and it came up with the below code. I removed some of the columns to save space.
EventAttendee.java
#Entity
#Table(name="event_attendee")
#NamedQuery(name="EventAttendee.findAll", query="SELECT e FROM EventAttendee e")
public class EventAttendee implements Serializable {
private static final long serialVersionUID = 1L;
#Id
#Column(name="attendee_id")
private long attendeeId;
//bi-directional many-to-one association to EventAttendeeLinkProgram
#OneToMany(mappedBy="eventAttendee")
private List<EventAttendeeLinkProgram> eventAttendeeLinkPrograms;
public List<EventAttendeeLinkProgram> getEventAttendeeLinkPrograms() {
return this.eventAttendeeLinkPrograms;
}
public void setEventAttendeeLinkPrograms(List<EventAttendeeLinkProgram> eventAttendeeLinkPrograms) {
this.eventAttendeeLinkPrograms = eventAttendeeLinkPrograms;
}
public EventAttendeeLinkProgram addEventAttendeeLinkProgram(EventAttendeeLinkProgram eventAttendeeLinkProgram) {
getEventAttendeeLinkPrograms().add(eventAttendeeLinkProgram);
eventAttendeeLinkProgram.setEventAttendee(this);
return eventAttendeeLinkProgram;
}
public EventAttendeeLinkProgram removeEventAttendeeLinkProgram(EventAttendeeLinkProgram eventAttendeeLinkProgram) {
getEventAttendeeLinkPrograms().remove(eventAttendeeLinkProgram);
eventAttendeeLinkProgram.setEventAttendee(null);
return eventAttendeeLinkProgram;
}
}
EventAttendeeLinkProgram.java
#Entity
#Table(name="event_attendee_link_program")
#NamedQuery(name="EventAttendeeLinkProgram.findAll", query="SELECT e FROM EventAttendeeLinkProgram e")
public class EventAttendeeLinkProgram implements Serializable {
private static final long serialVersionUID = 1L;
#EmbeddedId
private EventAttendeeLinkProgramPK id;
//bi-directional many-to-one association to EventAttendee
#ManyToOne
#JoinColumn(name="attendee_id", insertable=false, updatable=false)
private EventAttendee eventAttendee;
//bi-directional many-to-one association to EventOptionsAttendeeType
#ManyToOne
#JoinColumn(name="attendee_type_id", insertable=false, updatable=false)
private EventOptionsAttendeeType eventOptionsAttendeeType;
//bi-directional many-to-one association to EventProgram
#ManyToOne
#JoinColumn(name="program_id", insertable=false, updatable=false)
private EventProgram eventProgram;
public EventAttendeeLinkProgram() {
}
public EventAttendeeLinkProgramPK getId() {
return this.id;
}
public void setId(EventAttendeeLinkProgramPK id) {
this.id = id;
}
public EventAttendee getEventAttendee() {
return this.eventAttendee;
}
public void setEventAttendee(EventAttendee eventAttendee) {
this.eventAttendee = eventAttendee;
}
public EventOptionsAttendeeType getEventOptionsAttendeeType() {
return this.eventOptionsAttendeeType;
}
public void setEventOptionsAttendeeType(EventOptionsAttendeeType eventOptionsAttendeeType) {
this.eventOptionsAttendeeType = eventOptionsAttendeeType;
}
public EventProgram getEventProgram() {
return this.eventProgram;
}
public void setEventProgram(EventProgram eventProgram) {
this.eventProgram = eventProgram;
}
}
EventAttendeeLinkProgramPK.java
#Embeddable
public class EventAttendeeLinkProgramPK implements Serializable {
//default serial version id, required for serializable classes.
private static final long serialVersionUID = 1L;
#Column(name="attendee_id", insertable=false, updatable=false)
private int attendeeId;
#Column(name="attendee_type_id", insertable=false, updatable=false)
private int attendeeTypeId;
#Column(name="program_id", insertable=false, updatable=false)
private int programId;
public EventAttendeeLinkProgramPK() {
}
public int getAttendeeId() {
return this.attendeeId;
}
public void setAttendeeId(int attendeeId) {
this.attendeeId = attendeeId;
}
public int getAttendeeTypeId() {
return this.attendeeTypeId;
}
public void setAttendeeTypeId(int attendeeTypeId) {
this.attendeeTypeId = attendeeTypeId;
}
public int getProgramId() {
return this.programId;
}
public void setProgramId(int programId) {
this.programId = programId;
}
public boolean equals(Object other) {
if (this == other) {
return true;
}
if (!(other instanceof EventAttendeeLinkProgramPK)) {
return false;
}
EventAttendeeLinkProgramPK castOther = (EventAttendeeLinkProgramPK)other;
return
(this.attendeeId == castOther.attendeeId)
&& (this.attendeeTypeId == castOther.attendeeTypeId)
&& (this.programId == castOther.programId);
}
public int hashCode() {
final int prime = 31;
int hash = 17;
hash = hash * prime + this.attendeeId;
hash = hash * prime + this.attendeeTypeId;
hash = hash * prime + this.programId;
return hash;
}
}
EventAttendeeServiceImpl.java
#Service
#Primary
public class EventAttendeeServiceImpl implements EventAttendeeService {
#Autowired
private EventAttendeeRepository eventAttendeeRepository;
#Autowired
private EventOptionsAttendeeTypeRepository eventOptionsAttendeeTypeRepository;
#Autowired
private EventProgramRepository eventProgramRepository;
#Override
#Transactional
public String addEventAttendee(EventAttendee eventAttendee) {
EventAttendeeLinkProgram ep = new EventAttendeeLinkProgram();
ep.setEventOptionsAttendeeType(eventOptionsAttendeeTypeRepository.findOne(2L));
ep.setEventProgram(eventProgramRepository.findOne(2L));
eventAttendee.setEventAttendeeLinkPrograms(new ArrayList<>());
eventAttendee.getEventAttendeeLinkPrograms().add(ep);
eventAttendeeRepository.save(eventAttendee);
return "";
}
With this in place, my code is not throwing any errors. It is saving the EventAttendee, but nothing is being saved to the EventAttendeeLinkProgram. Please Note: I am trying so save both EventAttendee and EventAttendeeLinkProgram entities. So I think hibernate should be smart enought to forst save EventAttendee and generating the Id for it, then use that Id to store in EventAttendeeLinkProgram.
Why don't you let spring do the heavy lifting:
First create a JPA repository in spring:
public interface UserRepository extends CrudRepository<User, Long>{
}
Then create your 2 entities with the relationship
#Entity
public class User {
#Id
#GeneratedValue(strategy=GenerationType.AUTO)
private Long id;
#Column(name = "name")
private String name;
#OneToMany(cascade = CascadeType.ALL, mappedBy = "user", orphanRemoval = true, fetch = FetchType.EAGER)
private List<UserType> userTypes;
And :
#Entity
public class UserType {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
#ManyToOne(fetch = FetchType.EAGER)
#JoinColumn(name = "user_id")
private User user;
My test looks like this:
#RunWith(SpringRunner.class)
#SpringBootTest(classes = Application.class)
public class UserRepositoryTest extends AbstractTest {
#Autowired
private UserRepository userRepository;
#Test
#Transactional
public void test1() throws SQLException {
showTables();
User user1 = makeUser("Greg");
userRepository.save(user1);
System.out.println(user1);
userRepository.save(makeUser("George"));
assertEquals(2, userRepository.count());
User user = userRepository.findOne(1l);
}
User makeUser(String name) {
User user = new User();
user.setName(name);
user.setUserTypes(new ArrayList<>());
user.getUserTypes().add(makeUserType("admin"));
user.getUserTypes().add(makeUserType("head chef"));
return user;
}
UserType makeUserType(String description) {
UserType userType = new UserType();
userType.setDescription(description);
return userType;
}
}
First of all, user save return the identifier directly
Long insertId = (Long) session.save(user);
Then you'd better call the rollback on the txtransaction itself instead of retrieving again the transaction from the session.
Finally, when using spring you should consider to let spring manage the transaction itself (container managed transaction)using #Transactional annotation instead of using user managed transaction. It's logical as you let spring manage the session for you (sessionFactory.getCurrentSession()) and both session and transaction should have the same scope (e.g. the unit of work).
Consider reading some literature on Session (e.g. JPA entityManager) and transaction management.

Spring + JPA "Lock wait timeout exceeded; try restarting transaction"

I'm new to Spring and JPA and I encountered the problem specified in the title. To simplify the problem, I have two classes: User and FeedItem. User can have more FeedItems but the relationship is bi-directional (FeedItem knows with which User it's associated). They're both persisted in the database using JPA+Hibernate:
#Entity
#Table
public class User
{
#Id
#GeneratedValue
#Column(name = "id", nullable = false, length = 8)
private int id;
#Column(nullable = false, length = 32, unique = true)
private String alias;
#OneToMany(cascade = CascadeType.ALL, mappedBy = "user")
private List<FeedItem> feedItems = new ArrayList<FeedItem>();
public User()
{
}
public User(String alias)
{
this.alias = alias;
}
... getters/setters...
}
#Entity
#Table
public class FeedItem
{
#Id
#GeneratedValue
#Column(name = "id", nullable = false, length = 16)
private int id;
#Column(nullable = false, length = 64)
private String title;
#ManyToOne
#JoinColumn(name = "userId", nullable = false)
private User user;
public FeedItem()
{
}
public FeedItem(String title, User user)
{
this.title = title;
this.user = user;
}
... getters/setters...
}
The DAOs:
#Repository
public class UserJpaDao implements UserDao
{
private EntityManager em;
#Transactional
public User save(User user)
{
return this.em.merge(user);
}
#Transactional
public void delete(User user)
{
this.em.remove(user);
}
#Transactional(readOnly = true)
public User findById(int id)
{
return this.em.find(User.class, id);
}
#PersistenceContext
void setEntityManager(EntityManager entityManager)
{
this.em = entityManager;
}
}
#Repository
public class FeedItemJpaDao implements FeedItemDao
{
private EntityManager em;
#Transactional
public FeedItem save(FeedItem feedItem)
{
return this.em.merge(feedItem);
}
#Transactional
public void delete(FeedItem feedItem)
{
this.em.remove(feedItem);
}
#Transactional
public FeedItem findById(int id)
{
return this.em.find(FeedItem.class, id);
}
#PersistenceContext
void setEntityManager(EntityManager entityManager)
{
this.em = entityManager;
}
}
This is the test giving the error:
#RunWith(SpringJUnit4ClassRunner.class)
public class FeedItemJpaDaoTest
{
#Autowired
private DriverManagerDataSource dataSource;
#Autowired
private FeedItemJpaDao feedItemDao;
#Autowired
private UserJpaDao userDao;
#Before
#Transactional
public void setUp() throws Exception
{
DatabaseOperation.CLEAN_INSERT.execute(getConnection(), getDataSet());
}
#After
#Transactional
public void tearDown() throws Exception
{
DatabaseOperation.DELETE_ALL.execute(getConnection(), getDataSet());
}
#Test
#Transactional
public void testSave() throws Exception
{
User user = userDao.findById(3);
FeedItem feedItem = new FeedItem("Achievement unlocked!", user);
feedItem = feedItemDao.save(feedItem);
assertEquals(feedItem, feedItemDao.findById(feedItem.getId()));
}
private IDatabaseConnection getConnection() throws Exception
{
return new DatabaseConnection(dataSource.getConnection());
}
private IDataSet getDataSet() throws Exception
{
return new FlatXmlDataSetBuilder().build(new File("src/test/resources/dataset.xml"));
}
}
I don't understand why the error is happening -- any suggestion is appreciated!
Thank you.
EDIT: Seems like the problem is due to DbUnit: if I comment out the tearDown() method, the error doesn't occour
Solved the problem following this: http://tadaya.wordpress.com/2008/04/27/transaction-aware-datasource-use-dbunit-hibernate-in-spring/

Resources