Spring Data/JPS (N+1) SELECTs with OneToOne associations - spring

In my Spring Data application I ran into (N+1) selects issue.
I have a following Spring Data entities:
#Entity
#Table(name = "card_categories")
public class CardCategory extends BaseEntity implements Serializable {
#Id
#SequenceGenerator(name = "card_categories_id_seq", sequenceName = "card_categories_id_seq", allocationSize = 1)
#GeneratedValue(strategy = GenerationType.AUTO, generator = "card_categories_id_seq")
private Long id;
private String name;
...
}
#Entity
#Table(name = "levels")
public class Level extends BaseEntity implements Serializable {
#Id
#SequenceGenerator(name = "levels_id_seq", sequenceName = "levels_id_seq", allocationSize = 1)
#GeneratedValue(strategy = GenerationType.AUTO, generator = "levels_id_seq")
private Long id;
private String name;
...
}
#Entity
#Table(name = "card_categories_levels")
public class CardCategoryLevel extends BaseEntity implements Serializable {
#Id
#SequenceGenerator(name = "card_categories_levels_id_seq", sequenceName = "card_categories_levels_id_seq", allocationSize = 1)
#GeneratedValue(strategy = GenerationType.AUTO, generator = "card_categories_levels_id_seq")
private Long id;
#OneToOne
#JoinColumn(name = "card_category_id")
private CardCategory cardCategory;
#OneToOne
#JoinColumn(name = "level_id")
private Level level;
...
}
and empty Spring Data repository:
#Repository
public interface CardCategoryLevelRepository extends JpaRepository<CardCategoryLevel, Long> {
}
When I try to fetch all CardCategoryLevel entities by cardCategoryLevelRepository.findAll() method it produces 3 SELECTs for an each row in my card_categories_levels table.
In order to use one single JOIN instead of N+1 SELECTs I have reimplemented my CardCategoryLevelRepository:
#Repository
public interface CardCategoryLevelRepository extends JpaRepository<CardCategoryLevel, Long> {
#Query(value = "SELECT ccl FROM CardCategoryLevel ccl LEFT JOIN FETCH ccl.cardCategory cc LEFT JOIN FETCH ccl.level l where cc = :cardCategory and l = :level")
CardCategoryLevel findByCardCategoryAndLevel(#Param("cardCategory") CardCategory cardCategory, #Param("level") Level level);
#Override
#Query(value = "SELECT ccl FROM CardCategoryLevel ccl LEFT JOIN FETCH ccl.cardCategory LEFT JOIN FETCH ccl.level")
List<CardCategoryLevel> findAll();
}
but I'm not sure I did it in a right optimal way.
Please validate my approach and tell it is optimal workaround for (N+1) SELECTs issue with OneToOne associations in Spring Data or no and what is the best way to solve it.
Should I leave it as is or may be move to some other abstraction.. for example like QueryDSL or something like that ?

Thanks to Xtreme Biker I have re-implemented my solution with entity graphs and QueryDSL

Related

findBy not working with inherited properties

I have the following model and repository:
#Entity
#Table(name = "db_user", uniqueConstraints = { #UniqueConstraint(columnNames = "email") })
public class User {
#Id
#GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "seq_user")
#SequenceGenerator(name = "seq_user", sequenceName = "seq_user")
#Column(name = "id")
private Long id;
// ...
}
#Entity
#Table(name = "movie")
public class Movie extends AbstractItem {
// Id column inherited from AbstractItem
// ...
}
#Entity
#Table(name = "movie_user")
public class MovieOwnership extends AbstractOwnership {
#ManyToOne
private Movie movie;
// ...
}
#MappedSuperclass
public abstract class AbstractOwnership{
#Id
#SequenceGenerator(name = "seq_default", sequenceName = "seq_default")
#GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "seq_default")
#Column(name = "id")
private Long id;
#ManyToOne
private User owner;
// ...
}
public interface MovieOwnershipRepository extends QueryDslJpaRepository<MovieOwnership, Long> {
List<MovieOwnership> findByOwnerId(Long ownerId);
MovieOwnership findByOwnerIdAndMovie(Long ownerId, Movie movieId);
List<MovieOwnership> findByOwnerIdAndMovieIdIn(Long ownerId, Set<Long> movieIds);
}
I'm trying to use Spring's findBy requests to fetch MovieOwnerships by owner or movie, using the id field of both entities. I'm able to work directly with the owner's id, but using MovieId in my requests seems broken (I can use the whole Movie object though). In the code above, the first two findBy are fine but the last one throws this exception:
Caused by: java.lang.IllegalArgumentException: Unable to locate
Attribute with the the given name [movieId] on this ManagedType
[carrm.app.data.AbstractOwnership]
It compiles if I try with another property from Movie (like findByMovieTitle), but I can't make it work on the id.
Any idea how to solve this?
I tried the same with JpaRepository instead of QueryDslJpaRepository.
The SQL is generated correctly:
select movieowner0_.id as id1_1_, movieowner0_.owner_id as owner_id2_1_, movieowner0_.movie_id as movie_id3_1_
from movie_ownership movieowner0_
left outer join user user1_ on movieowner0_.owner_id=user1_.id
left outer join movie movie2_ on movieowner0_.movie_id=movie2_.id
where user1_.id=? and (movie2_.id in (?))
So it must be a QueryDslJpaRepository implementation bug.
I would suggest you use JpaRepository instead.

Using JPARepository on Entity with Inheritancetype.TABLE_PER_CLASS result in wrong query

I've defined 2 entities in my project using the inheritance type "TABLE_PER_CLASS".
After that I've defined 2 repositories to access data but, when I use them to find records for the B entity, the query generated does not include a JOIN statement but only "SELECT id, name, alternate_name from b" and it fails because "name" field does not exists.
Did I miss something that I can't see?
Entity A:
#Entity
#Inheritance(strategy = InheritanceType.TABLE_PER_CLASS)
#Table(name = "a")
#Cache(usage = CacheConcurrencyStrategy.NONSTRICT_READ_WRITE)
public abstract class A implements Serializable {
private static final long serialVersionUID = 1L;
#Id
#GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "sequenceGenerator")
#SequenceGenerator(name = "sequenceGenerator")
private Long id;
#Column(name = "name")
private String name;
...
...
}
Entity B:
#Entity
#Table(name = "b")
#Cache(usage = CacheConcurrencyStrategy.NONSTRICT_READ_WRITE)
public class B extends A implements Serializable {
private static final long serialVersionUID = 1L;
#Id
#GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "sequenceGenerator")
#SequenceGenerator(name = "sequenceGenerator")
private Long id;
#Column(name = "alternate_name")
private String alternateName;
...
...
}
BaseRepository:
#SuppressWarnings("unused")
#NoRepositoryBean
public interface BaseRepository<T extends A> extends JpaRepository<T, Long> {
}
ARepository:
#SuppressWarnings("unused")
#Repository
public interface ARepository extends BaseRepository<A> {
}
BRepository:
#SuppressWarnings("unused")
#Repository
public interface BRepository extends BaseRepository<B> {
}
I assume that you have a table a and a table b.
But TABLE_PER_CLASS will only generate/use a table per concrete subclass. In your case just table b.
If you want table a and b you must use the JOINED strategy.
Try to use #GeneratedValue annotation without explicitly saying the strategy.
#Id
#GeneratedValue
private Long id;

Jpa findAllBy* using Long id instead of an entity

I just do not want to do:
myEntity = findById(Long id)
findAllByEntityAnd*(MyEntity myEntity, *)
instead I want do:
findAllByEntityAnd*(Long entityId, *)
Is there something I missed to achieve what I wanted or I just cannot achieve it directly?
Thanks for the help ~
When I tried it, the Spring prompted me:
java.lang.IllegalArgumentException: Could not create query metamodel for method public abstract java.util.List com.worksap.morphling.raptor.dump.thread.dao.ThreadDoRepository.findAllByDumpDoAndLocksWaitingContains(java.lang.Long,java.lang.String)!
Here is my table for your reference:
#Data
#Builder
#Entity
#Table(name = "thread_info")
#AllArgsConstructor
#NoArgsConstructor
public class ThreadDo implements Serializable {
private static final long serialVersionUID = -1L;
String name;
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
#ManyToOne
#JoinColumn(name = "thread_dump_id")
#JsonIgnore
private ThreadDumpDo dumpDo;
...
}
#Data
#Builder
#Entity
#Table(name = "thread_dump")
#AllArgsConstructor
#NoArgsConstructor
public class ThreadDumpDo implements Serializable {
private static final long serialVersionUID = -1L;
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
#OneToMany(
mappedBy = "dumpDo",
cascade = CascadeType.ALL,
orphanRemoval = true
)
private List<ThreadDo> threadDoList;
}
And then I have a ThreadDoRepository and want to locate the threadDos directly like this:
List<ThreadDo> findAllByDumpDoAndLocksWaitingContains(Long dumpId, String lockWaiting);
I can achieve it via #Query as follows, but I really think it's pretty ugly since it's easy to interpret (I think).
#Query(value = "select * from thread_info t where t.thread_dump_id = ?1 and t.locks_waiting like ?2",
nativeQuery = true)
List<ThreadDo> findAllByDumpDoAndLocksWaitingContains(Long dumpId, String lockWaiting);
Try this
List<ThreadDo> findAllByDumpDo_IdAndLocksWaitingContains(Long dumpId, String lockWaiting);

