LINQ: Split Where OR conditions - sql

So I have the following where conditions
sessions = sessions.Where(y => y.session.SESSION_DIVISION.Any(x => x.DIVISION.ToUpper().Contains(SearchContent)) ||
y.session.ROOM.ToUpper().Contains(SearchContent) ||
y.session.COURSE.ToUpper().Contains(SearchContent));
I want to split this into multiple lines based on whether a string is empty for example:
if (!String.IsNullOrEmpty(Division)) {
sessions = sessions.Where(y => y.session.SESSION_DIVISION.Any(x => x.DIVISION.ToUpper().Contains(SearchContent)));
}
if (!String.IsNullOrEmpty(Room)) {
// this shoudl be OR
sessions = sessions.Where(y => y.session.ROOM.ToUpper().Contains(SearchContent));
}
if (!String.IsNullOrEmpty(course)) {
// this shoudl be OR
sessions = sessions.Where(y => y.session.COURSE.ToUpper().Contains(SearchContent));
}
If you notice I want to add multiple OR conditions split based on whether the Room, course, and Division strings are empty or not.

There are a few ways to go about this:
Apply the "where" to the original query each time, and then Union() the resulting queries.
var queries = new List<IQueryable<Session>>();
if (!String.IsNullOrEmpty(Division)) {
queries.Add(sessions.Where(y => y.session.SESSION_DIVISION.Any(x => x.DIVISION.ToUpper().Contains(SearchContent))));
}
if (!String.IsNullOrEmpty(Room)) {
// this shoudl be OR
queries.Add(sessions.Where(y => y.session.ROOM.ToUpper().Contains(SearchContent)));
}
if (!String.IsNullOrEmpty(course)) {
// this shoudl be OR
queries.Add(sessions.Where(y => y.session.COURSE.ToUpper().Contains(SearchContent)));
}
sessions = queries.Aggregate(sessions.Where(y => false), (q1, q2) => q1.Union(q2));
Do Expression manipulation to merge the bodies of your lambda expressions together, joined by OrElse expressions. (Complicated unless you've already got libraries to help you: after joining the bodies, you also have to traverse the expression tree to replace the parameter expressions. It can get sticky. See this post for details.
Use a tool like PredicateBuilder to do #2 for you.

.Where() assumes logical AND and as far as I know, there's no out of box solution to do it. If you want to separate OR statements, you may want to look into using Predicate Builder or Dynamic Linq.

You can create an extension method to conditionally apply the filter:
public static IQueryable<T> WhereIf<T>(
this IQueryable<T> source, bool condition,
Expression<Func<T, bool>> predicate)
{
return condition ? source.Where(predicate) : source;
}
And use it like this:
using static System.String;
...
var res = sessions
.WhereIf(!IsNullOrEmpty(Division), y => y.session.SESSION_DIVISION.ToUpper().Contains(SearchContent))
.WhereIf(!IsNullOrEmpty(Room), y => y.session.ROOM.ToUpper().Contains(SearchContent))
.WhereIf(!IsNullOrEmpty(course), y => y.session.COURSE.ToUpper().Contains(SearchContent)));

Related

Yii2 - hasMany relation with multiple columns

I have a table message_thread:
id
sender_id
recipient_id
I want to declare a relation in my User model that will fetch all message threads as follows:
SELECT *
FROM message_thread
WHERE sender_id = {user.id}
OR recipent_id = {user.id}
I have tried the following:
public function getMessageThreads()
{
return $this->hasMany(MessageThread::className(), ['sender_id' => 'id'])
->orWhere(['recipient_id' => 'id']);
}
But it generates an AND query. Does anyone know how to do this?
You cannot create regular relation in this way - Yii will not be able to map related records for eager loading, so it not supporting this. You can find some explanation int this answer and related issue on GitHub.
Depending on use case you may try two approach to get something similar:
1. Two regular relations and getter to simplify access
public function getSenderThreads() {
return $this->hasMany(MessageThread::className(), ['sender_id' => 'id']);
}
public function getRecipientThreads() {
return $this->hasMany(MessageThread::className(), ['recipient_id' => 'id']);
}
public function getMessageThreads() {
return array_merge($this->senderThreads, $this->recipientThreads);
}
In this way you have two separate relations for sender and recipient threads, so you can use them directly with joins or eager loading. But you also have getter which will return result ofboth relations, so you can access all threads by $model->messageThreads.
2. Fake relation
public function getMessageThreads()
{
$query = MessageThread::find()
->andWhere([
'or',
['sender_id' => $this->id],
['recipient_id' => $this->id],
]);
$query->multiple = true;
return $query;
}
This is not real relation. You will not be able to use it with eager loading or for joins, but it will fetch all user threads in one query and you still will be able to use it as regular active record relation - $model->getMessageThreads() will return ActiveQuery and $model->messageThreads array of models.
Why orOnCondition() will not work
orOnCondition() and andOnCondition() are for additional ON conditions which will always be appended to base relation condition using AND. So if you have relation defined like this:
$this->hasMany(MessageThread::className(), ['sender_id' => 'id'])
->orOnCondition(['recipient_id' => new Expression('id')])
->orOnCondition(['shared' => 1]);
It will generate condition like this:
sender_id = id AND (recipent_id = id OR shared = 1)
As you can see conditions defined by orOnCondition() are separated from condition from relation defined in hasMany() and they're always joined using AND.
For this query
SELECT *
FROM message_thread
WHERE sender_id = {user.id}
OR recipent_id = {user.id}
You Can use these
$query = (new \yii\db\Query)->from("message_thread")
$query->orFilterWhere(['sender_id'=>$user_id])->orFilterWhere(['recipent_id '=>$user_id]);

