How to use #NamedEntityGraph with #EmbeddedId? - spring

I'm trying to have Spring Data JPA issue one query using joins to eagerly get a graph of entities:
#Entity
#NamedEntityGraph(name = "PositionKey.all",
attributeNodes = {#NamedAttributeNode("positionKey.account"),
#NamedAttributeNode("positionKey.product")
})
#Data
public class Position {
#EmbeddedId
private PositionKey positionKey;
}
#Embeddable
#Data
public class PositionKey implements Serializable {
#ManyToOne
#JoinColumn(name = "accountId")
private Account account;
#ManyToOne
#JoinColumn(name = "productId")
private Product product;
}
Here's my Spring Data repo:
public interface PositionRepository extends JpaRepository<Position, PositionKey> {
#EntityGraph(value = "PositionKey.all", type = EntityGraphType.LOAD)
List<Position> findByPositionKeyAccountIn(Set<Account> accounts);
}
This produces the following exception:
java.lang.IllegalArgumentException: Unable to locate Attribute with the the given name [positionKey.account] on this ManagedType
I want all of the accounts and products to be retrieved in one join statement with the positions. How can I do this / reference the embedded ID properties?

I would suggest refactoring the entity this way if it possible
#Entity
#NamedEntityGraph(name = "PositionKey.all",
attributeNodes = {#NamedAttributeNode("account"),
#NamedAttributeNode("product")
})
#Data
public class Position {
#EmbeddedId
private PositionKey positionKey;
#MapsId("accountId")
#ManyToOne
#JoinColumn(name = "accountId")
private Account account;
#MapsId("productId")
#ManyToOne
#JoinColumn(name = "productId")
private Product product;
}
#Embeddable
#Data
public class PositionKey implements Serializable {
#Column(name = "accountId")
private Long accountId;
#Column(name = "productId")
private Long productId;
}
Such an EmbeddedId is much easier to use. For instance, when you are trying to get an entity by id, you do not need to create a complex key containing two entities.

Related

Spring JPA Unable To Find Composite Foreign Key Target Column (Non-PK)

