yii2 How to order via relation by COUNT? - yii

I have a organizers, which do tournaments where a player can participate.
So I have the tables organizer, tournament, participation, player.
I made a relation in the organizer model that gives me all players who ever payer in a tournament organized by that organizer.
public function getTournaments()
{
return $this->hasMany(Tournament::className(), ['organizer' => 'id'])->where(['hasBeenAudited' => 1]);
}
public function getParticipations()
{
return $this->hasMany(Participation::className(), ['tournament' => 'id'])
->via('tournament')->where(['didShowUp' => 1]);
}
public function getPlayers() {
return $this->hasMany(Player::className(), ['id' => 'player'])
->via('participation');
}
So now I can do $model->players and retrieve all players that have ever played for that organization.
I now would like to order $model->players according to the number of tournaments a player has played for that organization, so that the player who has played the most tournaments for the organization will show up at the top.
How do I do that?

You neet to do 'group by' query. Try something like this
$model->getPlayers()
->with('tournaments')
->select(['organization.name', 'player.name'])
->groupBy(['organization.name', 'player.name'])
->orderBy(['count(tournaments.id)'=>SORT_DESC])
->all();

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

CGridview and Yii Active Record Relation

I have two tables tbl_business and business_contacts of the following structure:
tbl_business
---
business_id (PK)
othercolumns
and
business_contacts
---
contact_id (PK)
business_id
othercolumns
The scenario is that one business row has many contacts. I am using cGridview using gii's CRUD generator and needed to display firstname and lastname from business_contacts (one of multiple possible rows in the table) for each tbl_business record.
As far as I understand, I've updated the relation function in tbl_business's model as:
'businesscontacts' => array(self::HAS_MANY,'BusinessContact','business_id','select' => 'contact_firstname, contact_lastname')
and for the same, a contact relation is defined in the business_contacts' model as:
'contactbusiness' => array(self::BELONGS_TO,'BusinessContact','business_id')
I expected that would work for pulling related records so that I can have something in the grid like, business_id, contact_firstname, contact_lastname , ... otherbusinesstablecolumns .. but I'm only getting blank values under firstname and lastname .. could someone please help me understand the error? :(
So you are trying to display a table of Businesses (tbl_business) using CGridView? And in each Business's row you want to list multiple Contacts (business_contacts)?
CGridView does not support displaying HAS_MANY relations by default. CGridView makes it easy to list which Business a Contact BELONGS_TO (i.e. you can use a column name like contactbusiness.business_id), but not all of the Contacts that are in a business.
You can do it yourself though, by customizing a CDataColumn. (Note: this will not allow you to sort and filter the column, just view. You'll have to do a lot more work in to get those working.)
First, in your Business model, add a method like this to print out all of the contacts:
public function contactsToString() {
$return = '';
foreach ($this->businesscontacts as $contact) {
$return .= $contact->contact_firstname.' '.$contact->contact_firstname.'<br />';
}
return $return;
}
(EDIT: Or do this to print out just the first contact):
public function contactsToString() {
if($firstContact = array_shift($this->businesscontacts)) {
return $firstContact->contact_firstname.' '.$firstContact->contact_firstname;
}
return '';
}
Then make a new column in your grid and fill it with this data like so:
<?php $this->widget('zii.widgets.grid.CGridView', array(
'id'=>'business-grid',
'dataProvider'=>$model->yourDataProviderFunction(),
'columns'=>
'business_id',
array(
'header'=>'Business Contacts', // give new column a header
'type'=>'HTML', // set it to manual HTML
'value'=>'$data->contactsToString()' // here is where you call the new function
),
// other columns
)); ?>
EDIT2: Yet another way of doing this, if you just want to print out ONE of a HAS_MANY relation, would be to set up a new (additional) HAS_ONE relation for the same table:
public function relations()
{
return array(
'businesscontacts' => array(self::HAS_MANY,'BusinessContact','business_id','select' => 'contact_firstname, contact_lastname') // original
'firstBusinesscontact' => array(self::HAS_ONE, 'BusinessContact', 'business_id'), // the new relation
);
}
Then, in your CGridView you can just set up a column like so:
'columns'=>array(
'firstBusinesscontact.contact_firstname',
),
Getting only the first contact could be achieved like this also:
$this->widget('zii.widgets.grid.CGridView', array(
'id'=>'business-grid',
'dataProvider'=>$model->yourDataProviderFunction(),
'columns'=>
'business_id',
//....
array(
'name' => 'contacts.contact_firstname',
'value' => '$data->contacts[0]->contact_firstname', // <------------------------
'type' => 'raw'
);
//....
),

Applying global filter to Entity Framework model

I have the following database schema :
Store
StoreId
Name
...
Order
OrderId
StoreId
....
I have a asp.net mvc backend application with a data repostory like this (simplified):
public IQueryable<Order> GetOrders() {
return storeDB.Orders
.OrderByDescending(o => o.DateDue)
.ThenByDescending(o => o.DateCreated);
}
I have similar code to show sales, employees, products etc ... I want to put a dropdown somewhere in the application to allow me to set a global filter to show only orders, employees, products, etc ... from a given StoreId, if storeId is null I should show all records from all stores (remove the filter) what is the best way to do it ?
Should I set the storeId in the session and filter the data in the controller? should I add a storeId parameter in my datalayer and evalute if it's null or not before applying the filter?
I would go for a List<Int> or List<Guid> or whatever DataType you use for the StoreID and use this list as a parameter in the method. Then you can eventually also show orders for multiple stores, or for a single store or for all stores. And then simply use an if statement to detect of the list contains something or nothing, something like:
public IQueryable<Order> GetOrders(List<Guid> storeIDs)
{
if(storeIDs == null || storeIDs.Count == 0)
{
return storeDB.Orders
.OrderByDescending(o => o.DateDue)
.ThenByDescending(o => o.DateCreated);
}
else
{
return storeDB.Orders
.Where(o => storeIDs.Contains(o.StoreID))
.OrderByDescending(o => o.DateDue)
.ThenByDescending(o => o.DateCreated);
}
}
This is a nice setup according to me. Don't store the filter parameters in a Session or whatever. In the way described above the method can be used from / by multiple functional "places" in your application.

NHibernate many-to-many assocations making both ends as a parent by using a relationship entity in the Domain Model

Entities:
Team <-> TeamEmployee <-> Employee
Requirements:
A Team and an Employee can exist without its counterpart.
In the Team-TeamEmployee relation the Team is responsible (parent) [using later a TeamRepository].
In the Employee-TeamEmployee relation the Employee is responsible (parent) [using later an EmployeeRepository].
Duplicates are not allowed.
Deleting a Team deletes all Employees in the Team, if the Employee is not in another Team.
Deleting an Employee deletes only a Team, if the Team does not contain no more Employees.
Mapping:
public class TeamMap : ClassMap<Team>
{
public TeamMap()
{
// identity mapping
Id(p => p.Id)
.Column("TeamID")
.GeneratedBy.Identity();
// column mapping
Map(p => p.Name);
// associations
HasMany(p => p.TeamEmployees)
.KeyColumn("TeamID")
.Inverse()
.Cascade.SaveUpdate()
.AsSet()
.LazyLoad();
}
}
public class EmployeeMap : ClassMap<Employee>
{
public EmployeeMap()
{
// identifier mapping
Id(p => p.Id)
.Column("EmployeeID")
.GeneratedBy.Identity();
// column mapping
Map(p => p.EMail);
Map(p => p.LastName);
Map(p => p.FirstName);
// associations
HasMany(p => p.TeamEmployees)
.Inverse()
.Cascade.SaveUpdate()
.KeyColumn("EmployeeID")
.AsSet()
.LazyLoad();
HasMany(p => p.LoanedItems)
.Cascade.SaveUpdate()
.LazyLoad()
.KeyColumn("EmployeeID");
}
}
public class TeamEmployeeMap : ClassMap<TeamEmployee>
{
public TeamEmployeeMap()
{
Id(p => p.Id);
References(p => p.Employee)
.Column("EmployeeID")
.LazyLoad();
References(p => p.Team)
.Column("TeamID")
.LazyLoad();
}
}
Creating Employees and Teams:
var employee1 = new Employee { EMail = "Mail", FirstName = "Firstname", LastName = "Lastname" };
var team1 = new Team { Name = "Team1" };
var team2 = new Team { Name = "Team2" };
employee1.AddTeam(team1);
employee1.AddTeam(team2);
var employee2 = new Employee { EMail = "Mail2", FirstName = "Firstname2", LastName = "Lastname2" };
var team3 = new Team { Name = "Team3" };
employee2.AddTeam(team3);
employee2.AddTeam(team1);
team1.AddEmployee(employee1);
team1.AddEmployee(employee2);
team2.AddEmployee(employee1);
team3.AddEmployee(employee2);
session.SaveOrUpdate(team1);
session.SaveOrUpdate(team2);
session.SaveOrUpdate(team3);
session.SaveOrUpdate(employee1);
session.SaveOrUpdate(employee2);
After this I commit the changes by using transaction.Commit().
The first strange thing is that I have to save Teams and Employees instead only one of them (why?!). If I only save all teams or (Xor) all employees then I get a TransientObjectException:
"object references an unsaved
transient instance - save the
transient instance before flushing.
Type: Core.Domain.Model.Employee,
Entity: Core.Domain.Model.Employee"
When I save all created Teams and Employees everything saves fine, BUT the relation table TeamEmployee has duplicate assoications.
ID EID TID
1 1 1
2 2 1
3 1 2
4 2 3
5 1 1
6 1 2
7 2 3
8 2 1
So instead of 4 relations there are 8 relations. 4 relations for the left side and 4 relations for the right side. :[
What do I wrong?
Further questions: When I delete a Team or an Employee, do I have to remove the team or the Employee from the TeamEmployee list in the object model or does NHibernate make the job for me (using session.delete(..))?
You are talking about business logic. It's not the purpose of NHibernate to implement the business logic.
What your code is doing:
You mapped two different collections of TeamEmployees, one in Team, one in Employee. In your code, you add items to both collections, creating new instances of TeamEmployee each time. So why do you expect that NHibernate should not store all these distinct instances?
What you could do to fix it:
You made TeamEmployee an entity (in contrast to a value type). To create an instance only once, you would have to instantiate it only once in memory and reuse it in both collections. Only do this when you really need this class in your domain model. (eg. because it contains additional information about the relations and is actually an entity of its own.)
If you don't need the class, it is much easier to map it as a many-to-many relation (as already proposed by Chris Conway). Because there are two collections in memory which are expected to contain the same data, you tell NHibernate to ignore one of them when storing, using Inverse.
The parent on both ends problem
There is no parent on both ends. I think it's clear that neither the Team nor the Employee is a parent of the other, they are independent. You probably mean that they are both parents of the intermediate TeamEmployee. They can't be parent (and therefore owner) of the same instance. Either one of them is the parent, or it is another independent instance, which makes managing it much more complicated (this is how you implemented it now). If you map it as a many-to-many relation, it will be managed by NHibernate.
To be done by your business logic:
storing new Teams and new Employees
managing the relations and keeping them in sync
deleting Teams and Employees when they are not used anymore. (There is explicitly no persistent garbage collection implementation in NHibernate, for several reasons.)
Looks like you need a HasManyToMany instead of two HasMany maps. Also, there is no need for the TeamEmployeeMap unless you have some other property in that table that needs mapped. Another thing, only one side needs to have the Inverse() set and since you're adding teams to employees I think you need to make the TeamMap the inverse. Having the inverse on one side only will get rid of the duplicate entries in the database.
Maybe something like this:
public class TeamMap : ClassMap<Team>
{
public TeamMap()
{
// identity mapping
Id(p => p.Id)
.Column("TeamID")
.GeneratedBy.Identity();
// column mapping
Map(p => p.Name);
// associations
HasManyToMany(x => x.TeamEmployees)
.Table("TeamEmployees")
.ParentKeyColumn("TeamID")
.ChildKeyColumn("EmployeeID")
.LazyLoad()
.Inverse()
.AsSet();
}
}
public class EmployeeMap : ClassMap<Employee>
{
public EmployeeMap()
{
// identifier mapping
Id(p => p.Id)
.Column("EmployeeID")
.GeneratedBy.Identity();
// column mapping
Map(p => p.EMail);
Map(p => p.LastName);
Map(p => p.FirstName);
// associations
HasManyToMany(x => x.TeamEmployees)
.Table("TeamEmployees")
.ParentKeyColumn("EmployeeID")
.ChildKeyColumn("TeamID")
.Cascade.SaveUpdate()
.LazyLoad()
.AsSet();
HasMany(p => p.LoanedItems)
.Cascade.SaveUpdate()
.LazyLoad()
.KeyColumn("EmployeeID");
}
}
Using this, the delete will delete the TeamEmployee from the database for you.
NHibernate does not allow many-to-many association with parents at both ends.

NHibernate Lambdas joined ordered collection

I have a entity 'Person' a person has a collection of Friends (also Person entities)
I want to get the first 10 Friends of a particular person, ordered by LatestLogin.
My best effort is:
public static IList<Person> GetFriends(Person person, int count)
{
Person personAlias = null;
Person friendAlias = null;
ICriteria criteria = NHibernateSessionManager.Instance.GetSession()
.CreateCriteria(typeof (Person), () => personAlias)
.CreateCriteria(() => personAlias.Friends, () => friendAlias, JoinType.LeftOuterJoin)
.AddOrder(() => friendAlias.LatestLogin, Order.Desc)
.Add<Person>(p => p.ID == person.ID)
.SetMaxResults(count);
return criteria.List<Person>();
}
Which Does grab all the users friends, but they are not ordered by LatestLogin. Any ideas?
I know it may sound weird but the solution is to change the line:
.AddOrder(() => friendAlias.LatestLogin, Order.Desc)
with:
.AddOrder(() => personAlias.LatestLogin, Order.Desc)
You have to see it the other way around in order to understand why this is necessary and not obvious at the beginning.
You are requesting Person objects (personAlias) that have the same 'Parent' friend (friendAlias) with ID == person.ID (.Add(p => p.ID == person.ID)) therefore you need to sort by the personAlias.LatestLogin and NOT the friendAlias.LatestLogin.