Mapping object's related entities by other field than ID - java

I am using the following entities:
#Entity
#Table(name = "books")
public class Book {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
#ManyToOne
private User user;
private UUID uuid = UUID.randomUUID();
}
and
#Entity
#Table(name = "users")
public class User {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
private String email;
private String firstName;
private String lastName;
private String password;
#OneToMany(mappedBy = "user")
private List<Book> books;
private UUID uuid = UUID.randomUUID();
}
and the following DTO:
public class NewBookRequest {
private UUID userUuid;
}
and the converter:
#Component
public class NewBookRequestToEntityConverter implements Converter<NewBookRequest, Book> {
private final ModelMapper modelMapper;
public NewBookRequestToEntityConverter(final ModelMapper modelMapper) {
this.modelMapper = modelMapper;
}
#Override
public Book convert(#NotNull final NewBookRequest source) {
return modelMapper.map(source, Book.class);
}
}
and there is part of service code:
public void addBook(final NewBookRequest newBookRequest) {
final Book newBook = newBookRequestToEntityConverter.convert(newBookRequest);
bookRepository.save(newBook);
}
When I try to save the converted Book entity I get the ConstraintViolationException exception, as newBook.user.id is null. However, newBook.user.uuid is correctly assigned. Is there any way to automatically map newBook's user by its uuid? Or the only solution is to do something like this:
add new method to converter:
#Component
public class NewBookRequestToEntityConverter implements Converter<NewBookRequest, Book> {
private final ModelMapper modelMapper;
public NewBookRequestToEntityConverter(final ModelMapper modelMapper) {
this.modelMapper = modelMapper;
}
#Override
public Book convert(#NotNull final NewBookRequest source) {
return modelMapper.map(source, Book.class);
}
public Book convert(#NotNull final NewBookRequest source, final User user) {
Book target = modelMapper.map(source, Book.class);
target.setUser(user);
return target;
}
}
and modify service's code:
public void addBook(final NewBookRequest newBookRequest) {
final User user = userRepository.getUserByUuid(newBookRequest.getUserUuid());
final Book newBook = newBookRequestToEntityConverter.convert(newBookRequest, user);
bookRepository.save(newBook);
}
? Thanks for any help!

In Book or User entity you could use UUID as primary key. Did you try that?
#Id
#GeneratedValue(generator = "UUID")
#GenericGenerator(
name = "UUID",
strategy = "org.hibernate.id.UUIDGenerator",
)
#Column(name = "id", updatable = false, nullable = false)
private UUID id;
However on some databases there can be performance issue when using indexes based on UUID if there is high load.

I think problem is your converter does not initialize the primary key of User.
You may either change the type of id field in the User class due to answer https://stackoverflow.com/a/64141178/14225495, or add the annotation
#JoinColumn(name = "user_uuid", referencedColumnName = "uuid")
to the user field in the Book class. In this case you should also add column "user_uuid" in the books table, make sure the users.uuid column is unique and not nullable to create foreign key.

Related

Spring boot JPA: How to keep the child entity when deleting the parent entity in a many-to-one relationship?