User.java
#Entity
#Table(name = "users")
public class User implements Serializable {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "user_role_id", referencedColumnName = "id")
private UserRole userRole;
}
UserRole.java
#Data
#Entity
#Table(name = "user_roles")
public class UserRole implements Serializable {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
}
Client.java
#Data
#Entity
#Table(name = "clients")
public class Client implements Serializable {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
#OneToOne(cascade = CascadeType.ALL)
#JoinColumns({ #JoinColumn(name = "user_id", referencedColumnName = "id"),
#JoinColumn(name = "user_role_id", referencedColumnName = "user_role_id") })
private User user;
}
Error
org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'entityManagerFactory' defined in class path resource [org/springframework/boot/autoconfigure/orm/jpa/HibernateJpaConfiguration.class]: Invocation of init method failed; nested exception is org.hibernate.MappingException: Unable to find column with logical name: user_role_id in users
In RDBMS, users.(id, user_role_id) is unique so clients table can refer to that.
Last time, I was using insertable = false, updatable = false on user_role_id, but when I want to add records of new client, I always need to add user_role_id manually user.setUserRoleId(userRole.getId()) after user.setUserRole(userRole) and I think that is bad practice of ORM (it should be added automatically when I set user.setUserRole(userRole))
#Column(name = "user_role_id", insertable = false, updatable = false)
private Integer userRoleId;
What should I do so the relation can be mapped in Spring JPA? and what is the best practice?
In other words, this is also mean how to reference to foreign key generated logical name column?
OK! Please try following configuration:
Below is a important code part and under this link you may find repository with working example
UserRole.java
#Data
#Entity
#Table(name = "user_roles")
public class UserRole implements Serializable {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name = "role_id")
private Integer roleId;
}
User.java
#Data
#Entity
#Table(name = "users")
public class User implements Serializable {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name = "user_id")
private Integer userId;
#ManyToOne(fetch = FetchType.LAZY, cascade = CascadeType.ALL)
#JoinColumn(name = "user_role_id", referencedColumnName = "role_id")
private UserRole userRole;
}
Client.java
#Data
#Entity
#Table(name = "clients")
public class Client implements Serializable {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
#Column(name = "client_id")
private Integer clientId;
#OneToOne(fetch = FetchType.LAZY)
#JoinColumns(
value = {
#JoinColumn(name = "client_role_id", referencedColumnName = "user_role_id"),
#JoinColumn(name = "client_user_id", referencedColumnName = "user_id"),
}
,
foreignKey = #ForeignKey(
name = "FK_user_with_role",
foreignKeyDefinition = "FOREIGN KEY (client_user_id, client_role_id)\n" +
" REFERENCES users \n" +
" (user_id, user_role_id) \n" +
" ON UPDATE CASCADE\n" +
" ON DELETE CASCADE")
)
private User user;
}
Please note that beside adding a foreignKey in the Client implementation, you MUST keep the sequence of #JoinColum annotations.. I don't know what is the reason behind, but if you flip those lines you'll still get your error as it was before :)
EDIT: I've added another answer which fits best in my opinion. I'm leaving this one as well to see the other steps I tried.
Though the solution is not elegant and not using JPA as requested. Just in case anything in here would be helpful
If I understand the main issue correctly - you want to bind Client entity with Role entity via User entity, by first setting User's Role and then transfer that "property" by using only UserId instead setting additionally RoleId while creating Client.
Basically after playing for a while with your model I think the main issue is to assign data to each other within a #Transactional methods. That seems to be caused ba Lazy fetch strategy.
My proposal for solution that binds all your Entities according expectations differs only from yours with ommiting the RoleId JoinColumn in Clients table. I have checked that when calling a service that would have #Transactional methods, you can assign a Role to the User and User to the Client with simple user.setRole(roleEntity) followed by client.setUser(userEntity).
All the data is then consistent. No need to call further like getters and setters as you mentioned in the second part of your question. Question is if for any reason you need to have RoleId as well in your Clients Table, then this soultion would have to be enhanced by additional column?
UserRole.java
#Entity
#Table(name = "user_roles")
public class UserRole implements Serializable {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name = "role_id")
private Integer roleId;
//getters and setters and toString
}
User.java
#Entity
#Table(name = "users")
public class User implements Serializable {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name = "user_id")
private Integer userId;
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "user_role_id", referencedColumnName = "role_id")
private UserRole userRole;;
//getters and setters and toString;
}
Client.java
#Entity
#Table(name = "clients")
public class Client implements Serializable {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
#Column(name = "client_id")
private Integer clientId;
#OneToOne(fetch = FetchType.LAZY)
#JoinColumns({
#JoinColumn(name = "client_user_id", referencedColumnName = "user_id"),
})
private User user;
#Column(name = "client_role_id")
private Integer roleId;
#PrePersist
#PreUpdate
private void prePersist(){
try {
roleId = getUser().getUserRole().getRoleId();
} catch (NullPointerException e){
roleId = null;
}
}
//getters and setters and toString
}
UserService.java
#Service
public class UserService {
UserRepo userRepo;
public UserService(UserRepo userRepo) {
this.userRepo = userRepo;
}
#Transactional
public void save(User user) {
userRepo.save(user);
}
#Transactional
public User getReferenceById(int i) {
return userRepo.getReferenceById(i);
}
}
ClientService.java
#Service
public class ClientService {
private ClientRepo clientRepo;
private UserService userService;
public ClientService(ClientRepo clientRepo, UserService userService) {
this.clientRepo = clientRepo;
this.userService = userService;
}
#Transactional
public Client save(Client client){
return clientRepo.save(client);
}
#Transactional
public Client getReferenceById(int i) {
return clientRepo.getReferenceById(i);
}
#Transactional
public void printClient(Client client){
client = clientRepo.getReferenceById(client.getClientId());
System.out.println(client);
}
#Transactional
public void bindUserToClient(int userId, int clientId) {
Client entity = clientRepo.findById(clientId).orElseGet(Client::new);
entity.setUser(userService.getReferenceById(userId));
}
#Transactional
public void printClient(int i) {
clientRepo.findById(i).ifPresentOrElse(this::printClient, EntityNotFoundException::new);
}
}
This configuration after running this commandLineRunner:
#Configuration
public class Config {
#Bean
#Transactional
public CommandLineRunner commandLineRunner(
#Autowired UserRoleRepo roleRepo,
#Autowired UserService userService,
#Autowired ClientService clientService
) {
return args -> {
for (int i = 0; i < 5; i++) {
roleRepo.save(new UserRole());
}
for (int i = 5; i > 0; i--) {
User user = new User();
user.setUserRole(roleRepo.getReferenceById(i));
userService.save(user);
}
Client client = new Client();
client.setUser(userService.getReferenceById(2));
client = clientService.save(client);
clientService.printClient(client);
client = new Client();
client.setClientId(1);
clientService.printClient(client);
int userId = 5;
clientService.bindUserToClient(userId, 1);
clientService.printClient(1);
};
}
}
gave me correct output in the console:
Client{id=1, user=User{id=2, userRole=UserRole{id=4}}}
Client{id=1, user=User{id=2, userRole=UserRole{id=4}}}
Client{id=1, user=User{id=5, userRole=UserRole{id=1}}}
WORKAROUND
I tried to reach the goal by use of Spring JPA but could'nt.
The workaround that keeps the referential integrity was by creating a constrains through DB like below and add #PrePersist and #PreUpdate annotated method which is updating the client's roleId as intended.
create table clients
(
client_id integer not null,
client_user_id integer,
client_role_id integer,
primary key (client_id)
);
create table user_roles
(
role_id integer generated by default as identity,
primary key (role_id)
);
create table users
(
user_id integer generated by default as identity,
user_role_id integer,
primary key (user_id),
CONSTRAINT User_Role UNIQUE (user_id, user_role_id)
);
alter table users
add constraint FK_role_id foreign key (user_role_id) references user_roles (role_id);
alter table clients
add constraint FK_user_id foreign key (client_user_id, client_role_id) references users (user_id, user_role_id) on update cascade ;
Thanks to that I could for instance update userRole in user entity, and the change was reflected in the clients table as well without any further actions

