Doctrine Update query with a sub-query - php

I'm trying to execute a query, similar to the following one, using doctrine dql:
Doctrine_Query::create()
->update('Table a')
->set('a.amount',
'(SELECT sum(b.amount) FROM Table b WHERE b.client_id = a.id AND b.regular = ? AND b.finished = ?)',
array(false, false))
->execute();
But it rises a Doctrine_Query_Exception with the message: "Unknown component alias b"
Is restriction about using sub-queries inside the 'set' clause, can you give me some help?
Thanks in advance.

Years later but may help.
Yes ]
If you need/want/have to, you can use the Querybuilder to execute an update query having a sub select statement, instead of using directly the underlying connection layer.
The idea here is to use the QueryBuilder twice.
Build a select statement to compute the new value.
Build the actual update query to submit to the database, in which you will inject the former select DQL, as you expected in order to issue a single database request.
Example ]
Given an application where users can sell objects. Each transaction involves a buyer and a seller. After a transaction ends, sellers and buyers can leave a review on how went the deal with their counter part.
You might need a User table, a Review table and a Transaction table.
The User table contains a field named rating which will hold the average rating for a user. The Review table stores a transaction id, the author id (who submitted the review), a value (from 0 to 5). Finally, the transaction contains a reference for both the seller and the buyer.
Now let's say you would like to update the average rating for a user after a review has been submitted by the counter part. The update query will compute the average rating for a user and put the result as the value of the User.rating property.
I used the following snippet with Doctrine 2.5 and Symfony3. Since the work is about users, I makes sense to create a new public function called updateRating( User $user) inside the AppBundle\Entity\UserRepository.php repository.
/**
* Update the average rating for a user
* #param User $user The user entity object target
*/
public function updateRating( User $user )
{
// Compute Subrequest. The reference table being Transaction, we get its repository first.
$transactionRepo = $this->_em->getRepository('AppBundle:Transaction');
$tqb = $postRepo->createQueryBuilder('t');
#1 Computing select
$select = $tqb->select('SUM(r.value)/count(r.value)')
// My Review table as no association declared inside annotation (because I do not need it elsewhere)
// So I need to specify the glue part in order join the two tables
->leftJoin('AppBundle:Review','r', Expr\Join::WITH, 'r.post = p.id AND r.author <> :author')
// In case you have an association declared inside the Transaction entity schema, simply replace the above leftJoin with something like
// ->leftJoin(t.reviews, 'r')
// Specify index first (Transaction has been declared as terminated)
->where( $tqb->expr()->eq('t.ended', ':ended') )
// The user can be seller or buyer
->andWhere( $tqb->expr()->orX(
$tqb->expr()->eq('t.seller', ':author'),
$tqb->expr()->eq('t.buyer', ':author')
));
#2 The actual update query, containing the above sub-request
$update = $this->createQueryBuilder('u')
// We want to update a row
->update()
// Setting the new value using the above sub-request
->set('u.rating', '('. $select->getQuery()->getDQL() .')')
// should apply to the user we want
->where('u.id = :author')
// Set parameters for both the main & sub queries
->setParameters([ 'ended' => 1, 'author' => $user->getId() ]);
// Get the update success status
return $update->getQuery()->getSingleScalarResult();
}
Now from the controller
// … Update User's rating
$em->getRepository('AppBundle:User')->updateRating($member);
// …

I'm not sure if there's a restriction on this but I remember fighting with this sometime ago. I eventually got it working with:
$q = Doctrine_Manager::getInstance()->getCurrentConnection();
$q->execute("UPDATE table a SET a.amount = (SELECT SUM(b.amount) FROM table b WHERE b.client_id = a.id AND b.regular = 0 AND b.finished = 0)");
See if that does the trick. Note that automatic variable escaping doesn't get executed with this query as it's not DQL.

Related

Laravel mysql select result

