I have the following classes, where there's a 1 - n relationship between Customer and Order, i.e. each Customer has many Orders
class Customer {
string identifier;
string country;
Collection orders;
}
class Order {
string identifier;
float amount;
}
class Report {
string identifier;
string country;
float amount;
}
I want to write the following in the form of one or more Drools decision tables.
For each Customer c
if c.country == US then
for each Order o
if o.amount > $10 then
create Report r
r.country = c.country
r.amount = o.amount
How possible is this using Drools decision tables?
When a Customer object meets the Customer condition, I then need to run each instance in the collection of Orders through the Order condition. If the order meets the condition, I need to create a report object that has values taken from the Customer and from the Order.
Drools will naturally iterate through collections.
This what your rule would look like in DRL:
rule "US Customer - Create Reports"
when
$customer: Customer( country == "US", $orders: orders != null )
$order: Order( amount > 10 ) from $orders
then
Report r = new Report();
r.country = $customer.country;
r.amount = $order.amount;
// TODO: do something with Report r here
end
This flattens naturally into a decision table in a very straight-forward fashion. You can even sub out the "US" (country) and 10 (amount) to variables.
Related
A newbie asks...
Part 1
Suppose I have 3 classes (and their equivalent SQL tables) :
Product
{
int Id;
List<Keyword> Keywords;
List<Serial> Serials;
}
Keyword
{
int Id;
int ProductId; // FK to Product
string Name;
}
Serial
{
int Id;
int ProductId; // FK to Product
string SerialNumber;
}
When loading PRODUCT == 123, the we could do this:
item = db.Products.FirstOrDefault(p => p.Id == 123);
item.Keywords = db.Keywords.Where(p => p.ProductId == 123).ToList();
item.Serials = db.Serials.Where(p => p.ProductId == 123).ToList();
which is 3 SQL statements.
Or we could do this:
from product in db.Products.AsNoTracking()
join link1 in Db.Keywords.AsNoTracking()
on product.Id equals link1.ProductId into kwJoin
from keyword in kwJoin.DefaultIfEmpty()
join link2 in Db.Serials.AsNoTracking()
on product.Id equals link2.ProductId into serJoin
from serial in serJoin.DefaultIfEmpty()
where product.Id == 123
select new { product, keyword, serial };
which gives 1 SQL statement but produces far too many rows (number of keywords x number of serials) that need to be coalesced together
Both seem less than efficient. Is there a better way?
Part 2
As another question, but using the same example, when we have a join like so:
from product in db.Products.AsNoTracking()
join link1 in Db.Keywords.AsNoTracking()
on product.Id equals link1.ProductId into kwJoin
from keyword in kwJoin.DefaultIfEmpty()
select new { product, keyword };
Is there a way to assign the keywords directly in the product, in select statement?
select new { product, product.Keywords = keyword };
Thanks for any help!
If the FKs exist, depending on how you have setup your DB context, the properties will automatically be fetched. No joins required. Part 1 query is simple as it has a filter. Part 2 might have issues depending on how many records needs to be fetched from the database. You can map the fields to anonymous objects(or DTOs) after the fact that you have keyword objects for each product in the list.
Part 1
item = db.Products
.Include(p=>p.Keywords)
.Include(s=>s.Serials)
.Where(p => p.Id == 123)
.FirstOrDefault();
Part 2
products = db.Products.Include(p=>p.Keywords).ToList();
I have an issue where we create a complex IQueryable that we need to make it more efficient.
There are 2 tables that should only be included if columns from them are being filtered.
My exact situation is complex to explain so I thought I could illustrate it with an example for cars.
If I have a CarFilter class like this:
public class CarFilter
{
public string BrandName { get;set; }
public decimal SalePrice {get; set; }
}
Let's say that we have a query for car sales:
var info = from car in cars
from carSale in carSales on carSale.BrandId == car.BrandId && car.ModelId == carSale.ModelId
from brand in carBrands on car.BrandId == brand.BrandId
select car
var cars = info.ToList();
Let's say that this is a huge query that returns 100'000 rows as we are looking at cars and sales and the associated brands.
The user only wants to see the details from car, the other 2 tables are for filtering purposes.
So if the user only wants to see Ford cars, our logic above is not efficient. We are joining in the huge car sale table for no reason as well as CarBrand as the user doesn't care about anything in there.
My question is how can I only include tables in my IQueryable if they are actually needed?
So if there is a BrandName in my filter I would include CarBrand table, if not, it's not included.
Using this example, the only time I would ever want both tables is if the user specified both a BrandName and SalePrice.
The semantics are not important here, i.e the number of records returned being impacted by the joins etc, I am looking for help on the approach
I am using EF Core
Paul
It is common for complex filtering. Just join when it is needed.
var query = cars;
if (filter.SalePrice > 0)
{
query =
from car in query
join carSale in carSales on new { car.BrandId, car.ModelId } equals new { carSale.BrandId, carSale.ModelId }
where carSale.Price >= filter.SalePrice
select car;
}
if (!filter.BrandName.IsNullOrEempty())
{
query =
from car in query
join brand in carBrands on car.BrandId equals brand.BrandId
where brand.Name == filter.BrandName
select car;
}
var result = query.ToList();
I have an entity which contains a list of elements and now I want to search over attributes of these elements. This constraint should be "and" connected. Please see these simple example:
#Entity
public class Parent {
#Column
#Enumerated(EnumType.STRING)
private City city;
#OneToMany(...)
private List<Children> childrens;
}
#Entity
public class Children {
#Column
#Enumerated(EnumType.STRING)
private School school;
#Column
private Integer yearInSchool;
}
Now I want to find Parents in a certain city, lets say "BigCity" with children in School "AwesomeSchool" which are in class/ year 6. I want to get the search result only via CriteriaBuilder.
So far I got:
final CriteriaBuilder c = getCriteriaBuilder();
final CriteriaQuery<Parent> query = c.createQuery(Parent.class);
final Root<Parent> r = query.from(Parent.class);
query.select(r)
.where(c.and(c.equal(r.get("city"), City.BigCity)),
c.equal(r.get("childrens").get("school"), School.AwesomeSchool),
c.equal(r.get("childrens").get("yearInSchool"), 6));
Unfortunately there are two problems here:
- it looks like I can't call get("school") on the list attribute
- this will return all parents with children which are either in "AwesomeSchool" or are 6 years in the school.
Can you help me please? I thought about using a join, but there the same question is: how can I define the where part of the join so that it considers that both attributes (school and yearInSchool) have to be fulfilled at the same time.
I found similar posts about querying for objects whose children fulfill one condition - but here the children has to fulfill two conditions at the same time.
Update 1
If I use a join to assert e.g. the "school" of one child, I get so far concerning the predicate:
Predicate predicate = r.join("childrens").get("school").in(School.AwesomeSchool)
How can I reuse this joined object to assert is also for the second filter condition?
You need to JOIN and then use the JOIN object you got when forming the join when forming the WHERE clauses.
Join childrenJoin = r.join("childrens");
query.where(c.and(c.equal(r.get("city"), City.BigCity)),
c.equal(childrenJoin.get("school"), School.AwesomeSchool),
c.equal(childrenJoin.get("yearInSchool"), 6));
Perhaps you mean your JPQL to be :
SELECT p FROM Parent p JOIN p.childrens c
WHERE p.city = :theCity AND c.school = :theSchool AND c.yearInSchool = 6
I'm having a Product object with a list of related products (which are also product objects). The field of related products is annotated like this:
public class Product {
#JoinTable(name = "RELATED_PRODUCT", joinColumns = {
#JoinColumn(name = "PRODUCT_ID", referencedColumnName = "id", nullable = false)}, inverseJoinColumns = {
#JoinColumn(name = "RELATED_PRODUCT_ID", referencedColumnName = "id", nullable = false)})
#ManyToMany(fetch = FetchType.LAZY)
List<Product> relatedProducts;
}
As you can see the list is fetched lazy, which is what I want in most cases. In some cases however, I want the list of related products to be filled immediatly. I created a query for this with a LEFT JOIN FETCH. However, I want only the related products to be added that have a certain rating, let's say a rating of > 3.
I tried the following:
SELECT DISTINCT p FROM Product p LEFT JOIN FETCH p.comparableProducts cp WHERE p.id = :id AND cp.rating > 3 AND CURRENT_DATE BETWEEN p.commenceDate AND p.removeDate
But this doesn't work. It always returns back ALL related products in the database, not just the ones that have a rating above 3. How is this fixable?
The easiest way to solve this problem is to load related products separately instead of trying to fit them into relatedProducts field.
It also makes perfect sense from object oriented point of view. I suppose you have something like "Product page" that contains the selected product and "recommended products". If so, such a page is a separate concept that deserves its own class:
public class ProductPage {
private Product product;
private List<Product> recommendedProducts;
...
}
Then you can fill such a class either by a single query:
SELECT DISTINCT p, cp FROM Product p LEFT JOIN p.comparableProducts cp WHERE p.id = :id AND cp.rating > 3 AND CURRENT_DATE BETWEEN p.commenceDate AND p.removeDate
or by two separate queries.
Unfortunately, this approach doesn't allow you to receive an instance of ProductPage directly from JPA, you need to write conversion code manually.
I have a problem getting the following scenario to work. A student can take tests. A student have over time taken a few tests and got a score for each test. Each student entity have a list of tests that they have completed mapped as #OneToMany.
Now I want to select all students that have completed tests on a range of grouped criterions. I want for example to search for all students that have:
Group 1: Completed "Test 1" and got a score "between 75 and 100"
and/or
Group 2: Completed "Test 2" and got a score "between 50 and 80"
This is what I have so far but it does not do what I need (cannot search by multiple parameters meaning that I have to perform the query multiple times):
SELECT s FROM Student s JOIN s.tests t WHERE t.score BETWEEN :minScore AND :maxScore AND t.testName = :testName
Is there a way to use a single NamedQuery to achieve what I want? To retrieve all Students that have completed a test that matches at least one of the parameter groups above? I've been experimenting with joins but keep running into the wall.
I made a sample code skeleton below to illustrate what I'm trying to do.
#Entity
#NamedQueries({
#NamedQuery(name="Student.findStudentByParams", query="????????") // What should this query look like to satisfy the criteria? (see below for more detail)
})
public class Student {
// .. Some other variables that are not relevant for this example
#Id
private String name;
#OneToMany(fetch=FetchType.EAGER, mappedBy = "student")
private List<Test> tests;
// Setters and getters
}
#Entity
public class Test {
private double score;
private String testName;
// .. Some other variables that are not relevant for this example
#ManyToOne(cascade=CascadeType.ALL)
private Student student;
// Setters and getters
}
public class SearchParameters {
private double minScore;
private double maxScore;
private String testName;
public SearchParameters(String minScore, String maxScore, String testName) {
this.minScore = minScore;
this.maxScore = maxScore;
this.testName = testName;
}
// Setters and getters
}
public class MainClass {
public static List<Student> getStudents(List<SearchParameters> searchParams) {
// Database initialization stuff
// What should the query look like to find all students that match any of the combined requirements in the searchParams list?
// Is it possible to do in a single query or should i make multiple ones?
// What parameters should i set? Is it possible to put in the entire array and do some sort of join?
// Retrieve all students which matches any of these search parameters:
// Have either:
// Completed "Test 1" and got a score between 75 and 100
// and/or:
// Completed "Test 2" and got a score between 50 and 80
Query namedQuery = em.createNamedQuery("Student.findStudentByParams");
namedQuery.setParameter(??);
return (List<Student>)namedQuery.getResultList();
}
public static void main() {
List<SearchParams> searchParams = new ArrayList<SearchParams();
searchParams.add(new SearchParameters(75,100, "Test 1"));
searchParams.add(new SearchParameters(50,80, "Test 2"));
// Retrieve all students which matches any of these search parameters:
// Have either:
// Completed "Test 1" and got a score between 75 and 100
// and/or:
// Completed "Test 2" and got a score between 50 and 80
ArrayList<Student> students = getStudents(searchParams);
for(Student s: students) // Print all user that match the criteria
{
System.out.println("Name: " + s.getName());
}
}
}
You need to use Criteria Builder (and eventually the canonical Metamodel).
Try something like this (code not tested):
EntityManager em; // put here your EntityManager instance
CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<Student> cq = cb.createQuery(Student.class);
Root<Student> student = cq.from(Student.class);
Predicate predicate = cb.disjunction();
for (SearchParams param : searchParams) {
ListJoin<Student, Test> tests = student.join(Student_.tests);
Predicate tempPredicate1 = cb.equal(tests.get(Test_.testName), param.getTestName());
Predicate tempPredicate2 = cb.ge(tests.get(Test_.score), param.getMinScore());
Predicate tempPredicate3 = cb.le(tests.get(Test_.score), param.getMaxScore());
Predicate tempPredicate = cb.and(tempPredicate1, tempPredicate2, tempPredicate3);
predicate = cb.or(predicate, tempPredicate);
}
cq.where(predicate);
TypedQuery<Student> tq = em.createQuery(cq);
return tq.getResultList();
I don't see how it would be possible without composing the query dynamically. Consider using the Criteria API to create it.
I would design the query like this:
select s from Student s where
exists (select t.id from Test t where t.student.id = s.id and ...)
or
exists (select t.id from Test t where t.student.id = s.id and ...)
or
exists (...)
As you see, there's a repeating pattern, and all these subqueries are similar an are combined into a disjunction.