I'm new to the Spring boot JPA and struggling to find out the relationships between multiple entities.
I have a User Entity, a Product Entity, and a Review Entity.
A user has many reviews.
A product has many reviews.
A review has a product and a user.
At the moment, I'm using one-to-many relationships for user&reivew, product&review. However, the error occurred when deleting a review: ERROR: update or delete on table "users" violates foreign key constraint "fkcgy7qjc1r99dp117y9en6lxye" on table "reviews".
My question:
How can I delete a Review Entity without deleting the Product entity and User entity?
Which cascade type should I use?
User Entity:
#Entity
#Table(name = "users")
public class User {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
#Column(name = "user_name")
private String userName;
#Column(name = "email")
private String email;
#Column(name = "password")
private String password;
#JsonManagedReference("reviews")
#JsonIgnore
#OneToMany(fetch = FetchType.LAZY,
mappedBy = "user")
private List<Review> reviews = new ArrayList<>();
//constructor + getter+ setter
Product Entity:
#Entity
#Table(name = "products")
public class Product {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private Float price;
#Transient
private Float rate;
private String category;
private String brand;
#JsonManagedReference("reviews")
#JsonIgnore
#OneToMany(mappedBy = "product")
List<Review> reviews = new ArrayList<>();
//constructor + getter+ setter
Review Entity:
#Entity
#Table(name = "reviews")
public class Review {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private Float rate;
private String comment;
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "user_id",referencedColumnName = "id")
#JsonBackReference("user")
private User user;
#ManyToOne(fetch = FetchType.LAZY)
#JsonBackReference("product")
#JoinColumn(name = "product_id",referencedColumnName = "id")
private Product product;
//constructor + getter+ setter
User Controller:
#CrossOrigin(origins = "http://localhost:3000")
#RestController
#RequestMapping(path="users/")
public class UserController {
private final UserService userService;
#Autowired
public UserController(UserService userService) {
this.userService = userService;
}
...
#DeleteMapping("{userid}")
public User deleteUser(#PathVariable("userid") Long userid){
return userService.deleteById(userid);
}
}
User service:
#Service
public class UserService {
private final UserRepository userRepository;
private final ReviewRepository reviewRepository;
//dependency injection
#Autowired
public UserService(UserRepository userRepository, ReviewRepository reviewRepository) {
this.userRepository = userRepository;
this.reviewRepository =reviewRepository;
}
...
public User getUserById(Long id){
return userRepository.findById(id).orElseThrow(()->
new UserNotFoundException(id));
}
public User deleteById(Long id){
User user = getUserById(id);
userRepository.delete(user);
return user;
}
}
Simple run:
#SpringBootApplication
public class GroceryShoppingAppApplication {
public static void main(String[] args) {
ConfigurableApplicationContext configurableApplicationContext =
SpringApplication.run(GroceryShoppingAppApplication.class, args);
UserRepository userRepository = configurableApplicationContext.getBean(UserRepository.class);
ProductRepository productRepository =configurableApplicationContext.getBean(ProductRepository.class);
ReviewRepository reviewRepository = configurableApplicationContext.getBean(ReviewRepository.class);
User debbi= new User("Debbi","debbi#gamil.com","password");
Product apple = new Product("Apple",(float)3.40,"Fruit","Gala");
Product milk = new Product("Milk",(float)5.22,"Dairy","Anchor");
Review review1 = new Review(debbi,(float)4.5,"Good taste",apple);
Review review2 = new Review(debbi,(float)5.0,"Good milk",milk);
productRepository.save(apple);
productRepository.save(milk);
userRepository.save(debbi);
reviewRepository.save(review1);
reviewRepository.save(review2);
I think I should not use casacadeType.All because when deleting a user, I shouldn't delete the product in the review. I tried other types, the error still remains. Thus, currently I didn't use any casacadeType and need to save each entity one by one.
Please help me with this.
You are getting an error because the user in the review model does not have a referenced Column value.
Try this code:
#JoinColumn(name = "user_id",referencedColumnName = "id")

How to get object from OneToMany collection of objects?

I have an Order entity and OrderProduct. I want to show order details on frontend and of course order products in it. So how to fetch product object in OrderProduct JSON. I'm missing product object in products array. I don't need order object one more time and i think it going to be a infinite recursion stuff with it. :)
My Order entity:
#Entity
#Getter
#Setter
#Table(name ="orders")
public class Order{
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
public Long id;
private BigDecimal totalPrice;
#OneToMany(mappedBy = "order", fetch = FetchType.EAGER, cascade = CascadeType.ALL)
#JsonManagedReference(value="orders")
private List<OrderProduct> products = new ArrayList<>();
private int userId;
#DateTimeFormat(pattern="dd/MM/yyyy")
private Date date = new Date();
#DateTimeFormat(pattern="dd/MM/yyyy")
private Date deliveryDate;
#Enumerated(EnumType.STRING)
private OrderType orderType;
}
My OrderProduct entity:
#Entity
#Setter
#Getter
public class OrderProduct {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
#ManyToOne(fetch = FetchType.EAGER)
#JsonBackReference(value="product")
#JoinColumn(name = "product_id")
private Product product;
#ManyToOne
#JsonBackReference(value="orders")
#JoinColumn(name = "order_id")
private Order order;
private Integer quantity;
}
Product entity:
#Entity
#Getter
#Setter
public class Product {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
#Column(unique = true)
private String name;
private double price;
#OneToMany(mappedBy = "product", cascade = CascadeType.ALL)
#JsonManagedReference(value="ingredients")
private List<Ingredient> ingredients = new ArrayList<>();
#OneToMany(mappedBy = "product",fetch = FetchType.EAGER)
#JsonManagedReference(value="product")
private List<OrderProduct> products = new ArrayList<>();
private String fileName;
}
This can help annotate one of your entity clases with
#JsonIdentityInfo(
property = "id",
generator = ObjectIdGenerators.PropertyGenerator.class
)
Every time when JSON serialization go in circles object data will be replaced with object id or orher field of entity for your choose.
You can use #JsonViewannotation to define the fields that you need to serialize to JSON
How it works:
You need define class with interfaces. For example:
public class SomeView {
public interface id {}
public interface CoreData extends id {}
public interface FullData extends CoreData {}
}
Mark entity fields with #JsonView(<some interface.class>)
public class User {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
#JsonView(SomeView.id.class)
private Long id;
#Column(nullable = false)
#JsonView(SomeView.CoreData.class)
private String username;
#Column(nullable = false)
#JsonView(SomeView.FullData.class)
private String email;
}
Annotate endpoint with #JsonView(<some interface.class>)
#GetMapping()
#JsonView(<some interface.class>)
public User getUser() {
return <get user entity somwhere>
}
In case #JsonView(SomeView.id.class) you will get this JSON:
{
id: <some id>
}
In case #JsonView(SomeView.CoreData.class):
{
id: <some id>,
username: <some username>
}
In case #JsonView(SomeView.FullData.class):
{
id: <some id>,
username: <some username>,
email: <some email>
}
#JsonView also works with embeded objects and you can annotate one field with multiply views classes - #JsonView({SomeView.FullData.class, SomeOtherView.OtherData.class})
In your case i think you should annotate all the fields you need except:
#OneToMany(mappedBy = "product",fetch = FetchType.EAGER)
#JsonManagedReference(value="product")
private List<OrderProduct> products = new ArrayList<>();
in Product
to avoid circular serialization
Or as alternative you can just use DTO classes or seralize oject to JSON manualy (https://thepracticaldeveloper.com/java-and-json-jackson-serialization-with-objectmapper/)
This can be done by my library beanknife
// This configure generate a class named ProductInfo which has the same shape with Product without property "products"
#ViewOf(value = Product.class, genName="ProductInfo", includePattern = ".*", excludes = {"products"})
class ProductInfoConfigure {}
// This configure generate a class named OrderProductRelation with the same shape of OrderProduct.
// But it has not order property and the type of its product property is change to ProductInfo generated above.
#ViewOf(value = OrderProduct.class, genName="OrderProductRelation", includePattern = ".*", excludes = {"order"})
class OrderProductRelationConfigure {
#OverrideViewProperty("product")
private ProductInfo product;
}
// This configure generate a class named OrderDetail with the same shape of Order.
// But the type of its products property is change to List<OrderProductRelation>
#ViewOf(value = Order.class, genName="OrderDetail", includePattern = ".*")
class OrderDetailConfigure {
#OverrideViewProperty("products")
private List<OrderProductRelation> products;
}
will generate these classes:
class ProductInfo {
private Long id;
private String name;
private double price;
private List<Ingredient> ingredients; // it is not processed because you have not provide the class Ingredient
private String fileName;
}
public class OrderProductRelation {
private Long id;
private ProductInfo product;
private Integer quantity;
}
public class OrderDetail {
public Long id;
private BigDecimal totalPrice;
private List<OrderProductRelation> products;
private int userId;
private Date date = new Date();
private Date deliveryDate;
private OrderType orderType;
}
Then
Order order = ...
OrderDetail orderDetail = OrderDetail.read(order);
// serialize the otherDetail instead of order.
List<Order> orders = ...
List<OrderDetail> orderDetails = OrderDetail.read(orders);
// serialize the orderDetails instead of orders.
Possible problems:
I doesn't use Lombok, so Lombok may need to be adapted because it change the byte code on the fly. But it is not a big problem, I will try to adapt it if someone commit the issue and provide enough use cases.
The generated class does not inherit the annotation on the original class. In next release I will provide a sulotion. At this moment, as a workaround, we can use custom method to convert the property manually. such as
#ViewOf(value = Order.class, genName="OrderDetail", includePattern = ".*")
class OrderDetailConfigure {
#OverrideViewProperty("products")
private List<OrderProductRelation> products;
#OverrideViewProperty("orderType")
public static String orderType(Order source) {
return source.getOrder().name();
}
}
The generated class will be changed to
public class OrderDetail {
public Long id;
private BigDecimal totalPrice;
private List<OrderProductRelation> products;
private int userId;
private Date date = new Date();
private Date deliveryDate;
private String orderType;
}
Update
Version 1.2.0 released. Add support of annotation inheritance.
#ViewOf(value = Order.class, genName="OrderDetail", includePattern = ".*")
#UseAnnotation({DateTimeFormat.class, Enumerated.class, JsonProperty.class})
class OrderDetailConfigure {
#OverrideViewProperty("products")
private List<OrderProductRelation> products;
}
generate
public class OrderDetail {
public Long id;
private BigDecimal totalPrice;
private List<OrderProductRelation> products;
private int userId;
#DateTimeFormat(pattern="dd/MM/yyyy")
private Date date;
#DateTimeFormat(pattern="dd/MM/yyyy")
private Date deliveryDate;
#Enumerated(EnumType.STRING)
private OrderType orderType;
}

How to save two related objects to data base with relation OneToOne

When I'm trying to save an U object I got next exception:
org.springframework.orm.jpa.JpaSystemException: attempted to assign id from null one-to-one property [com.roc.domain.A.user]; nested exception is org.hibernate.id.IdentifierGenerationException: attempted to assign id from null one-to-one property [com.roc.domain.A.user]
I have two tables:
1. user that columns are id(auto incr, primary), name.
2. contact that columns are id, user_id(that is foreign key -> user.id) and address.
#Entity
#Table(name = "a")
public class A {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name = "id")
private Long id;
#Column(name="address")
private String address;
#OneToOne
#MapsId
private U user;
public A() {
}
// getters and setters
}
#Entity
#Table(name = "u")
public class U {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name = "id")
private Long id;
#Column(name="username")
private String userName;
#JoinColumn(name = "user_id", referencedColumnName = "id")
#OneToOne(mappedBy = "user", cascade = CascadeType.ALL)
private A a;
public U(){};
}
#RunWith(SpringRunner.class)
#SpringBootTest
public class ApplicationTest {
#Autowired
private URepository uRepository;
#Test
public void simpleCrudTest() {
U user = new U("name", new A("address"));
uRepository.save(user);
}
}
You have set the cascade correctly however because the relationship is bi-directional you need to set both sides in the in-memory model.
#Test
public void simpleCrudTest() {
U user = new U("name", new A("address"));
//will work when this is added
a.setUser(user);
uRepository.save(user);
}
Otherwise, as the error states, A has a null reference for user on save.
Edit: To save using a single repository save call.
#Entity
#Table(name = "a")
public class A {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name = "id")
private Long id;
#Column(name = "address")
private String address;
#OneToOne
#MapsId
private U user;
public A() {
}
}
#Entity
#Table(name = "u")
public class U {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name = "id")
private Long id;
#Column(name = "username")
private String userName;
#JoinColumn(name = "user_id", referencedColumnName = "id")
#OneToOne(mappedBy = "user", cascade = CascadeType.ALL)
private A a;
public U() {
};
// method to manage the bidirectional association
public U addToA(A a) {
this.a.add(a);
a.setUser(this);
}
}
#RunWith(SpringRunner.class)
#SpringBootTest
public class ApplicationTest {
#Autowired
private URepository uRepository;
#Test
public void simpleCrudTest() {
U user = new U();
user.addToA(new A("address"));
user.setUserName("username");
uRepository.save(user);
}
}
Also, you refer to this link.
inserting values into multiple tables using hibernate
You have to save A first, Then set saved A to U and save U.
#RunWith(SpringRunner.class)
#SpringBootTest
public class ApplicationTest {
#Autowired
private URepository uRepository;
#Autowired
private ARepository aRepository;
#Test
#Trascational
public void simpleCrudTest() {
A a = new A();
a.setAddress("address");
a = aRepository.save(a);
U user = new U("name", a);
uRepository.save(user);
}
}