I am trying to add a feature to a website built with Laravel.
There is a table containing vote numbers and user. I want to get the total points a user has in a certain category. I do not have any PHP or Laravel experience but said I would give this a shot.
$votes1 = UserVotes::select ('select vote from user_votes where feedback_id = ? and feedback_type = 1', Auth::user()->id);
This should return an object containing the vote amount. I want to interrogate the the object to check if the vote number is above a certain amount and then do something based on that being the case or not.
if vote > 50{
//do stuff
}
foreach ($votes1 as $vote1) {
echo $vote1->vote;
}
The query should return 1. I have verified this by querying the database, so the problem is with my understanding of Laravel or php. What I am doing wrong?
You don't need to construct your own SQL statement; Eloquent will do that for you.
If your models are set up in the default way, your query would look something like:
$votes = UserVotes::where('feedback_id', Auth::user()->id)
->where('feedback_type', 1)
->get();
You can then iterate over that as normal.
Additionally, if there is a relationship set up with the user model you could do something like
$votes = Auth::user()->votes()
->where('feedback_type', 1)
->get();
Check out the documentation here: http://laravel.com/docs/4.2/eloquent
assuming UsersVotes extends Model, here's how you should do it:
UsersVotes::select('vote')->where('feedback_type', 1)->where('feedback_id', Auth::user()->id)-get();

Yii active record relation limit to one record

