I have this QueryDSL Predicate:
public class Predicates {
public static Predicate get() {
QUser qUser = QUser.user;
QUserCompany qUserCompany = QUserCompany.userCompany;
return qUser.id.eq(qUserCompany.userId);
}
}
as you see, I need two tables to query users (User and UserCompany), so I get an error when I declare my repository like this:
public interface UserRepository QueryDslPredicateExecutor<User>
{
}
because internally UserCompany table is not included in the query (QueryDslPredicateExecutor<User>).
List<User> users = userRepository.findAll(Predicates.get());
How can I build a Predicate using two tables and use it with QueryDslPredicateExecutor?
Solved! I have mapped the one-to-many relationship in the User entity to UserCompany and then:
public class Predicates {
public static Predicate get() {
QUser qUser = QUser.user;
return qUser.userCompanies.any().userId.eq(qUser.id);
}
}
Related
I'm using Criteria API to retrieve entities from the database. I have two entities as following:
public class EntityA {
String primaryKey;
#JoinColumn(
updatable = false,
insertable = false,
name = "primaryKey",
referencedColumnName = "primaryKeyOne")
List<EntityB> list;
}
public class EntityB {
String primaryKeyOne;
String primaryKeyTwo;
String name;
Integer value;
}
Now I want to create a Spring JPA Specification using these two entities.
public interface Specification<T> {
Predicate toPredicate(Root<T> root, CriteriaQuery<?> cr, CriteriaBuilder cb);
}
After I fetched EntityA, I can get the whole list of EntityB. But how do I filter out a specific instance of EntityB and create a predicate using it? My intention is as the following:
cb.gt(root.get("list").stream().filter(e -> "abc".equals(e.getName())).get("value"), 100)
Usually you should perform a join between the two entities and do the filtering based on the join.
Given your example, the specification will be something like the following:
public class EntityASpecifications {
public static Specification<EntityA> toSpecification(String name, Integer value){
return (root, criteriaQuery, criteriaBuilder) -> {
Join<EntityA, EntityB> entityBJoin = root.join("list");
return criteriaBuilder.and(
criteriaBuilder.equal(entityBJoin.get("name"), name),
criteriaBuilder.gt(entityBJoin.get("value"), value)
);
};
}
}
You will then use it with the repository of EntityA like so,
entityARepository.findAll(EntityASpecifications.toSpecification("abc", 100));
I'm trying to implement the equivalent of this SQL query using Spring Data JPA Specifications:
SELECT * FROM product WHERE category_id IN (....)
The two entities involved in this query have a OneToMany and ManyToOne relationship:
ProductEntity:
#Data
#NoArgsConstructor
#Entity
#Table(name = "PRODUCT")
public class ProductEntity extends AbstractAuditableEntity {
// skipped other properties for simplicity sake
#ManyToOne
private CategoryEntity categoryEntity;
}
CategoryEntity:
#Entity
#Table(name = "PRODUCT_CATEGORY")
#Data
public class CategoryEntity extends AbstractBaseEntity {
// skipped other properties for simplicity sake
#OneToMany(mappedBy = "categoryEntity")
private List<ProductEntity> productEntities;
}
My JPA Specifications query compiles and runs without any error, but doesn't return any result:
Specification definition:
public static Specification<ProductEntity> inCategories(List<Long> categories) {
return (root, query, builder) -> {
if (categories != null && !categories.isEmpty()) {
final Path<CategoryEntity> category = root.get("categoryEntity");
return category.get("id").in(categories);
} else {
// always-true predicate, means that no filtering would be applied
return builder.and();
}
};
}
Client code:
Page<ProductEntity> productEntityPage = productRepository.findAll(Specification
.where(ProductSpecifications.inCategories(filterCriteria.getCategories()))
, pageRequest);
Why doesn't it work? I get results when querying the database using SQL statements, so there must be something wrong with either my JPA Specifications query or my entities mapping.
What am I missing?
I think you should use join here
Join<ProductEntity, CategoryEntity> categoryJoin = root.join("categoryEntity");
return categoryJoin.get("id").in(categories);
instead of
Path<CategoryEntity> category = root.get("categoryEntity");
return category.get("id").in(categories);
since root.get("categoryEntity").get("id"); will give you nothing as no such path (product.categoryEntity.id) exists in product table.
I would like to change the way EF works with deleting records.
Instead of deleting the row in the database it should fill a column (GCColumn or so).
When retrieving data it should always filter on GCColumn IS NULL + the filter you apply.
Anyone know if this is achievable and how ?
I addition to my answer above, consider the case in which many or even all of your entities have this GCColumn.
You could start with a base entity for these pseudo-deletable entities:
public abstract class PseudoDeletable
{
public DateTime GCColumn { get; set;}
}
and have entities defined as:
public class Order : PseudoDeletable
{
public int Id { get; set; }
public int ProductId { get; set; }
public DateTime OrderDate { get; set; }
// etc.
}
Then, you could create a generic base repository
public class RepositoryBase<TEntity> where TEntity : PseudoDeletable
{
protected IDbSet<TEntity> DbSet { get; }
public RepositoryBase()
{
DbSet = context.Set<TEntity>();
}
private Expression<Func<TEntity, bool>> RemoveDeleted
{
get { return e => e.GCColumn == null; }
}
public virtual IEnumerable<TEntity> GetAll(Expression<Func<TEntity, bool>> expression)
{
expression = expression.And(RemoveDeleted);
return DbSet.Where(expression).ToList();
}
}
and have derived repositories, like:
public class OrderRepository : RepositoryBase<Order>
{
}
The GetAll method can then be called like this:
new orderRepository().GetAll(x => x.ProductId == 1);
and it will just return orders that have not been deleted.
Please note that you'll have an issue with entity includes for related records: how to include only un-deleted related entities, but that is a consequence of you desire to keep 'deleted' records in the database.
In one project we use the repository pattern for database access and each entity has its own repository.
It is a multi-tenant database and we use the type of filter you are looking for to filter entities accessible to the current user, not to filter for a delete flag, but the method could be used analogously.
Each repository that needs filtering, gets a filter method:
private Expression<Func<Order, bool>> RemoveDeleted
{
get
{
return order => order.GCColumn == null;
}
}
Then, add an expression to each repository method, like:
public override IEnumerable<Order> GetAll(Expression<Func<Order, bool>> expression)
{
expression = expression.And(RemoveDeleted);
return DbSet.Where(expression).ToList();
}
(The extension method Add comes from a set of ExpressionExtensions.)
Now, you can use expressions like:
orderRepository.GetAll(x => x.ProductId == productId);
and
orderRepository.GetAll(x => x.OrderDate >= DateTime.Now.AddMonths(-1));
So now you business logic can have many methods using the same GetAll() methods, with different filters, but doesn't have to care about 'deleted' entities. But you are still responsible for creating a correct filter for each repository method.
If the delete flag is not in all entities, but the delete status is registered in another entity, you can do the following:
private Expression<Func<Order, bool>> RemoveDeleted
{
get
{
return orderLine => orderLine.Order.GCColumn == null;
}
}
In this example orders are deleted in whole, not individual lines in it.
In the following class, both the methods return the same objects.
But is the list of objects returned from first is managed since it is part of transaction when compared to second one which is not part of transaction?
public class QueryServiceImpl implements QueryService {
#PersistenceContext(unitName="PC")
EntityManager em;
//default attribute
#TransactionAttribute(TransactionAttributeType.REQUIRED)
public List findAlItems() {
return em.createQuery("SELECT item FROM Item item",
Item.class)
.getResultList();
}
#TransactionAttribute(TransactionAttributeType.NOT_SUPPORTED)
public List findAlItemsNoTransaction() {
return em.createQuery("SELECT item FROM Item item",
Item.class)
.getResultList();
}
Regarding your example, you're right. All entities should not be managed after returning from the second method (findAlItemsNoTransaction). However, if you would like to keep them managed you should use:
#PersistenceContext(type=EXTENDED)
like it was showed in JPA specification:
/*
* An extended transaction context is used. The entities remain
* managed in the persistence context across multiple transactions.
*/
#Stateful
#Transaction(REQUIRES_NEW)
public class ShoppingCartImpl implements ShoppingCart {
#PersistenceContext(type=EXTENDED)
EntityManager em;
private Order order;
private Product product;
public void initOrder(Long id) {
order = em.find(Order.class, id);
}
public void initProduct(String name) {
product = (Product) em.createQuery("select p from Product p
where p.name = :name")
.setParameter("name", name)
.getSingleResult();
}
public LineItem createLineItem(int quantity) {
LineItem li = new LineItem(order, product, quantity);
order.getLineItems().add(li);
em.persist(li);
return li;
}
}
I have the following Play Framework entity (using Morphia for persistence) as part of a generic blogging app:
#Entity
public class Comment extends Model {
...
#Reference
#Indexed
public SiteUser commenter;
public static List<Comment> getLastCommentsByUsers(final List<SiteUser> users) {
final Query<Comment> query ds().createQuery(Comment.class);
query.field(commenter).hasAnyOf(users);
return query.asList();
}
}
SiteUser:
#Entity(noClassnameStored=true)
public class SiteUser extends AbstractUser {
public String realName;
}
AbstractUser:
public class AbstractUser extends Model {
#Indexed(value= IndexDirection.DESC, unique = true)
public String emailAddress;
#Required
public String password;
}
The method getLastCommentsByUsers() is supposed to return all comments by the users in the users parameter, but I always get an empty List back. The reason that Commment is a separate collection is to be able to retrieve last X Comments by certain users across their associated Posts, which isn't possible if the Comment is embedded in the Post collection.
Is there something wrong with my query (should I be using something other than hasAnyOf), or is it a problem with the relationship mapping - should I be using ObjectId instead?
I use the in() method with a list or set and its working perfectly. Here's a snippet:
List<String> keywordList;
List<Product> products = Product.find().field("keywords").in(keywordList).asList();
This should work for collection of embedded or references too.
You should use List<Key<SiteUser>> to query:
public static List<Comment> getLastCommentsByUsers(final List<SiteUser> users) {
final Query<Comment> query ds().createQuery(Comment.class);
query.field(commenter).hasAnyOf(toKeys(users)); // convert to keys
return query.asList();
}
public static List<Key<SiteUser>> toKeys(List<SiteUser> users) {
List<Key<SiteUser>> keys = new ArrayList<Key<SiteUser>>();
for(SiteUser user: users) {
keys.add(ds().getMapper().getKey(user));
}
return keys;
}
Or you can just get the keys by:
List<Key<SiteUser>> keys = ds().createQuery(SiteUser.class).query().filter(...).asKeyList();