Hibernate many to many with an additional column without changing HQL - java

In the existing project we have a many-to-many relationship, it turned out that now we need to keep the time when that connection happed. The following approach has been used
https://vladmihalcea.com/the-best-way-to-map-a-many-to-many-association-with-extra-columns-when-using-jpa-and-hibernate/
Now we have these classes (the names are just taken from the link above):
#Embeddable
public class PostTagId
implements Serializable {
#Column(name = "post_id")
private Long postId;
#Column(name = "tag_id")
private Long tagId;
}
#Entity
#Table(name = "post_tag")
public class PostTag {
#EmbeddedId
private PostTagId id;
#ManyToOne(fetch = FetchType.LAZY)
#MapsId("postId")
private Post post;
#ManyToOne(fetch = FetchType.LAZY)
#MapsId("tagId")
private Tag tag;
#Column(name = "created_on")
private Date createdOn = new Date();
}
#Entity
public class Tag {
#Id
#GeneratedValue
private Long id;
private String name;
}
#Entity
public class Post {
#Id
#GeneratedValue
private Long id;
private String title;
#OneToMany(
mappedBy = "post",
cascade = CascadeType.ALL,
orphanRemoval = true
)
private List<PostTag> tags = new ArrayList<>();
}
Since it is a big project and there are tons of places to change, the Post class still has methods like getTags, setTags, removeTags, etc.
List<Tag> getTags() {
return tags.stream().map(PostTag::getTag).collect(collectingAndThen(Collectors.toSet(), Collections::unmodifiableSet))
}
void addTag(Tag tag) {
tags.add(new PostTag(this, tag, new Date())
}
It allows to hide from the outer world that collection is changed from <Tag> to <PostTag>. The problem happens with buzillion HQL queries that we have at the moment, since smth like this won't work:
...
JOIN post.tags t
WHERE t.name = <?>
Because "t" is now PostTag and not Tag, hence t.name is trying to access an unknown field
Are there any ways to mitigate that problem? Add some annotations above PostTag, so that instead of accessing tag.name hibernate will do t.tag.name, use other ways of mapping, etc.

Related

Jpa, linked class take main class id

I have a Product :
#Data
#Entity
#Table(name = "products", schema = "laboratory", catalog = "laboratory")
#JsonIgnoreProperties({ "hibernateLazyInitializer", "handler" })
public class Product {
#Id
#GeneratedValue
private int id;
#ManyToOne(fetch = FetchType.LAZY, cascade= CascadeType.ALL)
#JoinColumn(name = "project_id")
#Transient
private Project project; // this one is for read only
#Column(name="project_id") // this will save the id in db for the project
private int projectId;
#OneToOne(cascade = CascadeType.ALL)
#JoinColumn(name="id")
private Inspection inspection;
#OneToOne(cascade = CascadeType.ALL)
#JoinColumn(name="id")
private Information information;
#OneToOne(cascade = CascadeType.ALL)
#JoinColumn(name="id")
private Departure departure;
private String un;
private String tc;
}
There is 3 class that this product needs in order to be a Product : Information, Inpection, Departure
All 3 of these classes are similar.
I want to link them by the Product.id witch is a #GeneratedValue AI in sql.
Here is one of the 3 class :
Information
#Data
#Entity
#Table(name = "products_informations", schema = "laboratory", catalog = "laboratory")
#JsonIgnoreProperties({ "hibernateLazyInitializer", "handler" })
public class Information {
#Id
private int productId;
private String description;
private String model;
private int year;
private String serialNumber;
private int odometre;
private int noCrochet;
private int nbKeys;
private String localisation;
private String cemeteryPosition;
#JsonFormat(pattern = "yyyy-MM-dd")
private Date receptionDate;
}
I want, WHEN I save() the product, that the private String productId in this class to automatically take the Id from the Product class without having to do it manually in my controller.
You have the mappings backwards in your model.
By using
public class Product {
#Id
#GeneratedValue
private int id;
#OneToOne(cascade = CascadeType.ALL)
#JoinColumn(name="id")
private Information information;
You've told JPA to use the PRODUCT.ID primary key as a foreign key to the Information table; foreign keys are controlled by the relationship, so it means your ID value is pulled from the information.productId value. Opposite of what you are asking for and it means you have 4 mappings trying to set the PRODUCT.ID column value (set them different and see for yourself).
Try this instead:
public class Product {
#Id
#GeneratedValue
private int id;
#OneToOne(mappedby="product", cascade = CascadeType.ALL)
private Information information;
..
}
public class Information {
#Id
private int productId;
#MapsId
private Product product;
..
}
With this you will need to set the Information.product reference, but JPA will use that to set your productId value, using the one you set within the product.id property. You just need to set this relationship when you add an Information instance to a product. Do the same for the other relationships

Null values are inserted in the foreign key fields with Hibernate

I have a Question Entity and Tag entity with getter, setter methods and a OneToMany relationship from question to tag and a OneToOne relationship from question to user
public class Question {
#Id
#GeneratedValue(strategy=GenerationType.IDENTITY)
private Long id;
#Column(name="title")
private String title;
#Column(name="body")
private String body;
#Temporal(TemporalType.DATE)
#Column(name="date_created")
private Date date_created;
#OneToOne(cascade=CascadeType.ALL)
#JoinColumn(name="user_id")
private User user;
#OneToMany(cascade=CascadeType.ALL)
#JoinColumn(name="tag_id")
private Tag tag;
#Column(name="answer_count")
private int answer_count;
#Column(name="view_count")
private int view_count;
public Question() {
}
Tag entity
public class Tag {
#Id
#GeneratedValue(strategy=GenerationType.IDENTITY)
private Long id;
#Column(name="name")
private String name;
#Column(name="username")
private String username;
#Temporal(TemporalType.DATE)
#Column(name="date_created")
private Date date_created;
public Tag() {
}
When I try to insert a question using Postman with the following details:
{
"title": "stefanyyyxx",
"body": "stefandoyee44",
"date_created": "2019-02-27",
"user_id" : 1,
"tag_id": 1,
"answer_count": 0,
"view_count": 0
}
QuestionRepository.java:
#Override
public void save(Question theQuestion) {
// get the current hibernate session
Session currentSession = entityManager.unwrap(Session.class);
// save employee
currentSession.saveOrUpdate(theQuestion);
}
Null values are being inserted for user_id and tag_id though I used JoinColumn().
MySQL:
As #Karol Dowbecki Suggested,
convert the JSON to DTO object and use that DTO to get the User, Tag Entities from their respective repositories.
Finally create the Question entity object and store it.
Question Entity
#Entity
#Table(name = "question")
public class Question {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
#Column(name = "title")
private String title;
#Column(name = "body")
private String body;
#Temporal(TemporalType.DATE)
#Column(name = "date_created")
private Date dateCreated;
#OneToOne(cascade = CascadeType.ALL)
#JoinColumn(name = "user_id")
private User user;
#OneToMany(cascade = CascadeType.ALL)
#JoinColumn(name = "tag_id")
private Set<Tag> tag;
#Column(name = "answer_count")
private int answerCount;
#Column(name = "view_count")
private int viewCount;
}
User Entity
#Entity
#Table(name = "user")
public class User {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
}
Tag Entity
#Entity
#Table(name = "tag")
public class Tag {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
#Column(name = "name")
private String name;
#Column(name = "username")
private String username;
#Temporal(TemporalType.DATE)
#Column(name = "date_created")
private Date dateCreated;
}
DTO Class
public class QuestionDTO {
private Long id;
private String title;
private String body;
private Date dateCreated;
private Long user;
private Long tag;
private int answerCount;
private int viewCount;
}
Test Class
#Service
public class TestService {
#Autowired
private QuestionRepository questionRepository;
#Autowired
private UserRepository userRepository;
#Autowired
private TagRepository tagRepository;
public void addQuestion(QuestionDTO dto) {
Tag tag = null;
User user = null;
Question question = null;
Set<Tag> tags = null;
tag = tagRepository.findById(dto.getTag());
tags = new HashSet<>();
tags.add(tag);
user = userRepository.findById(dto.getUser());
question = new Question();
question.setTag(tags);
question.setUser(user);
question.setId(dto.getId());
question.setBody(dto.getBody());
question.setTitle(dto.getTitle());
question.setViewCount(dto.getViewCount());
question.setAnswerCount(dto.getAnswerCount());
question.setDateCreated(dto.getDateCreated());
questionRepository.save(question);
}
}
NOTE :
The relation between Question and Tag are in OneToMany you have to use Collection type.
You have a mismatch between JSON and #Entity structure. JSON contains numeric identifiers while the #Entity contains actual objects representing relationships. You most likely should introduce a separate DTO class to map this JSON while in #Repository you should load User and Tag objects based on their id or create new ones. You already have CascadeType.ALL so Hibernate will cascade the persist operation.
Generally the controller layer should be separate from repository layer unless you are doing something very, very simple. This helps to evolve the service without changing the API contract e.g. adding new columns for auditing changes. By exposing the #Entity as DTO you make your life harder down the road.
You should add referencedColumnName in your Child Entity Foreign Key Column
referencedColumnName="your primaray key column name"
EDIT:
referencedColumnName
The name of the column referenced by this foreign key column.
When used with entity relationship mappings other than the cases
described here, the referenced column is in the table of the target
entity.
When used with a unidirectional OneToMany foreign key mapping, the
referenced column is in the table of the source entity.
When used inside a JoinTable annotation, the referenced key column is
in the entity table of the owning entity, or inverse entity if the
join is part of the inverse join definition.
When used in a CollectionTable mapping, the referenced column is in
the table of the entity containing the collection.
Default (only applies if single join column is being used): The same
name as the primary key column of the referenced table.
Asset is Parent Entity and AssetDetails is Child Entity
Here I have taken OneToOne Relationship
Asset.java
#Entity
#Table(name="asset")
public class Asset {
#Id
#GeneratedValue
#Column(name="assetid")
private BigInteger assetid;
#OneToOne(fetch = FetchType.LAZY, cascade = CascadeType.ALL, mappedBy = "asset")
#JsonBackReference
private AssetDetails assetDetails;
public AssetDetails getAssetDetails() {
return assetDetails;
}
public void setAssetDetails(AssetDetails assetDetails) {
this.assetDetails = assetDetails;
assetDetails.setAsset(this);
}
public Asset(your fields, AssetDetails assetDetails) {
super();
// your fields
this.assetDetails = assetDetails;
this.assetDetails.setAsset(this);
}
public Asset() {
super();
}
public BigInteger getAssetid() {
return assetid;
}
public void setAssetid(BigInteger assetid) {
this.assetid = assetid;
}
}
AssetDetails.java
#Entity
#Table(name="assetDetails")
public class AssetDetails {
#Id
#GeneratedValue
private BigInteger assetdetailid;
#JoinColumn(name = "assetid",nullable = false, updatable = false,referencedColumnName="assetid")
#OneToOne(cascade=CascadeType.ALL)
#JsonManagedReference
private Asset asset;
public Asset getAsset() {
return asset;
}
public void setAsset(Asset asset) {
this.asset = asset;
}
public AssetDetails(your fields,Asset asset) {
super();
//your fields
this.asset = asset;
}
}

Hibernate Fetch is not longer sub-type of Join

I'm using Hibernate 5.2.10 with dynamic criteria and find out that Fetch is not longer assignable to Join.
In the next example, I need to fetch group data in the same query, but I also need to use the group's field for sorting or restriction.
CriteriaBuilder cb = session.getCriteriaBuilder();
CriteriaQuery<GroupAssignment> query = cb.createQuery(GroupAssignment.class);
Root<GroupAssignment> root = query.from(GroupAssignment.class);
SingularAttributeJoin<GroupAssignment, Group> groupFetch = (SingularAttributeJoin<GroupAssignment, Group>) root.fetch(GroupAssignment_.group, JoinType.LEFT);
query.orderBy(cb.asc(groupFetch.get(Group_.title)));
I have manually cast Fetch to the SingularAttributeJoin and after that, I can use get method for the ordering purpose, but I'm looking for the right way how I can do that without casting manually.
GroupAssignment class:
#Entity()
#Table(name = "group_assignment")
public class GroupAssignment {
#Id
#Column(name = "group_assignment_id")
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
#ManyToOne
#JoinColumn(name = "group_id", nullable = false)
private Group group;
//other fields, getters and setters
}
GroupAssignment_ class:
#Generated(value = "org.hibernate.jpamodelgen.JPAMetaModelEntityProcessor")
#StaticMetamodel(GroupAssignment.class)
public abstract class GroupAssignment_ {
public static volatile SingularAttribute<GroupAssignment, Integer> id;
public static volatile SingularAttribute<GroupAssignment, Group> group;
}
Group class:
#Entity()
#Table(name = "navigation_group")
public class Group {
#Id
#Column(name = "group_id")
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
#Column(name = "title")
private String title;
//other fields, getters and setters
}
Group_ class:
#Generated(value = "org.hibernate.jpamodelgen.JPAMetaModelEntityProcessor")
#StaticMetamodel(Group.class)
public abstract class Group_ {
public static volatile SingularAttribute<Group, Integer> id;
public static volatile SingularAttribute<Group, String> title;
}
I think that javax.persistence.criteria.FetchParent may solve your problem:
FetchParent<GroupAssignment, Group> groupFetch = root.fetch(GroupAssignment_.group, JoinType.LEFT);
It appears that correct way would be next:
query.orderBy(cb.asc(root.get(GroupAssignment_.group).get(Group_.title)));
So using multiple call to .get method you can go as deep as needed.

When having a bidirectional relationship, how to avoid a field only when accessing from one side of the relationship

I have two classes :
#Entity
#JsonIdentityInfo(generator = ObjectIdGenerators.PropertyGenerator.class,
property = "id")
public class Client {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
#Column(name = "client_id")
private Long id;
private char gender;
#OneToMany(mappedBy = "client", cascade = CascadeType.ALL)
private List<SurveyData> survey = new ArrayList<SurveyData>(); }
And SurveyData:
#Entity
#JsonIdentityInfo(generator = ObjectIdGenerators.PropertyGenerator.class,
property = "id")
public class SurveyData{
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
#Column(name = "SURVEY_ID")
private Long Id;
private double score;
#ManyToOne
#JoinColumn(name = "client_id")
public Client client; }
This way when I access SurveyData, I get Client model as a result too. And when I access Client, I get SurveyData too.
But my problem is this.
I have a query that returns SurveyData Based on a condition, so say I would have this as a JSON Result:
{id:1
client:
{name: Name,
lastName: last,
survey:[{
score: 10,
///
}]
How do I avoid getting the survey List as a result when accessing from SurveyData.
I cannot use JsonIgnore, because I have other methods where when I access Client model, I will need to be able to access survey model using the List.
Is there a way to do this ?
So, basically, only when calling SurveyData, I need to ignore the List survey in the Client class..else I need to use it.
Please have a look at #JsonView. In the Message class sample, it provides a nice example using #JsonView(View.Summary.class) and #JsonView(View.SummaryWithRecipients.class), which is exactly what you're looking for.
public class View {
interface Summary {}
interface SummaryWithRecipients extends Summary {}
}
public class Message {
#JsonView(View.Summary.class)
private Long id;
#JsonView(View.Summary.class)
private LocalDate created;
#JsonView(View.Summary.class)
private String title;
#JsonView(View.Summary.class)
private User author;
#JsonView(View.SummaryWithRecipients.class)
private List<User> recipients;
private String body;
}
Now, depending on the annotation, it will either include or exclude the list of recipipients.
// excludes list of recipients
#JsonView(View.Summary.class)
#GetMapping("/")
public List<Message> getAllMessages() {
return messageService.getAll();
}
// includes list of recipients
#JsonView(View.SummaryWithRecipients.class)
#GetMapping("/with-recipients")
public List<Message> getAllMessagesWithRecipients() {
return messageService.getAll();
}

Join two tables in one java object by jpa

I need to join 2 tables into one object with some condition. I have the following:
#Entity
#Table(name = "polling")
public class Polling extends DomainIdObject {
#ManyToOne
#JoinColumn(name = "owner_id")
private Person owner;
#Column(name = "poll_name")
private String name;
#Column(name = "description")
private String description;
#ManyToMany(targetEntity = PollingSchedule.class, mappedBy = "polling", fetch = FetchType.EAGER)
private List<PollingSchedule> variants;
#Column(name = "start_time")
private LocalDateTime startTime;
#Column(name = "end_time")
private LocalDateTime endTime;
//getters and setters
#Entity
#Table(name = "polling_schedule")
public class PollingSchedule extends DomainIdObject {
#JoinColumn(name = "polling_id")
private Polling polling;
#Column(name = "poll_var")
private String pollingVariant;
//gettters and setters
but when I execute the following code:
Query query = getEntityManager().createNativeQuery("SELECT * FROM polling p WHERE p.id=1", Polling.class);
List list = query.getResultList();
List<PollingSchedule> variants = ((Polling) list.get(0)).getVariants();
the variants list is empty. Tables in DB looks following:
polling
|id|owner_id|poll_name|description|start_time|end_time|
polling_schedule
|id|polling_id|poll_var|
So, in result I want that Polling object contains only those PollingVariants, that have corresponding polling_id in polling_schedule table.
I've try use Filter, SecondaryTable annotations, but it`s not work for me (or I was incorrect use it).
I use hibernate4 and spring boot 1.5.1
Could anyone help me?
I think the relation between Polling and PollingSchedule is one-to-many (not many-to-many). And since you need a bidirectional relationship between those objects, you should change them like this:
Pooling.java
#Entity
#Table(name = "polling")
public class Polling extends DomainIdObject {
...
#OneToMany(mappedBy="polling")
private List<PollingSchedule> variants;
...
}
PoolingSchedule.java
#Entity
#Table(name = "polling_schedule")
public class PollingSchedule extends DomainIdObject {
#ManyToOne
#JoinColumn(name = "polling_id")
private Polling polling;
...
}

Categories

Resources