Yii2 - hasMany relation with multiple columns - yii

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]);

Related

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);
},
])

How to generate mysql query from relations using via or viaTable relations

I have the following relations in yii2 model
public function getStreamsFormations()
{
return $this->hasMany(StreamsFormations::className(), ['stream_id' => 'id']);
}
public function getFormations()
{
return $this->hasMany(Formations::className(), ['id' => 'formation_id'])->via('streamsFormations');
}
How to create complete mysql query from the relation "getFormations()"?
I need it to use it some where else...
I used ...
$query = $model->getFormations()->createCommnad()->rawSql;
But it skipped the via relation tables from the query.
Relations not have where condition ! they are just join 2 table to theme ! you have to put your condition in find() queries!
if you need more information just let me know .

Returning related fields with Lamba LINQ entity

I have seen many questions related to how you can return a limited set of fields from a single EF entity with anonymous types. My issue is about the opposite of that. I want to return values from related tables along with all of the fields in my entity table:
IQueryable<EntityModels.TBLEFFORT> query = db.TBLEFFORT.AsQueryable();
query = query.Where(a => a.EFFDELETE == "0");
if (!string.IsNullOrEmpty(sGuid))
{
query = query.Where(a => a.TBLEFFORTLINK.Any(b => b.TBLSHS.TBLTACT.Any(c => c.CGUID == sGuid)));
}
query.Select(x => new EffortSearchResult()
{
Efguid = x.EFGUID,
Efstatus = x.EFSTATUS
});
Here my base entity is TBLEFFORT and i query directly against it on the EFFDELETE field and then i query against it with a related table TBLSHS.
However, in my anon return object i only have fields from TBLEFFORT (EFGUID and EFSTATUS). How can i include fields from the related TBLSHS entity in my anonymous return object?
The related tables are lookups so they will be 1:1. I'm not looking to return complex sets based on a FK field in TBLEFFORT
I figured it out. you just extend out in your anon type and dig for what you need from the related tables:
Shguid = x.TBLEFFORTLINK.Select(b => b.TBLSHS.SOMEFIELD).FirstOrDefault(),

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)

Parent-child relationship with LINQ2SQL and POCO objects

I just started learning LINQ2SQL and one of the first things I wanted to try is a simple parent-child hierarchy, but I can't seem to find a good way to do it. I saw some examples here on SO and i've googled, but I couldn't apply them directly, so I'll explain exactly what i'm trying to accomplish.
Lets use the common example with tags.
Database tables: Post-- Post_Tags -- Tags
I've created a simple Post class so I avoid passing Linq2Sql classes around:
public class Post
{
public int Id {get; set;}
public int Title {get; set;}
public IEnumerable<string> Tags {get; set;}
}
I would like to select 5 latest records from the Posts table, get their related tags and return the IList where each Post has their Tags property filled.
Can you show me a concrete Linq2Sql code how could I do that?
I tried:
IList<Post> GetLatest()
{
return (from p in _db.Posts
orderby p.DateCreated descending
select new Post
{
Id = p.Id,
Title = p.Title,
Tags = p.Post_Tags.Select(pt => pt.Tag.Name)
}).Take(5).ToList();
}
This works but duplicates Post records for each Tag record and I have to duplicate property mapping (Id=p.Id, ...) in every method I user. I then tried this approach, but in this case, I have a roundtrip to DB for every tag:
IQueryable<Post> GetList()
{
return (from p in _db.Posts
select new Post
{
Id = p.Id,
Title = p.Title,
Tags = p.Post_Tags.Select(pt => pt.Tag.Name)
});
}
IList<Post> GetLatest()
{
return (from p in GetList()
orderby p.DateCreated descending
select p).Take(5).ToList();
}
If I were doing it in classic ADO.NET, I would create a stored procedure that returns two resultsets. One with Post records and second with related Tag records. I would then map them in the code (by hand, by DataRelation, ORM, etc.). Could I do the same with LINQ2SQL?
I'm really curious to see some code samples on how do you guys handle such simple hierarchies.
And yes, I would really like to return IList<> objects and my custom classes and not queryable Linq to Sql objects, because I would like to be flexible about the data access code if I for example decide to abandon Linq2Sql.
Thanks.
If you create a DataContext, the parent-child relationship is maintained automatically for you.
i.e. If you model the Posts and Tags and their relationship inside a Linq2Sql DataContext, you can then fetch posts like this:
var allPosts = from p in _db.Posts
orderby p.DateCreated descending
select p;
Then you won't have to worry about any tags at all, because they are accessible as a member of the variable p as in:
var allPostsList = allPosts.ToList();
var someTags = allPostsList[0].Post_Tags;
var moreTags = allPostsList[1].Post_Tags;
And then any repeated instance is then automatically updated across entire DataContext until you ask it to SubmitChanges();
IMO, That's the point of an ORM, you don't re-create the model class and maintain the mapping across many places because you want all those relationships managed for you by the ORM.
As for the roundtrip, if you refrain from any code that explicitly requests a trip to the database, all queries will be stored in an intermediate query representation and only when the data is actually needed to continue, is when the query will be translated to sql and dispatched to the database to fetch results.
i.e. the following code only access the database once
// these 3 variables are all in query form until otherwise needed
var allPosts = Posts.All();
var somePosts = allPosts.Where(p => p.Name.Contains("hello"));
var lesserPosts = somePosts.Where(p => p.Name.Contains("World"));
// calling "ToList" will force the query to be sent to the db
var result = lesserPosts.ToList();
How about if you set your DataLoadOptions first to explicitly load tags with posts? Something like:
IList<Post> GetLatest()
{
DataLoadOptions options = new DataLoadOptions();
options.LoadWith<Post>(post => post.Tags);
_db.LoadOptions = options;
return (from p in _db.Posts
orderby p.DateCreated descending)
Take(5).ToList();
}
List<Post> latestPosts = db.Posts
.OrderByDescending( p => p.DateCreated )
.Take(5)
.ToList();
// project the Posts to a List of IDs to send back in
List<int> postIDs = latestPosts
.Select(p => p.Id)
.ToList();
// fetch the strings and the ints used to connect
ILookup<int, string> tagNameLookup = db.Posts
.Where(p => postIDs.Contains(p.Id))
.SelectMany(p => p.Post_Tags)
.Select(pt => new {PostID = pt.PostID, TagName = pt.Tag.Name } )
.ToLookup(x => x.PostID, x => x.TagName);
//now form results
List<Post> results = latestPosts
.Select(p => new Post()
{
Id = p.Id,
Title = p.Title,
Tags = tagNameLookup[p.Id]
})
.ToList();