Yii 2 can rewrite WHERE condition from model?

I have some where condition in my model .
Its check is field active or no.
Now I need to write a join relation. But I need to remove where condition. Is it possible?
My model.
...
public static function find() {
return (new AssetgroupsQuery(get_called_class()))->active();
}
My relation
public function getAssetgroup(): \app\models\AssetgroupsQuery {
return $this->hasOne(Assetgroups::class, ['asg_id' => 'ass_group'])->andOnCondition(['asg_active' => '1'])
->viaTable('assets', ['ass_id' => 'log_ass_id',]);
}
I need to got all active assets and join, if asset is empty I need to got null fields, but
model where condition added to my current sql query and remove all fields which assets are null.
I try to add some where Condition to remove old where, but it don't work.
Can you help me?
You can reset existing conditions by using where(null).
On relation level:
public function getAssetgroup(): \app\models\AssetgroupsQuery {
return $this->hasOne(Assetgroups::class, ['asg_id' => 'ass_group'])
->andOnCondition(['asg_active' => '1'])
->where(null)
->viaTable('assets', ['ass_id' => 'log_ass_id',]);
}
Or directly on join:
$query = MyModel::find()
->joinWith([
'assetgroup' => function (ActiveQuery $query) {
$query->where(null);
},
])

Use of Cast Projection inside a Concat Projection

I have the following query:
var result = _session.QueryOver<Entity>()
.Where(e => e.Property == value)
.SelectList(list => list
.Select(f => Projections.Concat("prefix-", e.BigIntProperty)).WithAlias(() => alias.Whatever)
...
)
.TransformUsing(Transformers.AliasToBean<Model>())
.Future<Model>();
The problem is that Projections.Concat() accepts only strings and since e.BigIntProperty is not, the above doesn't compile. Is there a way to cast e.BigIntProperty to string?
I tried something like the following, which doesn't work either:
.Select(f => Projections.Concat("prefix-", Projection.Cast(NHibernateUtil.String, e.BigIntProperty))).WithAlias(() => alias.Whatever)
, since Projections.Cast returns an IProjection and not a string.
Projections.Cast seems terribly limited in that it can't take arbitrary Projections. Luckily you can easily create your own custom projection that enables you to do that:
public static class CustomProjections
{
public static IProjection Concat(params IProjection[] projections)
{
return Projections.SqlFunction(
"concat",
NHibernateUtil.String,
projections);
}
}
Then, you'll be able to use your CustomProjections class like this:
var result = _session.QueryOver<Entity>()
.Where(e => e.Property == value)
.SelectList(list => list
.Select(CustomProjections.Concat(
Projections.Constant("prefix-"),
Projections.Cast(
NHibernateUtil.String,
Projections.Property<Entity>(e => e.BigIntProperty))))
.WithAlias(() => alias.Whatever)
...
)
.TransformUsing(Transformers.AliasToBean<Model>())
.Future<Model>();
I've already accepted Andrew's answer, but just for reference, you could use Projections.SqlFunction("concat", ...) directly which solves the whole issue since it can take IProjection's as arguments and not only string.
var result = _session.QueryOver<Entity>()
.Where(e => e.Property == value)
.SelectList(list => list
.Select(Projections.SqlFunction("concat",
NHibernateUtil.String,
Projections.Constant("prefix-"),
Projections.Cast(NHibernateUtil.String, Projections.Property<Entity>(e => e.BigIntProperty))))
.WithAlias(() => alias.Whatever)
...
)
.TransformUsing(Transformers.AliasToBean<Model>())
.Future<Model>();
NOTE: It seems that when calling either Projections.Concat(...) or Projections.SqlFunction("concat", ...), the query that is produced actually uses the + operator, e.g.:
SELECT (a + b) as foo FROM table
instead of:
SELECT concat(a, b) as foo FROM table
Of course, CONCAT is only available from MS SQL Server versions 2012 and above, so this is correct. Possibly the MsSQl2012Dialect could make use of the CONCAT, since CONCAT doesn't require that the arguments are varchar, they might as well be integers.
Unfortunately MsSQl2012Dialect doesn't do that, but it is very easy to build a custom Dialect:
public class CustomMsSql2012Dialect : MsSql2012Dialect
{
protected override void RegisterFunctions()
{
base.RegisterFunctions();
base.RegisterFunction("concat", new VarArgsSQLFunction(NHibernateUtil.String, "concat(", ",", ")"));
}
}
So, if you use version 2012 or above and you declare the above as your Dialect, you can ditch the Projections.Cast(...) part