I am using PHP Yii framework's Active Records to model a relation between two tables. The join involves a column and a literal, and could match 2+ rows but must be limited to only ever return 1 row.
I'm using Yii version 1.1.13, and MySQL 5.1.something.
My problem isn't the SQL, but how to configure the Yii model classes to work in all cases. I can get the classes to work sometimes (simple eager loading) but not always (never for lazy loading).
First I will describe the database. Then the goal. Then I will include examples of code I've tried and why it failed.
Sorry for the length, this is complex and examples are necessary.
The database:
TABLE sites
columns:
id INT
name VARCHAR
type VARCHAR
rows:
id name type
-- ------- -----
1 Site A foo
2 Site B bar
3 Site C bar
TABLE field_options
columns:
id INT
field VARCHAR
option_value VARCHAR
option_label VARCHAR
rows:
id field option_value option_label
-- ----------- ------------- -------------
1 sites.type foo Foo Style Site
2 sites.type bar Bar-Like Site
3 sites.type bar Bar Site
So sites has an informal a reference to field_options where:
field_options.field = 'sites.type' and
field_options.option_value = sites.type
The goal:
The goal is for sites to look up the relevant field_options.option_label to go with its type value. If there happens to be more than one matching row, pick only one (any one, doesn't matter which).
Using SQL this is easy, I can do it 2 ways:
I can join using a subquery:
SELECT
sites.id,
f1.option_label AS type_label
FROM sites
LEFT JOIN field_options AS f1 ON f1.id = (
SELECT id FROM field_options
WHERE
field_options.field = 'sites.type'
AND field_options.option_value = sites.type
LIMIT 1
)
Or I can use a subquery as a column reference in the select clause:
SELECT
sites.id,
(
SELECT id FROM field_options
WHERE
field_options.field = 'sites.type'
AND field_options.option_value = sites.type
LIMIT 1
) AS type_label
FROM sites
Either way works great. So how do I model this in Yii??
What I've tried so far:
1. Use "on" array key in relation
I can get a simple eager lookup to work with this code:
class Sites extends CActiveRecord
{
...
public function relations()
{
return array(
'type_option' => array(
self::BELONGS_TO,
'FieldOptions', // that's the class for field_options
'', // no normal foreign key
'on' => "type_option.id = (SELECT id FROM field_options WHERE field = 'sites.type' AND option_value = t.type LIMIT 1)",
),
);
}
}
This works when I load a set of Sites objects and force it to eager load type_label, e.g. Sites::model()->with('type_label')->findByPk(1).
It does not work if type_label is lazy-loaded.
$site = Sites::model()->findByPk(1);
$label = $site->type_option->option_label; // ERROR: column t.type doesn't exist
2. Force eager loading always
Building on #1 above, I tried forcing Yii to always to eager loading, never lazy loading:
class Sites extends CActiveRecord
{
public function relations()
{
....
}
public function defaultScope()
{
return array(
'with' => array( 'type_option' ),
);
}
}
Now everything always works when I load Sites, but it's no good because there are other models (not pictured here) that have relations that point to Sites, and those result in errors:
$site = Sites::model()->findByPk(1);
$label = $site->type_option->option_label; // works now
$other = OtherModel::model()->with('site_relation')->findByPk(1); // ERROR: column t.type doesn't exist, because 't' refers to OtherModel now
3. Make the reference to the base table somehow relative
If there was a way that I could refer to the base table, other than "t", that was guaranteed to point to the correct alias, that would work, e.g.
'on' => "type_option.id = (SELECT id FROM field_options WHERE field = 'sites.type' AND option_value = %%BASE_TABLE%%.type LIMIT 1)",
where %%BASE_TABLE%% always refers to the correct alias for table sites. But I know of no such token.
4. Add a true virtual database column
This way would be the best, if I could convince Yii that the table has an extra column, which should be loaded just like every other column, except the SQL is a subquery -- that would be awesome. But again, I don't see any way to mess with the column list, it's all done automatically.
So, after all that... does anyone have any ideas?
EDIT Mar 21/15: I just spent a long time investigating the possibility of subclassing parts of Yii to get the job done. No luck.
I tried creating a new type of relation based on BELONGS_TO (class CBelongsToRelation), to see if I could somehow add in context sensitivity so it could react differently depending on whether it was being lazy-loaded or not. But Yii isn't built that way. There is no place where I can hook in code during query buiding from inside a relation object. And there is also no way I can tell even what the base class is, relation objects have no link back to the parent model.
All of the code that assembles these queries for active records and their relations is locked up in a separate set of classes (CActiveFinder, CJoinQuery, etc.) that cannot be extended or replaced without replacing the entire AR system pretty much. So that's out.
I then tried to see if I can create "fake" database column entries that would actually be a subquery. Answer: no. I figured out how I could add additional columns to Yii's automatically generated schema data. But,
a) there's no way to define a column in such a way that it can be a derived value, Yii assumes it's a column name in way too many places for that; and
b) there also doesn't appear to be any way to avoid having it try to insert/update to those columns on save.
So it really is looking like Yii (1.x) just does not have any way to make this happen.
Limited solution provided by #eggyal in comments: #eggyal has a suggestion that will meet my needs. He suggests creating a MySQL view table to add extra columns for each label, using a subquery to look up the value. To allow editing, the view would have to be tied to a separate Yii class, so the downside is everywhere in my code I need to be aware of whether I'm loading a record for reading only (must use the view's class) or read/write (must use the base table's class, does not have the extra columns). That said, it is a workable solution for my particular case, maybe even the only solution -- although not an answer to this question as written, so I'm not going to put it in as an answer.
OK, after a lot of attempts, I have found a solution. Thanks to #eggyal for making me think about database views.
As a quick recap, my goal was:
link one Yii model (CActiveRecord) to another using a relation()
the table join is complex and could match more than one row
the relation must never join more than one row (i.e. LIMIT 1)
I got it to work by:
creating a view from the field_options base table, using SQL GROUP BY to eliminate duplicate rows
creating a separate Yii model (CActiveRecord class) for the view
using the new model/view for the relation(), not the original table
Even then there were some wrinkles (maybe a Yii bug?) I had to work around.
Here are all the details:
The SQL view:
CREATE VIEW field_options_distinct AS
SELECT
field,
option_value,
option_label
FROM
field_options
GROUP BY
field,
option_value
;
This view contains only the columns I care about, and only ever one row per field/option_value pair.
The Yii model class:
class FieldOptionsDistinct extends CActiveRecord
{
public function tableName()
{
return 'field_options_distinct'; // the view
}
/*
I found I needed the following to override Yii's default table data.
The view doesn't have a primary key, and that confused Yii's AR finding system
and resulted in a PHP "invalid foreach()" error.
So the code below works around it by diving into the Yii table metadata object
and manually setting the primary key column list.
*/
private $bMetaDataSet = FALSE;
public function getMetaData()
{
$oMetaData = parent::getMetaData();
if (!$this->bMetaDataSet) {
$oMetaData->tableSchema->primaryKey = array( 'field', 'option_value' );
$this->bMetaDataSet = TRUE;
}
return $oMetaData;
}
}
The Yii relation():
class Sites extends CActiveRecord
{
// ...
public function relations()
{
return (
'type_option' => array(
self::BELONGS_TO,
'FieldOptionsDistinct',
array(
'type' => 'option_value',
),
'on' => "type_option.field = 'sites.type'",
),
);
}
}
And all that does the trick. Easy, right?!?

Fat-Free-Framework - Prevent table name from being inserted into mapper object's virtual field