Selecting latest workout set by user

I'm trying to select the latest added workout set associated with a given user.
Users has sessions and sessions has sets. My entities are defined as below.
#Entity(name = "users") // Postgres doesn't like the table name "user"
public class User {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private long id;
#OneToMany(mappedBy = "user")
private Set<Session> sessions;
...
#Entity
public class Session {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private long id;
#ManyToOne
private User user;
#OneToMany(mappedBy = "session")
private Set<WorkoutSet> sets;
...
#Entity
public class WorkoutSet {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private long id;
#OneToOne
private User user;
#ManyToOne
private Session session;
private LocalDateTime timestamp = LocalDateTime.now();
...
The following sql query seems to do the trick
select w
from workout_set w
inner join session s
on w.session_id = s.id
where s.user_id = 1
order by w.timestamp DESC
limit 1
But when I'm trying to do something like the below
#Repository
public interface WorkoutSetRepository extends CrudRepository<WorkoutSet, Long> {
#Query("select w from WorkoutSet w inner join Session s on w.session_id = s.id where s.user = :user order by w.timestamp")
List<WorkoutSet> findLastSet(User user, Pageable limit);
I get...
org.springframework.data.mapping.PropertyReferenceException: No property user found for type WorkoutSet!
Any clues about how to do the query right? I'm very open to alternative ways as well because I'd rather avoid writing jpql if possible.
Try this:
#Repository
public interface WorkoutSetRepository extends CrudRepository<WorkoutSet, Long> {
#Query("select w from WorkoutSet w inner join w.session s where s.user = :user order by w.timestamp")
List<WorkoutSet> findLastSet(#Param("user") TestUser user);
}
Note the difference in the join clause. This works with hibernate 5.0.11.

spring data jpa: No aliases found in result tuple! Make sure your query defines aliases

When I try to get the users using repository interface I received following exception "org.springframework.dao.InvalidDataAccessApiUsageException: No aliases found in result tuple! Make sure your query defines aliases!; nested exception is java.lang.IllegalStateException: No aliases found in result tuple! Make sure your query defines aliases!"
Repository:
#Repository
public interface UserRelationshipRepository
extends JpaRepository<UserRelationship, Long>, QueryDslPredicateExecutor<UserRelationship> {
#Query(value = "SELECT ur.id.toUser FROM UserRelationship ur WHERE ur.fromUser = :fromUser AND ur.relationshipTypeId = 1")
Set<User> findUserFriends(#Param("fromUser") User fromUser);
}
Entities:
#Entity
#NamedEntityGraph(name = "graph.User", attributeNodes = {})
#Table(name = "users")
public class User extends BaseEntity implements UserDetails {
private static final long serialVersionUID = 8884184875433252086L;
#Id
#SequenceGenerator(name = "users_id_seq", sequenceName = "users_id_seq", allocationSize = 1)
#GeneratedValue(strategy = GenerationType.AUTO, generator = "users_id_seq")
private Long id;
#Column(name = "first_name")
private String firstName;
#OneToMany(fetch = FetchType.LAZY, mappedBy = "fromUser", cascade = CascadeType.ALL)
private Set<UserRelationship> relationships = new HashSet<UserRelationship>();
// getters setters
}
#Entity
#NamedEntityGraph(name = "graph.UserRelationship", attributeNodes = {})
#Table(name = "users_relationships")
public class UserRelationship extends BaseEntity implements Serializable {
private static final long serialVersionUID = -6367981399229734837L;
#EmbeddedId
private final UserRelationshipId id = new UserRelationshipId();
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "from_user_id", nullable = false)
#MapsId("fromUserId") // maps fromUserId attribute of the embedded id
private User fromUser;
#Column(name = "relationship_type_id")
private Long relationshipTypeId;
}
I am using '1.11.0.BUILD-SNAPSHOT' version of spring data jpa.
This is already known issue, and it is marked as resolved, but I am still get it.
Please, help me to solve this.
Update:
If I change repository method's return type to Set<Object> then all works fine.
You're running into DATAJPA-885, which is already fixed and will be part of the Spring Data Hopper SR2 release.

Resources