JPA Criteria seems to ignore join on condition

I've a User and Contact entities in my app and I need to every user can add some private comment about every contact and that comment must be available only for that user. So I create new entity - PrivateInfo. Here's the code.
User class:
#Entity
#Table(name = "users")
#XmlAccessorType(XmlAccessType.FIELD)
public class User implements Serializable {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private long id;
private String login;
// other fields
}
Contact class:
#Entity
#Table(name = "contacts")
#XmlAccessorType(XmlAccessType.FIELD)
public class Contact implements Serializable {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private long id;
#Column(name = "first_name")
private String firstName;
#Column(name = "last_name")
private String lastName;
#OneToMany(fetch = LAZY, cascade = ALL, mappedBy = "contact")
private Set<PrivateInfo> privateInfo;
// etc.
}
PrivateInfo class:
#Entity
#Table(name = "private_info")
#XmlAccessorType(XmlAccessType.FIELD)
public class PrivateInfo implements Serializable {
#EmbeddedId
private PrivateInfoKey pk;
#Column(name = "additional_info")
private String additionalInfo;
#ManyToOne(fetch = FetchType.EAGER)
#MapsId("contactId")
private Contact contact;
}
#Embeddable
public class PrivateInfoKey implements Serializable {
#Column(name = "contact_id")
private Long contactId;
#Column(name = "user_id")
private Long userId;
}
I'm using Spring Data repositories with JpaSpecificationExecutor for querying so here's my attempt to write specification for getting all contacts with private info for specific user.
public static Specification<Contact> withPrivateInfo(final long userId) {
return new Specification<Contact>() {
#Override
public Predicate toPredicate(Root<Contact> root, CriteriaQuery<?> query, CriteriaBuilder cb) {
Join<Contact, PrivateInfo> joinPrivateInfo = root.join(Contact_.privateInfo, JoinType.LEFT);
joinPrivateInfo.on(cb.equal(
joinPrivateInfo.get(PrivateInfo_.pk).get(PrivateInfoKey_.userId), userId
));
return cb.conjunction(); // translates in sql like '... where 1 = 1'
}
};
}
However, when I call
contactRepository.findAll(withPrivateInfo(1));
I'm receiving contacts and each of them contains in privateInfo field all users information about this contact (not only for user with id = 1, as expected). Seems like join on condition ignored.
Any suggestions how to achieve my goal? Maybe with another entities structure. Is this possible with JPA/Criteria?