NHibernate accumulate queryOver conditions

I want to do a sort of filtering chain to filter Receipt objects using queryOver functionality.
The chain can differ in length, according to the parameters user chooses on the screen.
Eventually, I want the chain to run somehow like this:
public IList<Receipt> RunFilters()
{
IQueryOver<Receipt, Receipt> currQuery = NHibernateHelper.Session.QueryOver<Receipt>();
foreach (var item in filters)
{
currQuery = item.RunFilter(currQuery);
}
return currQuery.List();
}
So, the question is - how RunFilter should be defined? I thought it should be
public IQueryOver<Receipt, Receipt> RunFilter(IQueryOver<Receipt, Receipt> prevFilter)
and they I can do filters like
return prevFilter.Where(receipt => receipt.TotalSum > 0);
But I can't do
return prevFilter.JoinQueryOver(v => v.Store).Where(vv => vv.Name.Equals(m_storeName));
Any ideas?
Thanks in advance
Victor
return prevFilter.JoinQueryOver(v => v.Store).Where(vv => vv.Name.Equals(m_storeName));
the above can be written as
Store storeAlias = null;
return prevFilter.JoinAlias(v => v.Store, () => storeAlias).Where(() => storeAlias.Name == m_storeName);
EDIT: fixed equation

NHibernate Multiquery for eager loading without joins

Is it possible to use a multiquery and have two hql queries returning two different sets of entities where one of the sets are used in the other and that the session "fixes" this via the first level cache?
E.g. scenario (a dumb one and it could be solved with joins)
public class Room
{
...
public virtual ISet<Bookings> Bookings {get;set;}
public virtual bool IsAvailible {get;set;}
...
}
public class Booking
{
...
}
After executing a multicriteria with two hql's:
returning all rooms where
IsAvailible = true
returning all bookings having a room that has a room that IsAvailible
when accessing a room from the result and its bookings I want them to be resolved from the second resultset via the firstlevel cache of the session and there by avoiding n+1.
Generally speaking, NHibernate can use the cache to "combine" the results from queries executed through Multiquery. However, it should be noted that this usually only applies to cases where lazy collections are loaded with no restrictions whatsoever.
Examples:
Invoice iAlias = null;
InvoiceDetails idAlias = null;
// Base-Query: get Invoices with certain condition
var invoices = session.QueryOver<Invoice>()
.Where(i => i.Number == "001")
.Future<Invoice>();
// Option 1: this will still cause N+1 if we iterate through invoices,
// because it doesn't know better
var invoicedetails = session.QueryOver<InvoiceDetails>()
.JoinAlias(a => a.Invoice, () => iAlias)
.Where(() => iAlias.Number == "001")
.Future<InvoiceDetails>();
// Option 2: this will still cause N+1 if we iterate through invoices,
// because we limited the possible results using a where-condition
var invoices2 = session.QueryOver<Invoice>()
.Left.JoinAlias(i => i.Details, () => idAlias)
.Where(i => i.Number == "001")
.And(() => idAlias.Quantity > 5)
.Future<Invoice>();
// Option 3: this will work without N+1, because we don't use a filter
// -> NHibernate will use the collection in cache
var invoices3 = session.QueryOver<Invoice>()
.Left.JoinAlias(i => i.Details, () => idAlias)
.Where(i => i.Number == "001")
.Future<Invoice>();
foreach (Invoice i in invoices)
{
int count = i.Details.Count;
}
If we comment out two of the three options and execute the code, we will see that only option 3 will prevent a N+1, the other two will still load the InvoiceDetails for each Invoice in the loop.
Of course this is a very simple example and it is obvious that Option 3 could also be executed without the Base-query and still return the same result, but I hope you get the idea.
In the case where we load two different sets of entities, i.e. the root class is different as in Option 1, this "combining" will most likely not work.
Sorry, if I used QueryOver instead of HQL, but the same rules apply.
Gyus, keep in mind that sometimes you can have similar problems because of
LeftOuterJoin is not set.
.JoinAlias(x => x.Prop, () => propAlias, JoinType.LeftOuterJoin)