Introduction
Hello. I'm currently building a FacebookFeedParser, and at the moment I'm trying to build a method in a Controller that lists the Facebook pages with the best post/like ratio from today to the user. To do that, I have built the following query in pure SQL
SELECT pa.facebook_name, COUNT(po.id) AS postCount, SUM(likes) AS likes, SUM(likes)/COUNT(po.id) AS likesPerPost
FROM facebook_posts po
INNER JOIN facebook_pages pa ON pa.id = po.facebook_page_id
WHERE CONVERT_TZ(`facebook_created_time`, 'UTC', 'Europe/Berlin') > '2015-07-16 00:00:00'
GROUP by facebook_page_id
ORDER BY SUM(likes)/COUNT(po.id) DESC;
What I now wanted to do, is to transform this query into Laravel/Eloquent.
Current state
I have the following classes in my project
Controllers: PageController, PostController
Models: FacebookPost, FacebookPage, BaseModel
The FacebookPost and FacebookPage model are both defining their relations like this
FacebookPage
/**
* Defines the relation between FacebookPage and FacebookPost
* One page can have multiple posts
*
* #see FacebookPage
* #see FacebookPost
*
* #return \Illuminate\Database\Eloquent\Relations\HasMany
*/
public function posts()
{
return $this->hasMany(FacebookPost::class);
}
FacebookPost
/**
* Defines the association of this object with FacebookPage
* One facebook_post belongs to one facebook_page
*
* #return \Illuminate\Database\Eloquent\Relations\BelongsTo
*/
public function page()
{
return $this->belongsTo(FacebookPage::class, 'facebook_page_id');
}
In the BaseModel, I have defined a scope, which will be used multiple times in the project
/**
* Query scope to get the database values for today
*
* #param $query
* #return mixed
*/
public function scopeToday($query)
{
return $query->whereRaw('CONVERT_TZ(`'. $this->createDateColumn .'`, "UTC", "'. env('APP_TIMEZONE') .'") > "' . Carbon::today()->toDateTimeString() . '"');
}
And this is the query I've built in order to get those posts, with the filters
$posts = App\Models\FacebookPost::with('page')
->selectRaw('COUNT(id) AS postCount, SUM(likes) AS likes, SUM(likes)/COUNT(id) AS likesPerPost')
->today()
->groupBy('facebook_page_id')
->orderByRaw('SUM(likes)/COUNT(id) DESC')
->get();
Problem
The problem I'm currently having, is, that, when I try to rebuild the above query, I'm not getting all fields I want. As soon as I add an select to the Builder, the relations array, with the index page is null. If I ommit the select method, I'm getting the FacebookPage, but I want to have those specific fields
Now I'm getting an object. I guess this is because I'm using the Eloquent Builder right? Isn't it somehow possible to only get the fields I want to have? The result I'm expecting should look like this (per row)
facebook_name | postCount | likes | likesPerPost
McDonalds 1000 500 0.5
I also tried it like this
$posts = App\Models\FacebookPost::with(['page' => function($query) {
$query->select('facebook_name');
}])
->selectRaw('COUNT(id) AS postCount, SUM(likes) AS likes, SUM(likes)/COUNT(id) AS likesPerPost')
->today()
->groupBy('facebook_page_id')
->orderByRaw('SUM(likes)/COUNT(id) DESC')
->get();
Would I need to use the DB class instead of Eloquent? Or what would be the best solution for this problem?
Alternative solution
$pages = DB::table('facebook_posts')
->select(DB::raw('facebook_pages.facebook_name, COUNT(facebook_posts.id) AS postCount, SUM(likes) AS likes, ROUND(SUM(likes)/COUNT(facebook_posts.id)) AS likesPerPost'))
->join('facebook_pages', 'facebook_posts.facebook_page_id', '=', 'facebook_pages.id')
->whereRaw('CONVERT_TZ(`'. $this->createDateColumn .'`, "UTC", "'. env('APP_TIMEZONE') .'") > "' . Carbon::today()->toDateTimeString() . '"')
->groupBy('facebook_page_id')
->orderByRaw('ROUND(SUM(likes)/COUNT(facebook_posts.id)) DESC')
->get();
This, however, works. Would this be the correct solution for my use case? I'm just asking if it even make sense to use Eloquent here, since I'm not really trying to get an object, but data from multiple sources.
If you want with('page') to work when you specify the list of fields to fetch using select() you need to make sure that the foreign key is in that list, otherwise Laravel is not able to fetch related models. So in your case make sure that page_id is fetched from the database.
Related
I used to get my data using this command so the first array is for find variable and the second array is for the order.
So I need to order with specific column:name of test(that is an object that has many columns: id/name ) But I dont know how !! when I enter _test.name I get unrecognized field
->findBy(
array('_id' => $id),
array('_test' =>'ASC')
);
class model1
{
private $_idModel1;
/**
*
* #ORM\ManyToOne()
* #ORM\JoinColumns({
* #ORM\JoinColumn(name="id1",
* referencedColumnName="id2")
* })
*/
private $_test;
}
You cannot order using a relation with the findBy, but you can do it creating a query builder:
$model1Repository->createQueryBuilder('model1')
->leftJoin('model1._test', 't')
->where('model1._id = :id')
->setParameter('id', $id)
->orderBy('t.name', 'ASC');
The code has not been tested, you should adapt it to your code.
It creates a query builder over the model1 class , then joins it with your ManyToOne relation, filters via _id and then orders the result by the _test relation using the name field.
Aslo you need to declare in the #ManyToOne the target entity (https://www.doctrine-project.org/projects/doctrine-orm/en/2.8/reference/association-mapping.html#many-to-one-unidirectional)
#ManyToOne(targetEntity=TheNameOfTheTestClass)
Think of this class:
class Person {
/**
* #var ArrayCollection
*
* #ORM\ManyToMany(targetEntity="Person", fetch="EXTRA_LAZY")
* #ORM\OrderBy({"name" = "ASC"})
*/
public $friends;
/**
*
* #var Person
* #ORM\ManyToOne(targetEntity="Person")
*/
public $bestFriend;
}
I have to iterate a lot over all Persons, so I'd like to fetch join them all at once.
To save memory, I have to do this partially.
So what I can do is:
$this->em->createQuery('SELECT partial p.{id, name, bestFriend} FROM Person p')->getResult();
This is cool, after this query, all persons are in the UoW, and I can traverse the graph via $aPersion->bestFriend->bestFriend without creating an additional query to the DB, since all Persons are in memory.
However, this does not work with the ToMany association. Adding friends to the partial select gives an error. If I want to iterate over all friends, this will first create a query to the join table...
How can I realise the full hydration of the friends-ToMany-assotiation with one query? Maybe a second query could help? Or a clever join clause?
Thanks in advance!
I would create a query in PersonRepository.php with a leftJoin and a addSelect like so:
$qb = $this->em->getRepository('App:Person')
->createQueryBuilder('p')
->leftJoin('p.friends', 'friends')
->select('partial p.{id, name, bestFriend}'}
->addSelect('partial friends.{id, name}') // Retrieve what you want here
->getQuery()->getResult();
return $qb;
I have not tested this, but believe it should work.
#DirkJFaber your answer was right,
in terms of DQL here is my solution:
$this->em->createQuery('
SELECT partial p.{id, name, bestFriend}, f FROM Person p JOIN f.friends f')->getResult();
What I need :
I'm building an API that returns users and some relations : I have an entity called "User" which has a lot of relationships. Let's take the "comments" as example :
/**
* #ORM\OneToMany(targetEntity="Comments", mappedBy="idClient", cascade={"persist"})
*/
protected $comments;
In some cases, the client wants to get the user data and the comments data in the same query (by adding "comments" to the "include query param), and wants to sort the comments in a specific order. This order is provided by the client in the query params. In this example, the comments must be sorted by id ASC.
/api/users?include=comments&sort=comments.id
Note that order ASC is implicit in that case.
I have a search() function that build the query :
$qb = $this->createQueryBuilder($this->elementName);
/* SELECTs */
$selects = $this->getSelects($params);
foreach($selects as $select) {
$qb->addSelect($select);
}
/* WHEREs */
$wheres = $this->getWheres($params);
foreach($wheres as $where) {
$qb->andWhere($where);
}
/* ORDER BY */
foreach($sortBy as $column => $order) {
$qb->addOrderBy($column, $order);
}
/* LIMIT and OFFSET */
$qb->setFirstResult($offset)
->setMaxResults($limit);
$query = $qb->getQuery();
$results = $query->getResult();
This function is called to get the primary data of the request : the users data. Then, the users are transformed by a UserTransformer, in order to answer the client in a specific format (JSONAPI).
The relationships (as comments) are called later by querying the entity is the object transformer :
$comments = $user->getComments(); // Returning $this->comments in the User class.
return $this->collection($comments, new CommentsTransformer()); // sends the $comments data to the CommentsTransformer.
What I tried
I tried addOrderBy() to the query builder but I get an error because the DQL does not contains any association named comments.id :
Doctrine\ORM\Query\QueryException: [Semantical Error] line 0, col 110 near 'id ASC': Error: Class Foo\Users has no field or association named comments.id
Here is the DQL :
SELECT e FROM Foo\Users u WHERE [...] ORDER BY u.comments.id ASC
Is there any way I can "see" the comments properties and sort the comments on them in my Query ?
Or is there any way I can inject the sort order in my Users class so it can retrieve the comments data in that dynamical order ? like using $user->getComments($sortBy) and then catch the $sortBy in my Users class (or preferably on my entity mother class) and alter the build-in Doctrine request to add my sorting order ?
PS : sorry for (probably) bad english, it's not my mother tongue.
apply criteria in your getComments function like
use Doctrine\Common\Collections\Criteria;
public function getComments()
{
$criteria = Criteria::create()
->orderBy(['id' => Criteria::ASC]);
return $this->comments->matching($criteria);
}
I have an Alert Class with some data in it. I then have an Availability class. In my Availability class I have
/**
* #var \Nick\AlertBundle\Entity\Alert
*
* #ORM\ManyToOne(targetEntity="Nick\AlertBundle\Entity\Alert")
* #ORM\JoinColumns({
* #ORM\JoinColumn(name="availability_alert_id", referencedColumnName="id")
* })
*/
private $availabilityAlert;
So this is a foreign key back to my Alert class, linked to my Alerts class id.
I am now doing some work on the Availability data, so I have the DQL query
public function getAlertAvailability($id)
{
return $this->getEntityManager()
->createQuery(
'SELECT a.id, a.classLetter, a.flightNumber, a.alertPseudo, a.availability, a.lastUpdated
FROM NickAlertBundle:Availability a
WHERE a.availabilityAlert = :id
ORDER by a.classLetter, a.lastUpdated'
)
->setParameter('id', $id)
->getResult();
}
The way I call that is like this
public function getAvailabilityData(){
$alerts = $this->em->getRepository('NickAlertBundle:Alert')->getActiveAlertIds();
if (!$alerts) {
echo "No Availability";
}
foreach($alerts as $alert){
$alertId = (int)$alert['id'];
$allAvailability = $this->em->getRepository('NickAlertBundle:Availability')->getAlertAvailability($alertId);
}
}
So I essentially get all my active Alerts Ids, and then pass this to get my Availability for these individual Alerts.
Now I have a couple of problems.
Firstly, in the DQL query I make, I need to also get something from my Alert table (a field called command). How would I do a join in this query to get this piece of data?
Secondly, with the data that is returned, how do I access availabilityAlert in my Twig file?
UPDATE
Attempt at join
public function getAlertAvailability()
{
return $this->getEntityManager()
->createQuery(
'SELECT a.id, a.classLetter, a.flightNumber, a.alertPseudo, a.availability, a.lastUpdated, u.searchCommand
FROM NickAlertBundle:Availability a
JOIN a.availabilityAlert u
ORDER BY a.classLetter, a.lastUpdated'
)
->getResult();
}
Doctrine will load that entity as a proxy (for lazy loading) when the Availability entity is loaded.
You can access these via a normal getter / property access, but they will typically be lazy loaded by Doctrine. You can have them joined via a DQL query and the property will be hydrated with all the linked entities already loaded, see Improving Performance with a Join.
You can then access those associated entities in Twig as any other property.
I have a Car entity with a many-to-one relationship with an entity Owner. If I select all cars, Doctrine does one query on the Car table, and subsequently one query on the Owner table for each car. So fetching N cars becomes N+1 queries instead of a single JOIN query between the Car and Owner tables.
My entities are as follows:
/** #Entity */
class Car {
/** #Id #Column(type="smallint") */
private $id;
/** #ManyToOne(targetEntity="Owner", fetch="EAGER")
#JoinColumn(name="owner", referencedColumnName="id") */
private $owner;
public function getId() { return $this->id; }
public function getOwner() { return $this->owner; }
}
/** #Entity */
class Owner {
/** #Id #Column(type="smallint") */
private $id;
/** #Column(type="string") */
private $name;
public function getName() { return $this->name; }
}
If I want to list the cars with their owners, I do:
$repo = $em->getRepository('Car');
$cars = $repo->findAll();
foreach($cars as $car)
echo 'Car no. ' . $car->getId() .
' owned by ' . $car->getOwner()->getName() . '\n';
Now this all works very well, apart from the fact that Doctrine issues a query for each car.
SELECT * FROM Car;
SELECT * FROM Owner WHERE id = 1;
SELECT * FROM Owner WHERE id = 2;
SELECT * FROM Owner WHERE id = 3;
....
Of course I'd want my query log to look like this:
SELECT * FROM Car JOIN Owner ON Car.owner = Owner.id;
Whether I have fetch="EAGER" or fetch="LAZY" doesn't matter, and even if I make a custom DQL query with JOIN between the two entities, $car->getOwner() still causes Doctrine to query the database (unless I use EAGER, in which case $repo->findAll() causes all of them).
Am I just too tired here, and this is the way it is supposed to work - or is there a clever way to force Doctrine to do the JOIN query instead?
At least in 1.x Doctrine if you wanted to query for the related objects, you had to use DQL. For your case, the DQL query would look something like this:
//Assuming $em is EntityManager
$query = $em->createQuery('SELECT c, o FROM Car c JOIN c.owner o');
$cars = $query->execute();
Run first a DQL query where you select all the cars joined (DQL JOIN) with the owner. Put the owner in the select().
// preload cars
$qb = $em->createQueryBuilder()
->select('car, owner')
->from('\Entity\Car', 'car')
->leftJoin('c.owner', 'owner');
$query = $qb->getQuery();
// the following seems not needed, but I think it depends on the conf
$query->setFetchMode("\Entity\Car", "owner", "EAGER");
$query->execute(); //you don't have to use this result here, Doctrine will keep it
Doctrine 2 will then perform a JOIN (normally faster as it requires less db queries depending on the number of records).
Now launch your foreach, Doctrine will find the entities internally and it won't run single queries when you need the owner.
Monitor the number of queries first/after each change (eg. mysql general log)
Your query...
$car->getOwner() // "go and fetch this car's owner"
... is in a foreach loop so it will certainly issue the query several times.
If you're writing custom DQL to deal with this, $car->getOwner() shouldn't feature in this at all. This is a function of the Car class. The custom DQL you would write would mimick the exact SQL query you point out and get your join done efficiently.