MVC SpringBoot and JPA Relationship

I have an issue with the JPA relationship within a MVC SpringBoot application.
I'm trying to create an application where I have a number of users and every user can have a number of cars. A user can have multiple cars. I've done a #OneToOne relationship between the user and the car object, here's what I've done:
here is the User class:
#Entity
#Table(name = "user")
public class User implements Serializable {
private static final long serialVersionUID = 1L;
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
#Column(name = "username", nullable = false)
private String username;
#Column(name = "password", length = 500, nullable = false)
private String password;
#OneToMany(mappedBy = "user", cascade = CascadeType.ALL)
private List<Car> cars;
}
then here is the Car class:
#Entity
#Table(name = "car")
public class Car implements Serializable {
private static final long serialVersionUID = 1L;
#Id
#Column(length = 11)
#GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
#Column(name = "make", nullable = false)
private String make;
#Column(name = "model", nullable = false)
private String model;
#ManyToOne(fetch = FetchType.LAZY, cascade = CascadeType.ALL)
#JoinColumn(name = "id")
private User user;
}
Then, here is the actual service implementation
#Component
#Transactional(readOnly = true)
public class CarServiceImpl implements CarService {
#Inject
private CarRepository carRepository;
#Inject
private UserRepository userRepository;
#Override
#Transactional(readOnly = false)
public Car addCar(Long userId, Car car) {
User user = userRepository.findOne(userId);
user.getGpsLocationModels().add(car);
car.setUser(user);
carRepository.save(car);
return car;
}
then I have the endpoint but that works fully. The add method looks like does work as supposed to, at least I get the expected output, however the find method I have no idea how to write it, well can't figure it out how to retrieve cars based on user, I know how to get them by their ID but not for every user separately.
Here is my try:
#Override
public Car findCar(Long userId, Long carId) {
//get the current user (that comes as JSON Request Param)
User user = userRepository.findOne(userId);
//get the car based on its ID, here's the problem, I want the car based on its ID AND its user, I can't display cars which aren't for that particular user
Car car = carRepository.findOne(carId);
return car;
}
Here is the method for get all cars for a particular user:
#Override
public List<Car> displayAllCars(Long userId) {
return userRepository.findOne(userId).getCars();
}
I'd really appreciate any help that you could advise.
Thank you so much
Your mappings are incorrect. Car > User is #ManyToOne. If you also make this bi-directional you can also then retrieve the cars via the user:
#Entity
#Table(name = "user")
public class User implements Serializable {
#OneToMany(mappedBy ="user",cascade = CascadeType.ALL)
private Set<Car> cars;
public Set<Car> getCars(){
return cars;
}
public void addCar(Car car){
cars.add(car);
car.setUser(this);
}
}
#Entity
#Table(name = "car")
public class Car implements Serializable {
#ManyToOne(fetch=FetchType.LAZY, cascade = CascadeType.ALL)
#JoinColumn(name="user_id")
private User user;
}
#Override
public Set<Car> findCars(Long userId) {
return userRepository.findOne(userId).getCars();
}
You could have a method that accepts the user ID and returns the list in the car repository like:
List<Car> findCarByUser(Long userID);
And then you will have
#Override
public List<Car> displayAllCars(Long userId) {
return carRepository.findCarByUser(userId);
}

Categories