I have a data model that looks something like this:
public class Item {
private List<ItemAttribute> attributes;
// other stuff
}
public class ItemAttribute {
private String name;
private String value;
}
(this obviously simplifies away a lot of the extraneous stuff)
What I want to do is create a query to ask for all Items with one OR MORE particular attributes, ideally joined with arbitrary ANDs and ORs. Right now I'm keeping it simple and just trying to implement the AND case. In pseudo-SQL (or pseudo-HQL if you would), it would be something like:
select all items
where attributes contains(ItemAttribute(name="foo1", value="bar1"))
AND attributes contains(ItemAttribute(name="foo2", value="bar2"))
The examples in the Hibernate docs didn't seem to address this particular use case, but it seems like a fairly common one. The disjunction case would also be useful, especially so I could specify a list of possible values, i.e.
where attributes contains(ItemAttribute(name="foo", value="bar1"))
OR attributes contains(ItemAttribute(name="foo", value="bar2"))
-- etc.
Here's an example that works OK for a single attribute:
return getSession().createCriteria(Item.class)
.createAlias("itemAttributes", "ia")
.add(Restrictions.conjunction()
.add(Restrictions.eq("ia.name", "foo"))
.add(Restrictions.eq("ia.attributeValue", "bar")))
.list();
Learning how to do this would go a long ways towards expanding my understanding of Hibernate's potential. :)
Could you use aliasing to do this?
Criteria itemCriteria = session.createCriteria(Item.class);
itemCriteria.createAlias("itemAttributes", "ia1")
.createAlias("itemAttributes", "ia2")
.add(Restrictions.eq("ia1.name", "foo1"))
.add(Restrictions.eq("ia1.attributeValue", "bar1")))
.add(Restrictions.eq("ia2.name", "foo2"))
.add(Restrictions.eq("ia2.attributeValue", "bar2")))
Not sure how hibernate handles joining on the same property twice explicitly like that, maybe worth trying?
SELECT item FROM Item item JOIN item.attributes attr
WHERE attr IN (:attrList) GROUP BY item
and then in the Java code:
List<ItemAttribute> attrList = new ArrayList<ItemAttribute>();
attrList.add(..); // add as many attributes as needed
...// create a Query with the above string
query.setParameter("attrList", attrList);
Why wouldn't the following work?
return getSession().createCriteria(Item.class)
.createAlias("itemAttributes", "ia")
.add(Restrictions.or()
.add(Restrictions.conjunction()
.add(Restrictions.eq("ia.name", "foo1"))
.add(Restrictions.eq("ia.attributeValue", "bar1")))
.add(Restrictions.conjunction()
.add(Restrictions.eq("ia.name", "foo2"))
.add(Restrictions.eq("ia.attributeValue", "bar2"))))
.list();
That would be (name=foo1 && attributeValue=bar1) OR (name=foo2 && attributeValue=bar2)
I didn't test it, but this is how I should try to solve your problem if I would have to:
Map<String,String> map1 = new TreeMap<String,String>();
map1.put("ia.name","foo1");
map1.put("ia.value","bar1");
Map<String,String> map2 = new TreeMap<String,String>();
map2.put("ia.name","foo2");
map2.put("ia.value","bar2");
return getSession().createCriteria(Item.class)
.createAlias("itemAttributes", "ia")
.add(Restrictions.and()
.add(Restrictions.allEq(map1))
.add(Restrictions.allEq(map2))
)
.list();
Please, let me know if it worked. I think the same should work with or()...
Use LEFT_OUTER_JOIN to prevent "WHERE x = 1 AND x = 2" kind of issue
CreateAlias("itemAttributes", "ia", JoinType.LEFT_OUTER_JOIN)
Related
Currently we have a class that looks something like that (depersonalised and non-relevant parts removed):
#Entity
#Table(name = "MAIN_TABLE")
public class MainTable extends AbstractTable {
#OneToMany(fetch = FetchType.LAZY, mappedBy = "mainTable")
#OrderBy("CREATED_ON DESC")
private Set<MainTableState> states;
...
public MainTableState getActiveState(){
if(this.states == null || this.states.isEmpty()){
return null;
}
MainTableState latest = states.iterator().next();
// The reason we use this for-loop, even though we have the #OrderBy annotation,
// Is because we can later add states to this list, which aren't automatically ordered
for(MainTableState state : states){
if(state.getCreatedOn() != null && latest.getCreatedOn() != null &&
state.getCreatedOn().after(latest.getCreatedOn()){
latest = state;
}
}
return latest;
}
...
}
So currently it will retrieve all MainTableStates from the DB by default, and if we need the activeState we use the for-loop method. Obviously this is pretty bad for performance. Currently we don't use this list at all (the purpose was to have a history of states, but this has been postponed to the future), but we do use the getActiveState() method quite a bit, mostly to show a String inside of the MainTableState-class in the UI.
In addition, even if we would always use a TreeSet and keep it sorted so we won't need the loop but only need states.iterator().next() instead, it will still initialize the list of states. With some heavy performance testing we had more than 1 million MainTableState-instances when it crashed with an java.lang.OutOfMemoryError: GC overhead limit exceeded.
So, we want to change it to the following instead:
#Entity
#Table(name = "MAIN_TABLE")
public class MainTable extends AbstractEntity {
#???
private MainTableState activeState;
...
public MainTableStates getActiveState(){
return activeState;
}
...
}
So, my question, what should I put at the #??? to accomplish this? I'm assuming I need the #Formula or something similar, but how can I say to hibernate it should return a MainTableState object? I've seen #Formula being used with MAX for a date, but that was to get that date-property, not get an entire object based on that max date.
After #user2447161's suggestion I've used a #Where-annotation, which does indeed help to reduce the Collection size to 1 (sometimes), but I have two more related questions:
How to use #OnToMany and #Where but get a single object, instead of a list of objects of size one? Is this even possible? Here in a answer from December 2010 it is stated it isn't. Has this been fixed somewhere in the last six years?
How to deal with the random alias in the where clause? I could do something like this:
#OneToMany(fetch = FetchType.LAZY, mappedBy = "mainTable")
#Where(clause = "CREATED_ON = (SELECT MAX(mts.CREATED_ON) FROM MAIN_TABLE_STATES mts WHERE mts.FK_MAIN_ID = ???.MAIN_ID)")
private Set states; // TODO Get single object instead of collection with size 1
The problem with is that ??? is a random alias generated by hibernate (sometimes it's this_, sometimes it's something along the lines of mainTable_1_, etc.). How to set this alias for the entire query to the DB to use it here? I also tried MAIN_TABLE.MAIN_ID instead which doesn't work, and with no alias it also doesn't work because it uses the MainTableState-alias instead of MainTable-alias (like this below).
from
MAIN_TABLE this_
left outer join
MAIN_TABLE_STATUSES mainstat2_
on this_.main_id=mainstat2_.fk_main_id
and (
mainstat2_.created_on = (
SELECT
MAX(mts.created_on)
FROM
MAIN_TABLE_STATUSES mts
WHERE
-- mainstat2_.main_id should be this_.main_id instead here:
mts.fk_main_id = mainstat2_.main_id
)
)
Well, regarding your question #2, as it looks like you need a quick solution with minimal impact in your existing code, this may be acceptable: you can use an Interceptor to deal with the alias and generate the right sql statement. Do this:
use a unique string as alias placeholder in your #Where clause, for instance:
...WHERE mts.FK_MAIN_ID = ${MAIN_TABLE_ALIAS}.MAIN_ID...
if your application doesn't have one yet, create an Interceptor class extending EmptyInterceptor and configure it as a SessionFactory interceptor
override the onPrepareStatement method to replace the placeholder with the alias found after 'from MAIN_TABLE' with something like this:
public String onPrepareStatement(String sql) {
String modifiedSql = sql;
if (sql.contains("${MAIN_TABLE_ALIAS}")) {
String mainTableAlias = findMainTableAlias(sql);
modifiedSql = sql.replace("${MAIN_TABLE_ALIAS}", mainTableAlias);
}
return modifiedSql;
}
Be aware that this method will be called for every sql statement that hibernate generates in your application.
Additionaly, your #Where clause only works properly when a join is used, so you should set the fetch mode explicitly adding
#Fetch(FetchMode.JOIN)
to the states property to avoid that hibernate may use the select mode.
I would like to create criteria or add restriction to my existing criteria (i think it has to be criteria) that will ignore "-" when searching for data's.
E.g I'm searching for number "888" and I would like to get "8-8-8" too.
I have bean that contains my number field
A.java
#Data
public class A {
[...]
#Length(max = 16)
#Column(length = 16)
private String number;
}
And here's my criteria
public Pair<Long, List<A>> filter(CompoundFilter filter, CompoundSort sort, int start, int count) {
Criteria criteria = getSession().createCriteria(A.class, "a_alias");
...
}
What should I add to criteria to achieve my goal?
My other idea is to create some hidden field (by hidden i mean the one that i wont use on UI, only fill with datas) that will hold "transformed" values and when filtering number i will filter on this field
try using regex ,the pattern would look like "[0-9]-[0-9]- ... [0-9]" , here instead of 0-9 build the pattern with your desired number. If i understood your question correctly then this could be an answer.
Please be patient with the newbie question as I'm trying to learn MyBatis and java at the same time. I have an application in which I need to use threadsafe variables. Based on some research and my ideas of how the application will be used, I've settled on a CopyOnWriteArrayList over a Vector.
When I call a selectList from the mybatis sql session, is there any way to tell it to create an CopyOnWriteArrayList as its return rather than an ArrayList? Admittedly, my code to configure this is two lines instead of one, but something in me says that there's got to be a better way and/or I'm not the first one to have encountered this.
List<Team> teams = session.selectList("getTeamsByGameID", gameID);
List<Team> arrayListReturn = new CopyOnWriteArrayList<Team>(teams);
return arrayListReturn;
Thanks in advance,
I know of two ways to handle this.
Option 1: Use a Mapper class and specify the type of List to return there.
Define a Mapper interface:
public interface TeamMapper {
CopyOnWriteArrayList<Team> getTeamsByGameID();
}
Your mapper xml file stays the same. The code to do the query changes to:
TeamMapper m = session.getMapper(TeamMapper.class);
List<Team> lt = m.getTeamsByGameID();
System.out.println(lt.getClass());
//=> prints out "class java.util.concurrent.CopyOnWriteArrayList"
Option 2: Create a ResultHandler and pass that into the session.select() method.
Here you use the ResultHandler interface. That interface requires you to override one method, handleResult, which is given each result that comes back from the database as the query is in progress.
In your case, your ResultHandler would look something like this:
public class TeamResultHandler implements ResultHandler {
private List<Team> teams = new CopyOnWriteArrayList<Team>();
#Override
public void handleResult(ResultContext rc) {
countries.add((Team) rc.getResultObject());
}
// provide a getter so you can retrieve it when finished
public List<Team> getTeamList() {
return teams;
}
}
Instead of using selectList as you do above, you would now use the session.select(String, ResultHandler), like so:
TeamResultHandler rh = new TeamResultHandler();
session.select("getTeamsByGameID", rh);
List<Team> lt = rh.getTeamList();
return lt;
This solution is more verbose than your solution (requires an extra class and three lines in your query code, rather than 2), but it only creates one List, rather than two, so you'll have to decide which fits your needs best.
In addition, ResultHandlers can be useful for additional things - making sure results are sorted in a certain way or filtered or something else, in case you need that.
I've a class -
public class Data implements Identifiable{
private Integer id;
public Integer getId(){
return id;
}
}
now I've two collections-
List<Data> data1 = // few hundred Objects
Set<Integer> dataIds = // few object ids
I would like to extract the List<Data> from data1 which has ids in dataIds
How should be my approach? I'va guava in my classpath so can go with guava's Functional approach if comparable in performance/efficiency .
Unless all you want to do is iterate through the result once or you need a reusable live filtered view, you probably want a non-view list containing the matches. Creating a List or Set to store the result and then iterating through the data list and adding matches is a perfectly good approach and easy to understand!
List<Data> result = Lists.newArrayList();
for (Data data : data1) {
if (dataIds.contains(data.getId()))
result.add(data);
}
I see your Data class implements an Identifiable interface. Given that, you could create a Function<Identifiable, Integer> that gets the ID... Identifiables.getIdFunction() or something. This is nice because it'd likely be useful in various other places (I talk about that approach in a blog post here). With that in place, doing this with Guava would be fairly simple as well:
Predicate<Identifiable> predicate = Predicates.compose(
Predicates.in(dataIds), Identifiables.getIdFunction());
List<Data> filtered = Lists.newArrayList(Iterables.filter(data1, predicate));
This is basically functionally equivalent to the first example, but seems like it'd be harder to understand. Since there isn't any clear benefit to doing this (unlike in a situation where you want to just use the live view), my recommendation would be to just go with the first.
How about
Collections2.filter(
data1,
new Predicate<Data>() {
public boolean apply(Data d) {
return dataIds.contains(d.getId());
}
}
)
p.s. remember not to overcomplicate things, unless truly necessary.
With LambdaJ you could write:
List<Data> result = extract(data1, on(Data.class).getId());
I have two objects that look something like this:
public class Foo{
List<Bar> bars;
String name;
}
public class Bar {
String value;
}
I would like to use the Hibernate Criteria API to make an OR query that involves both of the properties of Foo. Specifically, I want to select all the Foos that have the name "somevalue" OR have a Bar in their bars collection that has a value of "anothervalue".
I know how to do these independently:
createCriteria(Foo.class).add(Restrictions.eq("name","somevalue"));
and
createCriteria(Foo.class).createCriteria("bars").add(Restrictions.eq("value","anothervalue"));
(taken from here)
but when I try to put these on either side of a Restrictions.or() statement, it gives me a compile error. So, my question is: is this even possible using Criteria, or will I have to use HQL or plain SQL?
Just from the top of my head, would this work?
Criteria criteria = session.createCriteria(Foo.class);
criteria.createAlias("bars", "bar");
String value = "somevalue";
criteria.add(Restrictions.or(Restrictions.eq("name", value), Restrictions.eq("bar.name", value)));