I try to build a bidirectional relationship. I am using Spring Boot 1.5.4.RELEASE with Spring Boot JPA to generate my repositories. I try to save two entities which are associated to each other, but it isnt working. I commented the test-statements which fails.
My Entites:
Driver:
#Entity
#ToString
#EqualsAndHashCode
public class Driver {
public static final String COLUMN_CAR = "car";
#Id
#GeneratedValue
private long id;
#Getter
#Setter
#OneToOne(cascade = CascadeType.ALL)
#JoinColumn(name = COLUMN_CAR)
private Car car;
}
Car:
#Entity
#ToString
#EqualsAndHashCode
public class Car {
#Id
#GeneratedValue
private long id;
#Getter
#Setter
#OneToOne(mappedBy = Driver.COLUMN_CAR)
private Driver driver;
}
I used Spring JPA to generate repositories.
DriverRepository:
#Repository
public interface DriverRepository extends CrudRepository<Driver, Long> { }
CarRepository:
#Repository
public interface CarRepository extends CrudRepository<Car, Long> { }
Test:
#RunWith(SpringRunner.class)
#SpringBootTest
#Transactional
public class StackoverflowTest {
#Autowired
private DriverRepository driverRepository;
#Autowired
private CarRepository carRepository;
#Test
public void test1() {
Driver driver = driverRepository.save(new Driver());
Car car = carRepository.save(new Car());
driver.setCar(car);
driverRepository.save(driver);
/* Success, so the driver got the car */
driverRepository.findAll().forEach(eachDriver -> Assert.assertNotNull(eachDriver.getCar()));
/* Fails, so the car doesnt got the driver */
carRepository.findAll().forEach(eachCar -> Assert.assertNotNull(eachCar.getDriver()));
}
#Test
public void test2() {
Driver driver = driverRepository.save(new Driver());
Car car = carRepository.save(new Car());
car.setDriver(driver);
carRepository.save(car);
/* Success, so the car got the driver */
carRepository.findAll().forEach(eachCar -> Assert.assertNotNull(eachCar.getDriver()));
/* Fails, so the driver doesnt got the car */
driverRepository.findAll().forEach(eachDriver -> Assert.assertNotNull(eachDriver.getCar()));
}
}
In both tests the last statement fails. Any ideas? Thanks in Advice.
Several mistakes in what you posted.
First:
#OneToOne(mappedBy = Driver.COLUMN_CAR)
mappedBy expects the name of the Java field/property on the other side of the association. Not the name of the database column. It works here because both happen to have the same name.
Second:
carRepository.findAll().forEach(eachCar -> Assert.assertNotNull(eachCar.getDriver()));
That fails simply because you're doing everything in a single transaction, and you failed to properly initialize the two sides of the association. So car.driver is just as you initialized it: null.
Third:
driverRepository.findAll().forEach(eachDriver -> Assert.assertNotNull(eachDriver.getCar()));
You made the same mistake as before, but worse. Here, you only initialized one side of the association, but you initialized the inverse side of the association (the one which has the mappedBy attribute). So the association won't even be saved in the database, as it would have been in your previous snippet.
Related
for example i have two entities: first is student and another is address. i want to insert data on both tables with foreign key relationship, but their is one condition: if any error occurs while inserting the data in any tables the all changes should be roll back.
how can i do please help
class Student{
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private int id;
private String firstName;
#OneToOne(fetch = FetchType.EAGER)
#JoinColumn(name="address_id",referencedColumnName = "id")
private Address address;
}
class Address {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private int id;
private String city;
}
#Service
public class StudentService {
#Autowired
private StudentRepo studentRepo;
#Autowired
private AddressRepo addressRepo;
public Student createStudent(StudentRequest studentRequest) {
Student student=new Student(studentRequest);
Address address=new Address();
address.setCity(studentRequest.getCity());
address.setStreet(studentRequest.getStreet());
address=addressRepo.save(address);
student.setAddress(address);
student=studentRepo.save(student);
return student;
}
This is a very general question, so I will try to point you in the right direction. For this I assume:
you are using spring-data-jpa
you have a datasource already configured
your application starts up correctly
you already configured the #Repository or you know how to use the EntityManager via #PersistenceContext
I will assume the Repository solution
The good news is: Persisting objects that are marked using OneToOne, OneToMany, ManyToOne and ManyToMany is a core feature, so most of this is already handled by JPA/Hibernate. All you have to ensure is that you read the documentation and follow it.
First step is to understand Cascading. Cascading is not turned on by default. There are tons of resources on the web, that help you understand this, I will point you to this one. This means that you will need to set the cascade attribute on the OneToOne annotation (see referenced tutorial).
Student.java
class Student{
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private int id;
private String firstName;
#OneToOne(fetch = FetchType.EAGER, cascade = CascadeType.ALL)
#JoinColumn(name="address_id",referencedColumnName = "id")
private Address address;
}
Then, using a normal #Repository:
public interface StudentRepository extends JpaRepository<Student, Long> {
}
Now you have correctly working mapping and the repository.
Once you have done this, you should be ready to roll, given the above assumptions hold. You can now write a small testcase for a student, e.g.
#DataJpaTest
class StudentRepositoryTest {
#Autowired
private StudentRepository studentRepository;
#Autowired
private AddressRepository addressRepository;
#BeforeEach
void setup() {
assumeThat(studentRepository).isNotNull();
assumeThat(addressRepository).isNotNull();
}
#Test
void testSaveStudent {
assumeThat(studentRepository.count()).isZero();
assumeThat(addressRepository.count()).isZero();
var student = new Student();
var address = new Address();
address.setCity("New York");
student.setFirstName("Max");
student.setAddress(address);
studentRepository.save(student);
assertThat(studentRepository.count()).isOne();
assertThat(addressRepository.count()).isOne();
}
Second: In regards to your transaction question. Per default when you invoke a repository's save method a transaction is started. So in the very base case you don't necessarily need a transaction. Although, once you deal with more complex business cases it's necessary to understand JPA's transaction manager. Transactions usually happen on the service layer. See links below for more info on Transactions.
#Service
#Transactional(readOnly = true)
public class StudentService {
#Autowired
private StudentRepository studentRepository;
#Transactional
public Student assignStudentToAddress(Student s, Address a) {
s.setAddress(a);
return studentRepository.save(s);
}
}
Some thoughts:
Are you sure that one student can only stay at one address? This would mean that living arrangements like in Big Bang Theory (shared apartment) wouldn't work. For testing purposes this works of course.
Consider the reverse: what if we have an address screen and we want to add students to it?
More resources:
Beginner's guide to mapping by VladMihalcea
Mapping OneToOne beginner friendly: by Baeldung and slightly more advanced by VladMihalcea
Mapping OneToMany beginner friendly: by baeldung and slightly more advanced: by VladMihalcea
Using Transactional by VladMihalcea and by MarcoBehler
With Stateless EJB :
#Stateless
public StudentDAO {
#PersistenceContext
private EntityManager em;
public Student createStudent(StudentRequest studentRequest) {
Student student=new Student(studentRequest);
Address address=new Address();
address.setCity(studentRequest.getCity());
address.setStreet(studentRequest.getStreet());
em.persist(address);
student.setAddress(address);
em.persist(student);
return student;
}
}
Say we have 2 classes Driver and Car with the Driver having a many-to-one relationship with the Car as follows.
#Entity
#Table(name = "driver")
public class Driver {
#Id #GeneratedValue
private Long id;
#ManyToOne
private Car car;
...
// getter setter ignored for brevity
}
Is there a way to set the value of car via post request for example by referencing car by its id by just JPA/Hibernate annotations? I'm still sort of new to Spring boot, so I was actually thinking of creating a new attribute Long carId and then apply #JsonIgnore to car, according to https://stackoverflow.com/a/42633336/9324939. Or is there any other suggestion or approach to get what I'm trying to achieve?
PS: In the database, they are already connected by reference.
-- in postgres
...
driver_id BIGINTEGER REFERENCES car (id)
...
please take a look here for a sample project I made to address this:
https://github.com/Fermi-4/StackOverflow---JPA-Relationships
Once started locally, use Postman to set the car to a driver:
http://localhost:9090/api/assigncar?driverId=1&carId=1
Driver Entity - using Lombok
#Entity
#Table(name = "driver")
#Data
public class Driver {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long driverId;
#ManyToOne(cascade = CascadeType.ALL, fetch = FetchType.LAZY)
private Car car;
}
Car Entity - using Lombok and #JsonIgnore to prevent infinite recursion
#Entity
#Table(name = "car")
#Data
public class Car {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long carId;
#JsonIgnore
#OneToMany
private List<Driver> drivers = new ArrayList<Driver>();
}
Repositories
public interface CarRepository extends JpaRepository<Car, Long> { }
public interface DriverRepository extends JpaRepository<Driver, Long> { }
Controller Class
#RequestMapping("/api")
#RestController
public class DriverController {
#Autowired
CarRepository _carRepo;
#Autowired
DriverRepository _driverRepo;
#PostMapping("/assigncar")
public Driver assignCarToDriver(#RequestParam Long driverId,#RequestParam Long carId) {
Car car = _carRepo.findById(carId).get();
Driver driver = _driverRepo.findById(driverId).get();
driver.setCar(car);
return _driverRepo.save(driver);
}
}
When you add new driver via post request , you can assign a new car or an existing car within your json object (you can try to add cascadeType.ALL within your #ManyToOne)
I am creating a spring boot application and use it's build in JpaRepository interface to store my entities. I have the following two entities (removed getters and setters for readability):
Profile entity
#Entity
public class Profile {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
private long id;
#OneToMany(mappedBy = "profileOne", orphanRemoval = true)
private List<Match> matchOnes;
#OneToMany(mappedBy = "profileTwo", orphanRemoval = true)
private List<Match> matchTwos;
}
Match entity
#Entity
#Table(uniqueConstraints={
#UniqueConstraint(columnNames = { "profileOne_id", "profileTwo_id" })
})
public class Match {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
private long id;
#ManyToOne
#JoinColumn(name = "profileOne_id")
private Profile profileOne;
#ManyToOne
#JoinColumn(name = "profileTwo_id")
private Profile profileTwo;
}
To understand the JpaRepository behavior I wrote the following unit test.
#RunWith(SpringRunner.class)
#DataJpaTest
public class IProfileDaoTest {
#Autowired
private IProfileDao profileDao; //This class extends JpaRepository<Profile, long>
#Autowired
private IMatchDao matchDao; //This class extends JpaRepository<Match, long>
#Test
public void saveProfileTest() throws Exception {
#Test
public void profileMatchRelationTest() throws Exception {
//Test if matches stored by the IMatchDao are retrievable from the IProfileDao
Profile profileOne = new Profile("Bob"),
profileTwo = new Profile("Alex");
profileDao.saveAndFlush(profileOne);
profileDao.saveAndFlush(profileTwo);
matchDao.saveAndFlush(new Match(profileOne, profileTwo));
profileOne = profileDao.getOne(profileOne.getId());
Assert.assertEquals("Match not retrievable by profile.", 1, profileOne.getMatchOnes().size());
}
}
Now I expected the matches to have appeared in the profile entity but they do not. I also tried adding CascadeType.ALL to the #ManyToOne annotation in the match entity and adding FetchType.EAGER to the #OneToMany annotation in the profile entity.
Is it possible to get the matches saved with the matchDao by requesting a profile in the profileDao? Or should I find the matches with a profile with a separate function?
Spring Data repositories don't write to the database immediately for performance (and probably other) reasons. In tests, if you need to test query methods you need to use the TestEntityManager provided by #DataJpaTest (it's just the entity manager that the repositories use anyway in the background, but with a few convenience methods for testing).
Update 1:
The matches aren't added to the profile. To make sure the relationship is bidirectional the matches should have the profiles but the profiles should also have the matches.
Try this:
#RunWith(SpringRunner.class)
#DataJpaTest
public class IProfileDaoTest {
#Autowired
private IProfileDao profileDao; //This class extends JpaRepository<Profile, long>
#Autowired
private IMatchDao matchDao; //This class extends JpaRepository<Match, long>
#Autowired
private TestEntityManager testEntityManager;
#Test
public void saveProfileTest() throws Exception {
#Test
public void profileMatchRelationTest() throws Exception {
//Test if matches stored by the IMatchDao are retrievable from the IProfileDao
Profile profileOne = new Profile("Bob"),
profileTwo = new Profile("Alex");
//Persist the profiles so they exist when they are added to the match
entityManager.persistAndFlush(profileOne);
entityManager.persistAndFlush(profileTwo);
//Create and persist the match with two profiles
Match yourMatch = entityManager.persistFlushFind(new Match(profileOne, profileTwo));
//Add the match to both profiles and persist them again.
profileOne.matchOnes.add(yourMatch);
entityManager.persistAndFlush(profileOne);
profileTwo.matchTwos.add(yourMatch);
entityManager.persistAndFlush(profileTwo);
profileOne = profileDao.getOne(profileOne.getId());
Assert.assertEquals("Match not retrievable by profile.", 1, profileOne.getMatchOnes().size());
}
}
Everything in your test happens in the same JPA session. Such a session guarantees that every entity is included only once. So when you execute
profileOne = profileDao.getOne(profileOne.getId());
you are getting the exact instance back you created 5 lines above.
Hibernate nor any other JPA implementation will change anything in the entity for loading.
If you want to actually reload an entity you'll have to either evict it first from the entity manager or use a fresh Session/EntityManager.
For more details see chapter 3 of the JPA specification.
I found weird Hibernate behavior that I can not understand.
Let's say I have class A (was inspired with this question JPA: How to have one-to-many relation of the same Entity type)
#Entity
public class A {
#Id
private String id;
private String name;
#ManyToOne
#JoinColumn(name = "PARENT")
private A parent;
#OneToMany(mappedBy = "parent",cascade = { CascadeType.PERSIST, CascadeType.MERGE, CascadeType.REMOVE })
private Set<A> children;
// Getters, Setters, etc...
}
Also, say we have Spring JPA Repository
public interface ARepository extends JpaRepository<A, Long> {}
And test class where magic happens
#RunWith(SpringJUnit4ClassRunner.class)
#ContextConfiguration({ "classpath:spring/applicationContext-test.xml" })
public class ATest {
#Autowired
private ARepository aRepository;
#Test
public void testA() {
A parent = new A();
parent.setName("I am Parent: 121_1001_21");
parent.setId("121_1001_21");
A son = new A();
son.setName("I am Son: 121_1001_31");
son.setId("121_1001_31");
son.setParent(parent);
A daughter = new A();
daughter.setName("I am Daughter: 121_1001_32");
daughter.setId("121_1001_32");
daughter.setParent(son);
// daughter.setParent(parent);// yes, I'm intentionally creates wrong hierarchy
parent.setChildren(new HashSet<>(Arrays.asList(daughter, son)));// order of elements in set matters!
son.setChildren(new HashSet<>(Arrays.asList(daughter)));
aRepository.save(parent);
}
}
So the hierarchy is following:
Parent (121_1001_21)
Son (121_1001_31)
Daughter (121_1001_32)
Daughter (121_1001_32)
But this test fails on saving entity with
javax.persistence.EntityNotFoundException:
Unable to find com.orga.pcom.om.core.entity.productorder.A with id 121_1001_31
After hours of debugging I found that Hibernates tries to load linked entities and load it in this way:
Parent (121_1001_21) 1st load
Son (121_1001_31) 3rd load (this entity loading is fail!)
Daughter (121_1001_32) 2nd load
Daughter (121_1001_32) 2nd load
and fails! So, the questions are:
Why Hibernate loads something while it saves something? :)
What is the best way to fix this issue?
Important: please note that I'm talking about Play! framework version 1 (1.2.6), not 2.x
I have 2 entities in my Play! v1 application, like that:
#Entity
#Table(name = "T_FOO")
public class Foo extends GenericModel {
#Id
#Column(name = "NAME")
public String name;
public static List<Foo> paginate(int start, int count) {
return all().from(start).fetch(count);
}
}
and
#Entity
#Table(name = "T_BAR")
public class Bar extends GenericModel {
#Id
#Column(name = "NAME")
public String name;
public static List<Bar> paginate(int start, int count) {
return all().from(start).fetch(count);
}
}
In my controller, I can do that without problem:
public static void index() {
List<Foo> foo = Foo.paginate(1, 5);
List<Bar> bar = Bar.paginate(2, 5);
render(foo, bar);
}
As you can see, these 2 entities are quite similar, so I created a super class to regroup common methods:
#MappedSuperclass
public class MyGenericModel<T> extends GenericModel {
public static <T> List<T> paginate(int start, int count) {
return all().from(start).fetch(count);
}
}
and make it as the parent of my entities, for example:
#Entity
#Table(name = "T_FOO")
public class Foo extends MyGenericModel<Foo> {
#Id
#Column(name = "NAME")
public String name;
}
However, with this modification, I get the following error when I try to call Foo.paginate() method:
UnsupportedOperationException occured : Please annotate your JPA model with #javax.persistence.Entity annotation.
play.exceptions.JavaExecutionException: Please annotate your JPA model with #javax.persistence.Entity annotation.
at play.mvc.ActionInvoker.invoke(ActionInvoker.java:237)
at Invocation.HTTP Request(Play!)
Caused by: java.lang.UnsupportedOperationException: Please annotate your JPA model with #javax.persistence.Entity annotation.
It seems that Play! wants me to annotate MyGenericModel with #Entity annotation, which I don't want.
I thought that annotating MyGenericModel with #MappedSuperclass would help me to avoid this problem, but it is not the case.
What did I make wrong, and how to make it work correctly?
Thanks.
Unfortunately you can't call the function all() from your class MyGenericModel because that function is enhanced at runtime and only for classes annotated with #Entity.
Possible solution could be that you use your own entity manager for your queries.
Or you could go back to your first option, there was nothing wrong with that :)
Unfortunately (this is also my pain) Generics are not yet supported by Ebean:
https://groups.google.com/forum/#!searchin/ebean/generics/ebean/QWjpI0LRCiA/io-Lm_gfYE4J