#OnetoMany entity in #ElementCollection

I have 2 entities and 1 embeddable object :
#Entity
class CourseDetails extends Course {
#Id
Integer id;
#ElementCollection
#CollectionTable(name = "course_section", joinColumns = #JoinColumn(name = "courseId"), foreignKey = #ForeignKey(name = "course_section_fk"))
private List<CourseSection> courseSection;
}
#Embeddable
public class CourseSection extends BaseBo {
#OneToMany
#JoinColumn(name="contentId")
private Set<CourseContent> courseContent = new HashSet<>();
}
#Entity
public class CourseContent {
private static final long serialVersionUID = 1856738483334146418L;
#Id
private Integer contentId;
private String contentSummary;
}
I want to store coursesection as an embedded object of course and course_section should contain reference of course_content. I tried the above structure but it gives error :
#ElementCollection cannot be used inside an #Embeddable that is also contained within an #ElementCollection
How to achieve this in spring boot-jpa ?

Entity not mapped to a single property error with inherited entites of one table

I have two entities SuperAlbumEntity and AlbumEntity reflecting the same table "albums".
SuperAlbumEntity:
#Entity
#Table(name = "albums")
#Inheritance(strategy = InheritanceType.SINGLE_TABLE)
public class SuperAlbumEntity {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
private Integer id;
//other fields
}
AlbumEntity:
#EqualsAndHashCode(callSuper = true)
#Entity
#Table(name = "albums")
public class AlbumEntity extends SuperEntity{
//some fields
#Column(name = "country")
private String country;
#OneToMany(fetch = FetchType.EAGER)
#JoinColumn(name = "country_name", referencedColumnName = "country")
private Set<CountryEntity> countrySet = new HashSet<>();
}
AlbumEntity has #OneToMany mapping to CountryEntity:
#Entity
#Table(name = "countries")
public class CountryEntity implements Serializable {
#Id
String id;
String country_name;
//other fields
}
Running my application I get the folowing error:
...
Caused by: org.hibernate.AnnotationException: referencedColumnNames(country) of CountryEntity.countrySet referencing AlbumEntity not mapped to a single property
...
What's interesting is that if I move country field from SuperAlbumEntity to AlbumEntity everything just works fine...
Can someone explain me why I get this error?
I'm not sure but I think is connected with the type of inherence that you used it. Try to modify your superclass to something like this:
SuperAlbumEntity:
#MappedSuperclass
public abstract class SuperAlbumEntity {
}
AlbumEntity:
#Entity
#Inheritance(strategy=InheritanceType.JOINED)
#Table(name = "albums")
public class AlbumEntity extends SuperEntity {
#OneToMany(fetch = FetchType.EAGER)
#JoinColumn(name = "country_name", referencedColumnName = "country")
private Set<CountryEntity> countrySet = new HashSet<>();
}

