I am trying to get familiar with spring and jpa. For start there is a table anime_details containing details of an anime. As it can have many genres, db has another table named genre. The intermediate table to contain their many to many relationship entries is also there. When I query for any anime by id, it should return the details of the anime along with the genres.
It does return an object with details of the anime and list of Genre objects (which is as expected). But what I want is to restrict the columns that will be fetched from Genre objects. For example only id or just id and name (In case there are more columns other than these).
AnimeDetails
#Getter
#Setter
#Entity
#Table(name = "anime_details")
public class AnimeDetails {
#Id
#SequenceGenerator(name = "animeDetailsSeq", sequenceName =
"anime_details_id_seq",
allocationSize = 1)
#GeneratedValue(strategy = GenerationType.SEQUENCE, generator =
"animeDetailsSeq")
private Integer id;
private String name;
private Integer episodes;
private Date started;
private Date ended;
private String status;
#ManyToMany
#JoinTable(
name = "anime_genre",
joinColumns = #JoinColumn(name = "details_id", referencedColumnName = "id"),
inverseJoinColumns = #JoinColumn(name = "genre_id", referencedColumnName = "id"))
#JsonManagedReference
private List<Genre> genres;
protected AnimeDetails() {
}
}
Genre
#Data
#Entity
#Table(name = "genre")
public class Genre {
#Id
#SequenceGenerator(name = "genreSeq", sequenceName = "genre_id_seq",
allocationSize = 1)
#GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "genreSeq")
private Integer id;
private String name;
#ManyToMany(mappedBy = "genres")
List<AnimeDetails> animes;
protected Genre() {
}
}
Expected payload
{
"id": 2,
"name": "Your Name",
"episodes": 1,
"started": "2016-08-25T18:00:00.000+0000",
"ended": "2016-08-25T18:00:00.000+0000",
"status": "Completed",
"genres": [
{
"id": 5,
"name": "Drama"
},
{
"id": 10,
"name": "Supernatural"
}
]
}
Right now, I get the result and manually get columns one by one and set those in a DTO. But that is not efficient as the database query is already fetching more data than needed.
Is there any specific annotation/property/jpql to reduce it?
Indeed was looking for a proper solution regarding the same issue , cause as you pointed out it is creating performance issues as there are huge useless data loads between the APP and the DB. Imagine that there could be, not only one query but much more and you need a global optimization solution...
From the first place Spring DATA is not supporting this operation so its leading you at the manual configuration and set up on a DTO reference. The same applies if you were using a custom Object and returning that inside the JPQL with the constructor trick , or else write a native query , get back a List<Object> and again manually map the data back to your actual object , which is the most efficient but not elegant solution
More info in this link , but try to check both answers for the details.
The other thing is that as you are using hibernate underneath , which is providing custom mappers , you could always write up your custom HQL(not jpql) , set up a proper DAO , wire up the EntityManager or directly the SessionFactory (which is breaking the abstract JPA contract , but you can utilize the full goodies that hibernates offers) and then return the same object, but only with the columns you need.
Example for the second point:
import javax.persistence.EntityManager;
import org.hibernate.query.Query;
import org.hibernate.transform.Transformers;
public CustomEntity getEntity(){
Query<CustomEntity> q = (Query<CustomEntity>) entityManager.createQuery("select
e.id,e.name from CustomEntity e where e.name = 'parameter'");
q.setResultTransformer(Transformers.aliasToBean(CustomEntity.class));
CustomEntity entity = (CustomEntity) q.getSingleResult();
return name;
}
Note CustomEntity is a managed Entity Bean / Table in the database , just placing this example to be close on what you might need to achieve.
Tried with
Spring boot 2.0.5.RELEASE
Spring Data 2.0.5.RELEASE
Hibernate-core 5.2.17.Final
I tried a different solution today. Lets look at the code first.
Genre
#Data
#Entity
#Table(name = "genre")
public class Genre {
#Id
#SequenceGenerator(name = "genreSeq", sequenceName = "genre_id_seq",
allocationSize = 1)
#GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "genreSeq")
private Integer id;
private String name;
#ManyToMany
#JoinTable(
name = "anime_genre",
joinColumns = #JoinColumn(name = "genre_id", referencedColumnName = "id"),
inverseJoinColumns = #JoinColumn(name = "details_id", referencedColumnName = "id"))
List<AnimeIdentity> animes;
protected Genre() {
}
}
AnimeIdentity
#Getter
#Setter
#Entity
#Table(name = "anime_details")
public class AnimeIdentity {
#Id
#SequenceGenerator(name = "animeDetailsSeq", sequenceName =
"anime_details_id_seq",
allocationSize = 1)
#GeneratedValue(strategy = GenerationType.SEQUENCE, generator =
"animeDetailsSeq")
private Integer id;
private String name;
protected AnimeIdentity() {};
}
Queries Hibernate made
Hibernate: select genre0_.id as id1_2_0_, genre0_.name as name2_2_0_ from genre genre0_ where genre0_.id=?<br>
Hibernate: select animes0_.genre_id as genre_id2_1_0_, animes0_.details_id as details_1_1_0_, animeident1_.id as id1_0_1_, animeident1_.name as name2_0_1_ from anime_genre animes0_ inner join anime_details animeident1_ on animes0_.details_id=animeident1_.id where animes0_.genre_id=?
Feel free to show me the pros and cons of this solution. To me its a good solution if my necessity is limited to only this. But in case of different type of queries making more and more entity pojos will be a tiresome task.
Related
I have 3 sql tables:
Account (ID (BIGINT),...)
Card (ID (BIGINT),...)
RelationAccountCard (ACCOUNT (BIGINT), CARD(BIGINT), QUANTITY(int))
One account can have multiple cards, one card can have multiple account
#Data
#Entity
public class Account {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
private long id;
#ManyToMany
#JoinTable(
name = "RELATION_ACCOUNT_CARD",
joinColumns = #JoinColumn(name = "ACCOUNT"),
inverseJoinColumns = #JoinColumn(name = "CARD"))
private Set<Card> cardsOwned = new HashSet<>();
}
#Data
#Entity
public class Card {
#Id
private long id;
#JsonIgnore
#ManyToMany(mappedBy = "cardsOwned")
private java.util.Set<Account> accounts;
}
This code above is working if the field "QUANTITY" in relationAccountCard didn't exist. I would like to make the code works knowing that I added the "QUANTITY" field. I tried for hours yesterday without success, do you have an idea of how to do this?
Basically I need to replace private Set<Card> cardsOwned; by private Set<RelationAccountCard> relationAccountCards; but I don't know how to
The solution was to delete the composite primary key. To make (card,account) unique, and to add an ID.
Once it was done, I simply followed the answer of this stackoverflow post:
Mapping many-to-many association table with extra column(s)
Say I have at least two entities.
#Entity
public class Process {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
#Column(unique = true)
private String name;
#ManyToAny(
metaColumn = #Column(name = "node_type"),
fetch = FetchType.LAZY
)
#AnyMetaDef(
idType = "long", metaType = "string",
metaValues = {
#MetaValue(targetEntity = Milestone.class, value = MILESTONE_DISC),
#MetaValue(targetEntity = Phase.class, value = PHASE_DISC)
}
)
#Cascade({org.hibernate.annotations.CascadeType.ALL})
#JoinTable(
name = "process_nodes",
joinColumns = #JoinColumn(name = "process_id", nullable = false),
inverseJoinColumns = #JoinColumn(name = "node_id", nullable = false)
)
private Collection<ProcessNode> nodes = new ArrayList<>();
...
}
#Entity
#ToString
#DiscriminatorValue(MILESTONE_DISC)
public class Milestone implements ProcessNode {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
#OneToMany(cascade = CascadeType.ALL, fetch = FetchType.LAZY)
private Collection<ResultDefinition> results;
#ToString.Exclude
#ManyToOne(fetch = FetchType.LAZY)
#Transient
private Process process;
...
}
Now I want to use spring data jpa specification to find (all) processes which have a milestone with name "S5".
Note that Milestone is a ProcessNode and there is another Entity called Phase which is also a ProcessNode. These can be contained in the "nodes" collection of my Process Entity.
I tried to write something like this:
public static Specification<Process> hasMilestoneWithName(final String milestoneName) {
return (Specification<Process>) (root, query, criteriaBuilder) -> {
Path<?> namePath = root.join("nodes").get("name");
return criteriaBuilder.equal(namePath, milestoneName);
};
}
This does not work, but throws:
Caused by: java.lang.IllegalArgumentException: Unable to locate Attribute with the the given name [nodes] on this ManagedType [com.smatrics.dffs.processservice.model.entities.Process]
I don't really know how to use the API. Examples often refer to a meta-model that would be generated by the IDE or maven, but I really do not want to have any static generated resources. Please help me resolve this with Specification of spring-data-jpa without a generated meta-model.
Also if you could help me write the hql it would be awesome.
Thanks!
I would suggest a simpler alternative, coming from bottom-up:
Load Milestone entities with name=S5: findByName("S5")
Return the Process for each Milestone
Filter out the duplicates
Or you could even save a few SQL queries by returning not the Milestone entity but only the ID of the Process for each Milestone and then load the Process nodes by a list of IDs:
The (native) SQL equivalent would be
select *
from process
where id in (
select process_id
from milestone
where name = 'S5'
)
Regardless of my solution your join does not look completely correct to me but I can't point out what's wrong - maybe there are other methods on the JPA metamodel that return a CollectionJoin? Not sure. Probably it is because #ManyToAny is not JPA standard so the JPA criteria API does not recognize nodes as a valid "joinable" field.
I do have two entities called School and Level with a Many-To-Many relationship.
#Data
#Entity
public class School {
#Id
#GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "school_generator")
#SequenceGenerator(name="school_generator", sequenceName = "school_seq", allocationSize=1)
#Column(name = "school_id")
private Long id;
#NotBlank
private String name;
#NotBlank
private String city;
#ManyToMany(cascade = {CascadeType.PERSIST, CascadeType.MERGE})
#JoinTable(
name = "school_level",
joinColumns = #JoinColumn(name = "school_id"),
inverseJoinColumns = #JoinColumn(name = "level_id"))
private Set<Level> levels = new HashSet<>();
}
#Data
#Entity
public class Level implements Serializable {
#Id
#GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "level_generator")
#SequenceGenerator(name = "level_generator", sequenceName = "level_seq", allocationSize = 1)
#Column(name = "level_id")
private Long id;
#NotBlank
#Column(nullable = false, unique = true)
private String name;
}
My SchoolController, Rest controller, has a create method which takes a SchoolDTO (for now it has the same structure as the entity).
When I post the DTO, the nested child list Levels are populated only with existing Levels ids (since Levels are created previously).
When I try to save through the JPARepository save method...An exception with this message is thrown:
"detached entity passed to persist: com.salimrahmani.adawat.domain.Level; nested exception is org.hibernate.PersistentObjectException: detached entity passed to persist: com.salimrahmani.adawat.domain.Level",
"trace": "org.springframework.dao.InvalidDataAccessApiUsageException: detached entity passed to persist:
The problem is solved by iterating through each Level Id ...get the references by using the JPA Repository method getOne (which calls entityManager.getReference)
and then loads the right model and then persist successfully.
#PostMapping("/{id}/grades")
public ResponseEntity<SchoolDTO> addLevelToPackage(#PathVariable Long id, #RequestBody #Valid LevelDTO levelDTO) {
return schoolService.findById(id)
.map(school -> {
Level level = levelMapper.map(levelDTO);
if(level.getId() != null) {
level = levelService.getOne(level.getId());
school.getLevels().add(level);
} // else error
School saved = schoolService.save(school);
return ResponseEntity.ok(schoolMapper.map(saved));
}).orElseGet(() -> ResponseEntity.notFound().build());
}
My question is: Is this the "only" correct way to posting association in Spring Data? Or, is there any better way?
In the addLevelToPackage function Why using the map schoolService.findById(id).map.. if as I think the schoolService will return only one instance of the entity School with the given id.
I think replacing the map with the following would be enough:
...
School theSchool = schoolService.findById(id);
Level theLevel = levelService.getOne(levelDTO.getId());
theSchool.getLevels().add(theLevel);
School saved = schoolService.save(theSchool);
return ResponseEntity.ok(schoolMapper.map(saved));
...
Building a Spring Boot REST service backed by MySQL here. I'm adding a super-simple chat feature to an app and this service will handle its backend/enndpoints. I'm new to JPA and have two concerns: (1) that my primordial data model itself may be a little awry; and (2) that I'm not wrapping that model correctly using JPA conventions/best practices.
So first: an overview of the simple problem I'm trying to solve: Users can send Messages to 1+ other Users. This creates a Conversation, which is really just a container of 1+ Messages. If the Conversation is only between 2 Users, it's considered (by the app) to be a Direct Message (DM). Otherwise its considered to be a Group Chat.
My tables (pseudo-schema):
[users]
=======
id PRIMARY KEY AUTO_INC INT NOT NULL,
username VARCHAR(255) NOT NULL
[conversations]
===============
id PRIMARY KEY AUTO_INC INT NOT NULL,
created_on DATETIME NOT NULL
[messages]
==========
id PRIMARY KEY AUTO_INC INT NOT NULL,
conversation_id FOREIGN KEY INT NOT NULL, # on conversations table
sender_id FOREIGN KEY INT NOT NULL, # on users table
text VARCHAR(2000) NOT NULL,
sent_at DATETIME
[users_x_conversations]
=======================
id PRIMARY KEY AUTO_INC INT NOT NULL,
conversation_id FOREIGN KEY INT NOT NULL, # on conversations table
user_id FOREIGN KEY INT NOT NULL, # on users table
So in my design above, you can see I'm really just using the [conversations] table as a placeholder and as a way of grouping messages to a single conversation_id, and then [users_x_conversations] is crosswalk (many-to-many) table where I'm actually storing who is a "member of" which conversation.
Is this the right approach to take or is there a better way to relate the tables here? That's Concern #1.
Assumning I'm modeling the problem at the database correctly, then I have the following JPA/entity classes:
#MappedSuperclass
abstract public class BaseEntity {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
// Ctors, getters & setters down here...
}
#Entity(name = 'messages')
#AttributeOverrides({
#AttributeOverride(name = 'id', column=#Column(name='message_id'))
})
public class Message extends BaseEntity {
#OneToOne(fetch = FetchType.EAGER, cascade = [CascadeType.PERSIST, CascadeType.MERGE])
#JoinColumn(name = 'conversation_id', referencedColumnName = 'conversation_id')
#NotNull
#Valid
private Conversation conversation;
#OneToOne(fetch = FetchType.EAGER, cascade = [CascadeType.PERSIST, CascadeType.MERGE])
#JoinColumn(name = 'user_id', referencedColumnName = 'user_id')
#NotNull
#Valid
private User sender;
#Column(name = 'message_text')
#NotEmpty
private String text;
#Column(name = 'message_sent_at')
#NotNull
private Date sentAt;
// Ctors, getters & setters down here...
}
#Entity(name = 'conversations')
#AttributeOverrides({
#AttributeOverride(name = 'id', column=#Column(name='conversation_id'))
})
public class Conversation extends BaseEntity {
#Column(name = 'conversation_created_on')
#NotNull
private Date createdOn;
// Ctors, getters & setters down here...
}
What I'm stuck on now is: how should I model my [users_x_conversations] table at the JPA layer? Should I create something like this:
#Entity(name = 'users_x_conversations')
#AttributeOverrides({
#AttributeOverride(name = 'id', column=#Column(name='users_x_conversations_id'))
})
public class UserConversations extends BaseEntity {
#ManyToMany(fetch = FetchType.EAGER, cascade = [CascadeType.PERSIST, CascadeType.MERGE])
#JoinTable(
name="users_x_conversations",
joinColumns=[
#JoinColumn(name="user_id")
],
inverseJoinColumns=[
#JoinColumn(name="conversation_id")
]
)
private Map<User,Conversation> userConversations;
// Ctors, getters & setters down here...
}
Basically my service will want to be able to do queries like:
Given a conversationId, who are all the users that are members of that conversation?; and
Given a userId, what are all the conversations that user is a member of (DM and Group Chat alike)?
Is this the right approach to take or is there a better way to relate the tables here?
Your approach seems OK at the DB layer, except that if users_x_conversations serves only as a join table (i.e. if there are no extra properties associated with the (user, conversation) associations represented within), then I would use (conversation_id, user_id) as its PK instead of giving it a surrogate key. If you don't do that, then you should at least put a uniqueness constraint on that pair.
What I'm stuck on now is: how should I model my [users_x_conversations] table at the JPA layer?
I take you to be asking whether you should model that table as an entity. If you insist on giving it a surrogate key as you have done, then that implies "yes". But as I already discussed, I don't think that's needful. Nor much useful, for that matter. I would recommend instead modeling a direct many-to-many relationship between Conversation and User entities, with this table (less its id column) serving as the join table:
#Entity
#Table(name = "converations")
public class Conversation extends BaseEntity {
#Column(name = 'conversation_created_on')
#NotNull
private Date createdOn;
#ManyToMany(mappedBy = "conversations")
#JoinTable(name = "users_x_conversations",
joinColumns = #JoinColumn(name="conversation_id", nullable = false, updateable = false),
inverseJoinColumns = #JoinColumn(name = "user_id", nullable = false, updateable = false)
)
private Set<User> users;
// Ctors, getters & setters down here...
}
#Entity
#Table(name = "users")
public class User extends BaseEntity {
#NotNull
private String username;
#ManyToMany(mappedBy = "users")
// this is the non-owning side of the relationship; the join table mapping
// is declared on the other side
private Set<Conversation> conversations;
// Ctors, getters & setters down here...
}
Note in that case that User and Conversation entities are directly associated in the object model.
On the other hand, if you did choose to model users_x_conversations via an entity of its own, then the code you present for it is all wrong. It would look more like this:
#Entity
#Table(name = "users_x_converations", uniqueConstraints =
#UniqueConstraint(columnNames={"converation_id", "user_id"}))
public class UserConversation extends BaseEntity {
#ManyToOne(optional = false)
#JoinColumn(name = "conversation_id", nullable = false, updatable = false)
Conversation conversation;
#ManyToOne(optional = false)
#JoinColumn(name = "user_id", nullable = false, updatable = false)
User user;
// Ctors, getters & setters down here...
}
Note well that:
This makes the object-level association between Conversations and Users indirect, via UserConversation entities. If the relationships are navigable from the other side, then they would be modelled via #OneToMany relationship fields of type Set<UserConversation> or List<UserConversation>.
It requires more code, and more objects in the system at runtime.
On the other hand, it does have the minor advantage of saving you from making a somewhat arbitrary choice of which side of a direct #ManyToMany relationship is the owning side.
This question already has answers here:
Selectively expand associations in Spring Data Rest response
(2 answers)
Closed 5 years ago.
Is there a way to return the full details of a joined entity instead of a link? In the example below I want to also return the details of the product, if I have list of 100 purchases, it would avoid having to make 100 calls to get the product details.
The repositories for Product, User and Purchase entities are all created using spring-data-jpa
{
"_embedded" : {
"purchase" : [ {
"_links" : {
"product" : {
"href" : "http://localhost:8080/webapp/purchase/1/product"
},
"user" : {
"href" : "http://localhost:8080/webapp/purchase/1/user"
}
},
"purchasedOn" : "2014-02-23",
"amount" : 1
} ]
}
}
Entities and Repositories;
#Entity
public class User {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
#OneToMany(fetch = FetchType.LAZY, cascade = CascadeType.ALL, targetEntity = Purchase.class, orphanRemoval = true)
#JoinColumn(name = "user_id", updatable = false)
private List<Purchase> purchases = new ArrayList<>();
}
#Entity
public class Product {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
private String name;
}
#Entity
public class Purchase implements Serializable {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
Long id;
#ManyToOne
#JoinColumn(name = "user_id", referencedColumnName = "id")
private User user;
#ManyToOne(fetch = FetchType.EAGER, targetEntity = Product.class)
#JoinColumn(name = "product_id", referencedColumnName = "id")
private Product product;
#Column(name = "purchase_date")
private Date purchaseDate;
private Integer amount;
}
#Repository
public interface PurchaseRepository extends JpaRepository<Purchase, Long> {}
It would seems there is already a New Feature request for this functionality;
https://jira.springsource.org/browse/DATAREST-221
https://jira.springsource.org/browse/DATAREST-243
I'll leave the question open until the feature has been implemented.
Yes.
You can use something like a TypedQuery.
So you would create a Plain Old Java Object (POJO) which holds all the columns (fields) you need. You then use a TypeQuery to get all the fields from the database and return in a custom result set (i.e. your POJO or a collection of your POJO).
Here is a an example:
TypedQuery<BookExport> q = (TypedQuery<SOME_POJO>) entityManager.createQuery(
"select new SOME_PJOP(a.field1, a.field2, a.field2) " +
"from SOME_ENTITY AS a",
SOME_POJO.class);
UPDATE
It looks like you have related entities already. Assuming you are using "Eager Loading", all you need to do is perform a simple query in your repository implementation. Eager Loading means JPA will automatically select the related attributes.If you are using Jersey to serialize the result in JSON, it by default, will serialize of the fields of the result set.
Here is an example from a JPA project I recently completed. The scheme is simple. I have a "Book" entity. Each "Book" has a related "Author". Each "Author" has a first and last name. If you select a "Book" the JPA query also select the Author's first and last name. I believe that is analogous to what you are trying to accomplish:
#Override
#Transactional
public Object findOne(Serializable id) {
Query q = entityManager.createQuery("select a from Book a where a.id = ?1");
q.setParameter(1, id);
return q.getSingleResult();
}
The input id is simply the Book key (a number like 105).
Hope that helps :)
You can write projecton to get the details.
For example PurchaseProjection can be written as.
#Projection(name="purchaseProduction",types={Purchase.class})
interface PurchaseProjection{
public User getUser();
public Product getProduct();
public Long getAmount();
}
You can access the result using
http://localhost:8080/webapp/purchase?projection=purchaseProduction