I've been using the Fat-Free Framework recently, and things are going well (arguably better the longer I use it and leverage its components); however, I'm having difficulty with the ORM injecting the MySQL table name into a virtual field (used for lookup).
I know the SQL is good, and I know I could perform a second database call to retrieve the lookup field data, but since I've got things nearly working in virtual field format (and it's probably easiest to digest and debug)...
Is there any way to prevent F3 from inserting the table name during SQL generation?
Setup is easy...
class Bookmark extends \DB\SQL\Mapper
In the constructor, after the call to the parent constructor, I add my virtual fields...
$this->type_name = '
SELECT CASE bookmark_type_id
WHEN 1 THEN \'Project\'
WHEN 2 THEN \'Member\'
ELSE \'Unknown\' END
';
NOTE: This works, though NOT if I use an IF, then I get the table name injected into the IF clause -- after the first comma.
$this->description = '
SELECT CASE bookmark_type_id
WHEN 1 THEN (SELECT p.title FROM projects p WHERE p.id = reference_id)
WHEN 2 THEN (SELECT CONCAT_WS(\' \', m.first_name, m.last_name) FROM members m WHERE m.id = reference_id)
ELSE \'Unknown\' END
';
NOTE: This fails with the table name inserted after the first comma (i.e. before m.first_name).
For clarity, this is the result (notice `cm_bookmark`.):
SELECT CASE bookmark_type_id
WHEN 1 THEN (SELECT p.title FROM projects p WHERE p.id = reference_id)
WHEN 2 THEN (SELECT CONCAT_WS(' ',`cm_bookmark`. m.first_name, m.last_name) AS FullName FROM members m WHERE m.id = reference_id)
ELSE 'Unknown' END
) AS `description`
I get the feeling this is just another one of those "don't do that" situations, but any thoughts on how to achieve this in F3 would be appreciated.
(Oddly, it's only after the first comma in the subquery. If the table name insertion was consistently clever, I'd expect to see it peppered in front of m.last_name too, but it isn't.)
EDIT: It seems as though it's related to the second occurrence of something in parentheses. I've used CONCAT() in another virtual field call, and it works fine -- but it's the first (and only) use of parentheses in the field set up. If I remove the call to CONCAT_WS() and return a single field, the setup above works fine.
EDIT2: To clarify how the load is occurring, see below...
// database setup
// (DB_* set up in config.ini)
$db = new \DB\SQL($f3->get('DB_CONN'), $f3->get('DB_USER'), $f3->get('DB_PASS'));
$f3->set('DB', $db);
...
// Actual call
$db = \Base::instance()->get('DB');
$bookmark = new \CM\Models\Bookmark($db);
$bookmark->load_by_id($item['id']);
...
// in Bookmark Class (i.e. load_by_id)
$b->load(array('id=?', $id));
The only answer (to stay on this path) I have come up with so far is to create another virtual field and piece the 2 parts together later.
Not ideal, but it works.
The mapper does not allow such advanced capabilities, but I would suggest you use Cortex which luckily extends mapper so not much code change.
Below is an example:
Class Bookmark extends \DB\Cortex{
protected
$db = 'DB', // From $f3->set('DB', $db);
$table = 'bookmarks'; // your table name
/* You can also define these custom field preprocessors as a method within the class, named set_* or get_*, where * is the name of your field.*/
public function get_desciption($value) {
switch($this->bookmark_type_id){
case "1":
/*.....................Hope you get the drill*/
}
}
}

How do I add a count of related records in CakePhp index

I am working on an application to store records for a youth group.
This youth group has a system of badges for members to achieve and I have the following tables;
members
badges
member_badges
The first two tables are self explanatory, members are the youth group members and the badges is the name of a badge, eg. FieldCraft, Life Saving, First Aid and so on.
The member_badges records the achievement of a member and will have the member_id, badge_id, date of attempt and status (Pass or Fail). We record failed attempts to as it will help us to gauge the effectiveness of a course - what's the pass rate and so on.
I want to create a table with the following data;
Badge | Number of Attempts | Pass | Fail
I've created a badgeController function as follow;
public function badgereportsummary() {
$paginate = array(
'contain' => array('MemberBadge'));
$this->Paginator->settings = $paginate;
$this->set('badges', $this->paginate());
}
Now this works in a fashion - it returns an array of Badges, each of which contains an array of the Member Badges and I've got a rough and ready page work with the following in the View
echo h(count($badge['MemberBadge']));
However - this won't allow me to sort on this field, and also in future I want to add the ability to limit on a date range e.g. "Show me the count of First Aid badges attempted from 1/1/15 through 30/3/15". I've seen some suggestions online about using counterCache (I've used that in other places on the app) but this won't allow me to work out count for date range etc.
What I think I want to to is add the count of the Member Badges on a particular page as a virtual field in the controller. I've been searching the docs and Google, and beating my head on the keyboard all day and can't work it out. Any suggestions on how to accomplishment this are greatly appreciated.
Your summary report (Badge | Number of Attempts | Pass | Fail) looks like it needs a SQL statement such as:
SELECT badge.description AS Badge
, COUNT(*) AS NumberOfAttempts
, SUM(IF(member_badges.status = 'Fail',0,1)) AS Fails
, SUM(IF(member_badges.status = 'Pass',0,1)) AS Passes
FROM member_badges
JOIN badges ON badges.id = member_badges.badge_id
WHERE member_badges.date_of_attempt BETWEEN '20140101' AND '20140131'
GROUP BY badge.description
ORDER BY NumberOfAttempts DESC;
I would then change your badgereportsummary method to generate this SQL, which is relatively straightforward.
Move the function from the Badge controller model to the MemberBadge model
Create virtual fields for the new column names within the method ->
$this->MemberBadge->virtualFields['NumberOfAttempts'] = 0;
$this->MemberBadge->virtualFields['Fails'] = 0;
$this->MemberBadge->virtualFields['Passes'] = 0;
Write the find:
Code Example
$fields = array('Badge.description'
,'COUNT(*) AS MemberBadge__NumberOfAttempts'
,'SUM(IF(MemberBadge.status = 'Fail',0,1)) AS MemberBadge__Fails'
,'SUM(IF(MemberBadge.status = 'Pass',0,1)) AS MemberBadge__Passes');
return $this->find('all'
,'conditions' => array('MemberBadge.date_of_attempt BETWEEN ? AND ?' => array($start_date,$end_date)
,'fields' => $fields
,'order' => array('NumberOfAttempts DESC')
,'group' => array('Badge.description'));
Then in any Badge controller method call $this->Badge->MemberBadge-> badgereportsummary($start_date, $end_date)
I would not use a permanent virtual field (i.e., defining the virtual field in the model) since this is an aggregate function. Also, I would not go with counterCache for your exact reason - needing to filter the results based on another field.

php: dividing a multitable select into several objects / object structure

let me at first state that we use php and postgre database. In our project we have decided not to use any ORM due to its overload of sql queries and we are taking the oposite way.
Imagine you have a select from several tables, lets say joined on id columns. For instance:
tables: users(id, name), items(id, name, description), comments(user_id, item_id, text, rating)
So basically you have a table of users, a table of some items and a table of comments which are related to one user and one item.
You create two objects - user and item representing their table row. And then you want to create a comment object. In an ORM it would contain objects user and item and they would load themselves with their queries, but that would be two queries and you re thinking...hm but I can select that data with a single query...but how?
Imagine that you have this select:
SELECT * FROM comments JOIN users ON comments.user_id = users.id JOIN items ON comments.item_id = items.id
(you can also imagine a WHERE clause with specified item id or user id etc.)
So how would you split the result of such a select into this class structure, lets say you want a list of comment objects:
user
item
comment (contains references to user and item object)
So far our theoretical solution was to prefix name of the columns with fixed prefixes :) and then propagating the result into the object structure and each objects takes what it needs from the select. Any other solutions? Lets say more sophisticated?
Thanks for any ideas
PS: obviously I have used a very simple example, but try to imagine that the problem is far larger and the structure far more complex
First of all, you might benefit from looking at the Data Mapper pattern. A simple use-case with would look like this:
$user = new User;
$mapper = new UserMapper( $db );
$user->setName('foobar');
$mapper->fetch( $user );
if ( $user->isBanned() )
{
throw new Exception('get out !');
}
$user->setLastActive( time() );
$mapper->store( $user );
As for the single query with data: that's not the important part. You just ALIAS it as required (oh .. and i hope you are not using the * for selecting rows). The important bit is creating an object graph from selected data. That where you use builders/factories.
//the rest of PDO-related code
$data = $statement->fecth(PDO::FETCH_ASSOC);
$comment = $commentFactory->build($data);
Where $commentFactory is instance of CommentFactory:
class CommentFactory
{
public function build( $params )
{
$author = new User;
$subject = new Item;
$comment = new Comment( $author, $subject );
$author->setId( $params['user_id']);
$author->setName( $params['user_name']);
$subject->setId( $param['item_id']);
$comment->setContent( $param['content']);
return $comment;
}
}
Additionally with setup like this, you can easily change how $comment is made, just by changing what class is the $commentFactory an instance of.

Categories