I'm using Spring Boot 2.3.0 along with Spring Data JPA and Spring MVC. I have the following 2 entities:
Country.java
#Entity
#Table(name = "countries")
public class Country
{
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name = "country_id")
private Integer id;
#Column(name = "country_name" , nullable = false , length = 50)
#NotNull #Size(max = 50)
private String name;
#Column(name = "country_acronym" , length = 3)
#Size(max = 3)
private String acronym;
//Getters-Setters
//Equals-Hashcode (determines equality based only on name attribute)
}
City.java
#Entity
#Table(name = "cities")
public class City
{
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
#Column(name = "city_name")
#Size(max = 50)
private String name;
#OneToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "city_country")
private Country country;
//Getters-Setters
//Equals/Hashcode (all attributes)
}
What I want to achieve is to save cities through REST calls. One catch is that I want in the body of the request to provide only the name of the country and if that country exists in the countries table, then it must be able to find the reference by itself , else it should first insert a new country and then match the reference.
For complete reference, let me provide the Repository and Controller classes:
CityRepository.java
#Repository
public interface CityRepository extends JpaRepository<City,Integer>
{
}
MainController.java
#RestController
public class MainController
{
#Autowired
private CityRepository cityRepository;
#PostMapping(value = "/countries")
private void insertCountry(#RequestBody #Valid Country country)
{
countryRepository.save(country);
}
#PostMapping(value = "/cities")
public void insertCities(#RequestBody #Valid City city)
{
cityRepository.save(city);
}
}
A sample body of a request:
{
"name": "Nikaia",
"country": {
"name": "Greece"
}
}
The error I get is that Hibernate always tries to save the country, it never looks if it exists (I get a constraint violation). I guess that the country never gets proxied by Hibernate because it isn't yet a persisted entity. Is there a way I can easily solve that using Data JPA ? Or should I go a level lower and play with the EntityManager? Complete code samples would be greatly appreciated.
It's logical. For your desire feature fetch the country by country name. If exist then set that in city object.
#PostMapping(value = "/cities")
public void insertCities(#RequestBody #Valid City city)
{
Country country = countryRepository.findByName(city.getCountry().getName());
if(country != null) city.setCountry(country);
cityRepository.save(city);
}
And findByName in CountryRepository also
Country findByName(String name);
Related
I'm learning how Spring framework works and as an example I'm trying to save cities and countries which users can log using the API endpoints. However, I can't figure out how to prevent duplicate entries.
For example I'm adding 2 cities in a country using the endpoint (photo below) but in the Country table I get duplicate values. How can I prevent duplicate values ? Thanks in advance.
#Getter
#Setter
#Entity
#Table(name = "COUNTRY")
public class CntCountry {
#Id
#SequenceGenerator(name = "CntCountry", sequenceName = "CNT_COUNTRY_ID_SEQ")
#GeneratedValue(generator = "CntCountry")
private Long id;
#Column(name = "COUNTRY_NAME", length = 30, nullable = false)
private String countryName;
#Column(name = "COUNTRY_CODE", length = 30, nullable = false)
private String countryCode;
}
#Getter
#Setter
#Table(name = "CITY")
#Entity
public class CtyCity {
#Id
#SequenceGenerator(name = "CtyCity", sequenceName = "CTY_CITY_ID_SEQ")
#GeneratedValue(generator = "CtyCity")
private Long id;
#Column(name = "CITY_NAME", length = 30, nullable = false)
private String cityName;
#Column(name = "PLATE_NUMBER", length = 30, nullable = false)
private Long plateNumber;
#ManyToOne(fetch = FetchType.EAGER, cascade = CascadeType.ALL)
#JoinColumn(name = "FK_COUNTRY")
private CntCountry country;
}
EDIT:
#PostMapping("/city")
public ResponseEntity<CtyCityDto> save(#RequestBody CtyCitySaveRequestDto ctyCitySaveRequestDto){
CtyCityDto ctyCityDto = ctyCityService.save(ctyCitySaveRequestDto);
return ResponseEntity.ok(ctyCityDto);
}
#Service
#AllArgsConstructor
public class CtyCityService {
private CtyCityDao ctyCityDao;
public CtyCityDto save(CtyCitySaveRequestDto ctyCitySaveRequestDto){
CtyCity ctyCity = CtyCityMapper.INSTANCE.convertToCtyCity(ctyCitySaveRequestDto);
ctyCity = ctyCityDao.save(ctyCity);
CtyCityDto ctyCityDto = CtyCityMapper.INSTANCE.convertToCtyCityDto(ctyCity);
return ctyCityDto;
}
}
public interface CtyCityDao extends JpaRepository<CtyCity,Long> {
}
#Data
public class CtyCityDto {
private Long id;
private String cityName;
private Long plateNumber;
private CntCountry country;
}
I'm not really following your naming conventions, and I think your DTO classes are just complicating things for you at this point... But in general terms, because the entities you're sending have no id value associated with them, JPA assumes they are different objects and adds them to the database with new id's because it hasn't been told anywhere that similar items might in fact be the same object, it needs to be told.
I can think of 2 ways to prevent entity duplication in your database.
1. The easiest way would be to set your Country and City names (or other attributes) to be "unique", you can do this in your entity classes simply by adding unique = true to the column data on the item you wish to be unique.
//In Country.java
#Column(name = "COUNTRY_NAME", length = 30, nullable = false, unique = true)
private String countryName;
//In City.java
#Column(name = "CITY_NAME", length = 30, nullable = false, unique = true)
private String cityName;
Although, you will then need to handle exceptions thrown if a duplicate is provided, in Spring Boot the best way to handle this is with a #ControllerAdvice but that's another subject.
2. Check if the entity exists by name or some other value. A common approach might be something like the following:
//In your service
public Object saveCountry(Country country){
Country existingCountry = countryRepository.findByName(country.getName()).orElse(null);
if(existingCountry == null){
//Country does not already exist so save the new Country
return countryRepository.save(country);
}
//The Country was found by name, so don't add a duplicate
else return "A Country with that name already exists";
}
//In your Country repository
Optional<Country> findByName(countryName);
In case my answer doesn't make sense, I have thrown together an example following my first suggestion (using the unique column attribute and a controller advice) which you can view/clone from here
I'm using Spring Boot and MySQL. I tried to add new entity(songs) in playlist table. They have many to many relationship. But as you can see in answer after mysql query it doesn't saved.
Other relationships work correctly
PlaylistEntity
#Data
#Entity
#Component
#Getter
#EqualsAndHashCode(exclude = "songs")
#NoArgsConstructor
#AllArgsConstructor
#Table(name = "playlists")
public class PlaylistEntity {
#Id
#GeneratedValue(generator = "uuid")
#GenericGenerator(name = "uuid", strategy = "uuid2")
private String id;
private String playlistTitle;
#ManyToOne(fetch = FetchType.EAGER, cascade = {CascadeType.MERGE})
#JoinColumn(name = "user_id", nullable = false)
private UserEntity user;
private LocalDateTime createdAt;
public PlaylistEntity(String playlistTitle, UserEntity user, LocalDateTime createdAt) {
this.playlistTitle = playlistTitle;
this.user = user;
this.createdAt = createdAt;
}
#Transient
#ManyToMany(cascade = CascadeType.ALL, fetch = FetchType.EAGER)
#JoinTable(name = "playlist_song",
joinColumns = #JoinColumn(name = "playlist_id", nullable=false),
inverseJoinColumns = #JoinColumn(name = "song_id", nullable=false))
private Set<SongEntity> songs = new HashSet<>();
}
PlaylistRepository
#Repository
public interface PlaylistRepository extends PagingAndSortingRepository<PlaylistEntity, String> {
#Query(value = "select * from playlists where user_id = :id", nativeQuery = true)
List<PlaylistEntity> showAllUserPlaylists(#Param("id") String id);
#Query(value = "select * from playlists where playlist_title = :playlist_title", nativeQuery = true)
PlaylistEntity findByName(#Param("playlist_title") String playlist_title);
}
SongEntity
#Data
#Entity
#Builder
#Getter
#EqualsAndHashCode(onlyExplicitlyIncluded = true)
#NoArgsConstructor
#AllArgsConstructor
#Table(name = "songs")
public class SongEntity {
#Id
#GeneratedValue(generator = "uuid")
#GenericGenerator(name = "uuid", strategy = "uuid2")
private String id;
private String title;
private String artist;
private String album;
private String genre;
#CreationTimestamp
private LocalDateTime releaseDate;
private int likes;
#Transient
#EqualsAndHashCode.Exclude
#ManyToMany(mappedBy = "songs")
private Set<PlaylistEntity> playlistEntities = new HashSet<>();
#Transient
#ManyToMany(mappedBy = "songs")
#EqualsAndHashCode.Exclude
private Set<SubscriptionEntity> subscriptionEntities = new HashSet<>();
public SongEntity(String name) {
this.title = name;
}
}
SongRepository
#Repository
public interface SongRepository extends JpaRepository<SongEntity, String> {
#Query(value="SELECT * FROM songs WHERE (:genre is null or genre = :genre) " +
"AND (:artist IS NULL or artist = :artist)", nativeQuery=true)
List<SongEntity> findByParams(#Param("genre") String genre, #Param("artist") String artist);
#Query(value="SELECT * FROM songs WHERE artist = :artist", nativeQuery=true)
List<SongEntity> findByArtist(#Param("artist") String artist);
#Query(value="SELECT * FROM songs WHERE genre = :genre", nativeQuery=true)
List<SongEntity> findByGenre(#Param("genre") String genre);
#Query(value = "SELECT s.title, s.likes FROM SongEntity s WHERE s.artist = :artist")
List<SongEntity> showSongsStatistics(#Param("artist") String artist);
}
Method where I saved song in playlist table
#Transactional
public Playlist addSongToPlaylist(String playlistId, String songId) throws Exception {
SongEntity addedSong = findSongById(songId)
PlaylistEntity requiredPlaylist = findPlaylistById(playlistId);
requiredPlaylist.getSongs().add(addedSong);
PlaylistEntity updatedPlaylist = playlistRepository.save(requiredPlaylist);
return playlistConverter.fromEntity(updatedPlaylist);
}
And controller
#Slf4j
#Configuration
#RestController
#AllArgsConstructor
#RequestMapping("/user/playlists")
public class PlaylistController {
private final PlaylistService playlistService;
#PostMapping(value = ADD_SONG_TO_PLAYLIST_URL)
Playlist addSongToThePlaylist(#RequestParam String playlistId, #RequestParam String songId) throws Exception {
return playlistService.addSongToPlaylist(playlistId, songId);
}
#UtilityClass
public static class Links {
public static final String ADD_SONG_TO_PLAYLIST_URL = "/addSong";
}
}
I use Postman to make requests. And after request I take this answer, which shows that song was added to playlist.
https://i.stack.imgur.com/VdfbC.png
But as I said, if check playlist_song db, its nothing there. it means that my program doest correctly save many-to-many tables.
https://i.stack.imgur.com/9lgW9.png
And in logs there are any exceptions.
So I can understand what is wrong.
Hope somebody has an idea.
For SongEntity's #EqualsAndHashCode(onlyExplicitlyIncluded = true), you don't have any explicitly included annotations. You have a few listed to Exclude, but based on the docs at https://projectlombok.org/features/EqualsAndHashCode, it seems to require you explicitly mark them as Included when you use this flag.
I'm not 100% sure what Lombok does when you put this flag at the class level but then don't explicitly include anything, but it seems like an invalid state and may be messing up your Equals and Hashcode and consequently messing up ability to track the items in the HashSet bucket that represents the songs that you're adding to.
So I'd start by fixing this so your Equals/HashCode is correct. Which looks like you'd want to either remove the onlyExplicitlyIncluded=true settings altogether OR you actually add specific Includes.
Here is a thread that talks about using explicit Includes if you want to stick with that route:
How to use #EqualsAndHashCode With Include - Lombok
Also, why do you have #Transient annotations on your relationships? That annotation usually tells the EntityManager to ignore the content it's attached to. In your case, if you're adding something to the Set but marking the Set as #Transient, then intuitively it should not affect in the Database. Suggest you remove those annotations where you want changes to objects in the relationships/sets to actually be reflected in the database.
I am developing a Rest API which will receive a JSON. I need to save the JSON correctly in a Postgres DB.
Right now I have:
#Entity
public class Customer {
#Id
#GeneratedValue(strategy = AUTO)
private Long id;
private String name;
#OneToMany(mappedBy = "customer", cascade = ALL)
private List<Address> address;
}
And
#Entity
public class Address {
#Id
#GeneratedValue(strategy = AUTO)
private Long id;
private String city;
private String number;
private String country;
#ManyToOne
#JoinColumn(name = "customer_id", nullable = false)
private Customer customer;
}
My controller has only this:
#RequestMapping(method = POST)
public ResponseEntity create(#RequestBody Customer customer) {
Customer customerSaved = repository.save(customer);
return new ResponseEntity(customerSaved, CREATED);
}
When I send a JSON like:
{
"name":"A name",
"address":[
{
"country":"Brazil",
"city":"Porto Alegre",
"number":"000"
}
]
}
I was expecting that the table Customer would have the name and table Address would have the three properties plus the customer_id. But the customer_id right now is null.
Any thoughts?
Change the setter method of List address like below:
public void setAddress(Set<Address> address) {
for (Address child : address) {
// initializing the TestObj instance in Children class (Owner side)
// so that it is not a null and PK can be created
child.setCustomer(this);
}
this.address = address;
}
Hibernate - One to many relation - foreign key always "null"
I have two entities Employee and Review. I am trying to create a OneToOne relationship Employee <-> Review.
When I update an Employee with a review, the Employee gets updated where the review becomes the corresponding review,
but the Review doesn't get the 'reviewee' column added with the ID of the employee which is what I expect.
What am I doing wrong?
These are my entities:
public class Employee {
#Id
#GeneratedValue(strategy=GenerationType.AUTO)
private Integer id;
private String name;
private String email;
#OneToOne(cascade = CascadeType.ALL)
#JoinColumn(name = "reviewee")
private Review review;
}
public class Review {
#Id
#GeneratedValue(strategy=GenerationType.AUTO)
private Integer id;
private String body;
private char completed;
#OneToOne(mappedBy = "review")
private Employee reviewee;
}
This is my employeeController update function:
#GetMapping(path="/update")
public #ResponseBody Employee updateEmployee (#RequestParam Integer id,
#RequestParam(value = "name", required=false) String name,
#RequestParam(value = "email", required=false) String email,
#RequestParam() Integer reviewId) {
Employee n = EmployeeRepository.findOne(id);
if(name == null) {
name = n.getName();
}
if(email == null) {
email = n.getEmail();
}
n.setName(name);
n.setEmail(email);
Review r = ReviewRepository.findOne(reviewId);
n.setReview(r);
EmployeeRepository.save(n);
return n;
}
The request:
curl 'localhost:8080/employees/update?id=2&reviewId=1'
Because the owner of the relationship (the one with #JoinColumn) is Employee, you have to create/update/delete the association by saving the Employee object.
This is what you are doing so far. But Hibernate will only update the owner when you save it. You should in addition do this before returning your entity:
r.setReviewee(n);
Notice that the next time you will retrieve the review, it will correctly have an Employee object.
Beware: I smell a Jackson infinite loop there when serializing.
Employee.review -> Review -> Review.reviewee -> Employee -> Employee.review...
EDIT
To prevent the Jackson infinite loop:
1. Ignore the serialization.
Employee.java
public class Employee {
// ...
// Do not serialize this field
#JsonIgnore
#OneToOne(cascade = CascadeType.ALL)
#JoinColumn(name = "reviewee")
private Review review;
// ...
}
2. Serialize as ID.
Employee.java
public class Employee {
// ...
// Serialize as a single value with the field "id"
#JsonIdentityInfo(generator = ObjectIdGenerators.PropertyGenerator.class, property = "id")
// Serialize as told by #JsonIdentityInfo immediately (if false -> on second and further occurrences)
#JsonIdentityReference(alwaysAsId = true)
// Rename to "review_id" (would be "review" otherwise)
#JsonProperty(value = "review_id")
#OneToOne(cascade = CascadeType.ALL)
#JoinColumn(name = "reviewee")
private Review review;
// ...
}
3. Alternative to serialize as ID: read-only reference to the foreign key.
Employee.java
public class Employee {
// ...
// Do not serialize this field
#JsonIgnore
#OneToOne(cascade = CascadeType.ALL)
#JoinColumn(name = "reviewee")
private Review review;
// Read-only access to the foreign key
#Column(name = "Review_id", insertable = false, updatable = false)
private Integer reviewId;
// ...
}
It's seems to be a configuration mismatch. Please try the below one.
public class Employee {
#Id
#GeneratedValue(strategy=GenerationType.AUTO)
private Integer id;
private String name;
private String email;
#OneToOne(mappedBy="reviewee",cascade = CascadeType.ALL)
private Review review; }
public class Review {
#Id
#GeneratedValue(generator="gen")
#GenericGenerator(name="gen", strategy="foreign", parameters={#Parameter(name="property",value="reviewee")})
private Integer id;
private String body;
private char completed;
#OneToOne
#PrimaryKeJoinCloumn
private Employee reviewee; }
I hope the above configuration works as you expected.
Please make sure you're calling the save function under Transaction boundary. Otherwise don't forget to call flush() before closing the session.
I am learning Hibernate ORM(v. 3 ) now and I've a got a question.
I have a table called USERS, created with annotations :
package com.hibernatedb.entities;
import javax.persistence.*;
#Entity
#Table(name = "USERS",uniqueConstraints = {#UniqueConstraint(columnNames={"USER_LOGIN", "USER_EMAIL"})})
public class User {
#Column(name = "USER_LOGIN", length=80, nullable=false)
private String login;
#Column(name = "USER_PASS", length=80, nullable=false)
private String password;
#Column(name = "USER_EMAIL", length=80, nullable=false)
private String email;
#Id
#GeneratedValue(strategy=GenerationType.AUTO)
#Column(name = "USER_ID", nullable=false)
private Long id;
...
// some getters and setters, toString() and other stuff
...
}
And a Product entity :
#Entity
#Table(name = "PRODUCTS",uniqueConstraints = {#UniqueConstraint(columnNames={"PRODUCT_ID", "PRODUCT_NAME"})})
public class Product {
#Id
#GeneratedValue(strategy=GenerationType.AUTO)
#Column(name="PRODUCT_ID")
private long id;
#Column(name="PRODUCT_NAME", length=85, nullable=false)
private String name;
#Column(name="PRODUCT_DESCRIPTION", columnDefinition="mediumtext", length=1000)
private String description;
#Column(name="PRODUCT_COST", nullable=false)
private double cost;
So my question is : How can a create a table called like "USER +
User.getId()
BUYS", which contains a 2 foreign keys (USER_ID and PRODUCT_ID) for user in entity (table record) "User" without raw SQL table creation, but using Hibernate annotations or XML mapping.So i want to have something like
public class TransactionEntityBulider() {
public TransactionEntityBulder(User user)
// something that build me "USER + User.getId() BUYS" table and
}
public TransactionEntity getEntity() {
// something that return a "USER + User.getId() BUYS" table entity
}
Also i would like to see some another ways to solve my problem.
I think hibernate is not done for that kind of usage, because you would have to use dynamic mappings. Hibernate provide ways to specify mapping statically (xml and annotations).
I suggest you modify your approach. It normally should not be harmfull to have all the "USER_BUY" in the same table. Example :
#Entity
public class User {
...
#OneToMany(cascade=CascadeType.ALL, mappedBy="user")
List<UserBuys> buys = new ArrayList<UserBuys>();
...
}
#Entity
public class Product { ... }
#Entity
public class UserBuys {
...
#ManyToOne
Product product;
#ManyToOne
User user;
}