Springboot add problem in oneTOMany relation

I'm writing 3 tables in the following relation:
Club class:
#Setter
#Getter
#Entity
#Table(name = "Club")
public class Club {
#Id
#GeneratedValue
private Long id;
private String name;
private String type;
private String mainPage;
private String logo;
#OneToMany(mappedBy="clubProductKey.club", cascade = CascadeType.ALL)
#JsonIgnoreProperties(value = "clubProductKey.club", allowSetters=true)
private Set<ClubProduct> clubProducts;
...
Product class:
#Setter
#Getter
#Entity
#Table(name = "Product")
public class Product {
#Id
#GeneratedValue
private Long id;
#OneToMany(mappedBy="clubProductKey.product", cascade = CascadeType.ALL)
#JsonIgnoreProperties(value = "clubProductKey.product", allowSetters=true)
private Set<ClubProduct> clubProducts;
...
ClubProduct class:
#Setter
#Getter
#Entity
#Table(name = "ClubProduct")
public class ClubProduct {
#EmbeddedId
private ClubProductKey clubProductKey;
...
ClubProductKey class:
#Setter
#Getter
#Embeddable
public class ClubProductKey implements Serializable {
#ManyToOne(cascade = {CascadeType.MERGE,CascadeType.REFRESH })
#JoinColumn(name = "club_id", referencedColumnName = "id")
#JsonIgnoreProperties(value = "clubProducts", allowSetters=true)
private Club club;
#ManyToOne(cascade = {CascadeType.MERGE,CascadeType.REFRESH })
#JoinColumn(name = "product_id", referencedColumnName = "id")
#JsonIgnoreProperties(value = "clubProducts", allowSetters=true)
private Product product;
...
ClubProductRepository class:
public interface ClubProductRepository extends JpaRepository<ClubProduct, ClubProductKey> {
public List<ClubProduct> findByClubProductKeyClub(Club club);
public List<ClubProduct> findByClubProductKeyProduct(Product product);
}
I try to save clubProduct like this:
#Service
public class ClubProductServiceImp implements ClubProductService {
#Autowired
private ClubProductRepository clubProductRepository;
...
ClubProduct savedClubProduct = clubProductRepository.save(clubProduct);
return savedClubProduct;
}
However I find that the clubProduct is not saved in the clubProducts list in the club or product entity, the list is null. Must I add lines like club.getClubProducts.add(clubProduct) or is there any other way to make it added automatically?
Thank you.
The #OnetoMany mapping in your Club class uses the attribute mappedby which means that it represents the owning side of the relation responsible for handling the mapping. However, we still need to have both sides in sync as otherwise, we break the Domain Model relationship consistency, and the entity state transitions are not guaranteed to work unless both sides are properly synchronized.
The answer is yes, you have to manage the java relations yourself so that the clubProducts gets persisted. You are using an instance of the repository class club to persist the data so , you should add a setter method like :
public void addClubProduct(ClubProduct clubProduct) {
if (clubProduct!= null) {
if (clubProduct== null) {
clubProduct= new ArrayList<ClubProduct>();
}
clubProducts.add(clubProduct);
clubProduct.setClubProduct(this);
}
}
also a method to remove it from the list and use these method in your code to set the values to the list properly before initiating save . Read related article

How to create JPA Specification for multiple tables?

I had entities User, BlockUnit, Block and Unit as Follows.
User has ManyToMany relation with blockunit.
#Entity
public class User {
#ManyToMany
private Set<BlockUnit> blockUnits;
}
#Entity
public class BlockUnit {
#Id
private Long id;
#OneToOne(targetEntity = Block.class)
#JoinColumn(name = "block_id")
private Block block;
#OneToOne(targetEntity = Unit.class)
#JoinColumn(name = "unit_id")
private Unit unit;
#ManyToMany(fetch = FetchType.LAZY)
private Set<User> users = new HashSet<>();
}
BlockUnit has OneToOne relation with Block and Unit.
#Entity
public class Block {
#Id
private Long id;
}
#Entity
public class Unit {
#Id
private Long id;
}
How should I create the JPA criteria specification for above in order to select user's having Block ID and Unit ID?
Thank you very much in advance!

Resources