This question already has answers here:
Closed 10 years ago.
Possible Duplicate:
How do I add a limit to update-query in Zend Framework?
I would like to do a update and place LIMIT 1 as precaution.
Here is my code:
$vals = array();
$vals['status'] = 'closed';
$where = $write->quoteInto('id =?', $orderID);
$write->update("order", $vals ,$where);
Where can i add a LIMIT into this query?
(looks like has been asked in the past but i hope someone out there has the answer)
It looks like you're using Zend_Db_Adapter to perform your queries so I'm not sure you can do what I do, anyway here goes.
I usually use the Zend_Db_Table_Row save() method to insert and update records, however I also use the DbTable models to provide access to the Table and Table_Row abstract api's.
public function save(Music_Model_Artist $artist) {
//if id is not null
if (!is_null($artist->id)) {
//find row, uses fetchRow() so will return NULL if not found
$row = $this->findById($artist->id);
$row->id = $artist->id;
} else {
//if ID is null create new row
$row = $this->_getGateway()->createRow();
}
//assign data to row
$row->name = $artist->name;
//save new row or update
$row->save();
//return the row, in case I need it for something else
return $row;
}
what this amounts to is that if I include and ID in the data array (or in this case an entity object) the row will be updated and if there is no ID in the object a new row will be created.
If your curious here's how I __construct the class:
public function __construct(Zend_Db_Table_Abstract $tableGateway = NULL) {
//pass in concrete DbTable Model, parent alts to using table name string to construct class
$this->_tableGateway = new Application_Model_DbTable_Artist();
parent::__construct($tableGateway);
}
Hope this provides some help.
Hmmm... My Zend is rusty, but I think in the past I have used: used http://framework.zend.com/manual/en/zend.db.statement.html to execute SQL (selects/inserts/updates) as normal sql statements...
But yes all the old and current info is correct - update() has not ability to place a 'limit' - you just need to hope you designed the DB properly and such tables won't have duplicate keys!
Also... 'order' is a REALLY bad table-name :) with 'order' being a reserved word in MySQL ( Indeed any DB! ).
Ok being that it looks like there isnt a way to do this i just decided to use the straingt query functionality. I still used the where built functionality, to remove any funny unwanted values from the WHERE.
$where = $write->quoteInto('id =?', $order_id);
$write->query("UPDATE orders SET status = 'closed' WHERE $where LIMIT 1");
Related
i am using Fat-Free Framework to do rapid prototyping of my application. Now, whenever I try to load some data from database, i can use the load() function within the SQL\Mapper but it returned all of the column.
I found SELECT() function but it does not returning any data.
$this->load(['myId=?',$id]) will return the data along with the other columns
$this->select('name',['myId=?',$id]) should return the data from name column but i got nothing.
$this->db->exec('SELECT name FROM persons WHERE myId=?',$id) will return the the data from name column.
what is the proper way of using SELECT() from Fat-Free framework? my goal is to only retrieve single data from name column only.
The right way to do it is like this:
$table = new DB\SQL\Mapper($db, 'persons');
// assign to $results
$results = $table->load(array('myId=?', $id));
foreach($results => $row){
echo $row->name;
}
As described here: https://fatfreeframework.com/3.6/databases#SeekandYouShallFind
I'm requesting the community's wisdom because I want to avoid bad coding practices and/or mistakes.
I'm having a php class wich is an objects manager. It does all the work with the database: inserting new data, updating it, getting it and deleting it (I've read it's called CRUD...). So it has a function that gets an element by id.
What I want to write is a function that gets a list of objects from the table.
I will then use a mysql query that goes something like
SELECT * FROM mytable WHERE column1='foo'
And then some order by and limit/offset.
However, in my application there are different cases in which I will need different lists from this table. The WHERE clause will then be different.
Should I write different functions, one per type of list?
Or should I write one generic function to which I will send arguments that then dynamically creates the query? If so, do you have any advice on how to do this properly?
EDIT:
Thanks for all your answers! I should tell that I'm not using any framework (maybe wasn't the best idea...), so I didn't know about query builders. I'll investigate that (either finding a standalone uery builder or migrating to a framework or writing my own, I don't know yet). That will be useful any time I need to execute a mysql query :-)
Although I'm still confused:
Let's say I need several lists of clients (objects), for example all clients, clients over 18, clients currently online...
What approach would be best to retrieve those lists? I can either have 3 functions in my clients manager
allClients() {//execute a specific query and return list of objects}
allClientsOver18() {//execute specific query and return list of objects}
allClientsOnline() {//execute specific query and return list of objects}
or I can have one function tht builds the query based on parameters
listClients($some, $parameters)
{
//Build the query based on the parameters (definitely need a query builder!)
//Execute the query
//return list of objects
}
Which approach would be best (I guess it depends on circumstances) and mostly, why?
Thanks in advance!
Rouli
Thanks for all the info on query builders, I didn't even know it existed! :-) However I'm still confused as to wether I should write one specific function for each case (that function can still use the query builder to write its specific query), or write one generic function that builds dynamically the query based onf parameters. Which would be better in which case? I've added an example in my question, hope it makes it clearer!
This depends on how often you use each of these isolated queries, how complex the conditions are and how often you my need to combine the conditions with other queries. For eaxample if each the "online" and "over18" are just simple conditions then you could just use the normal findBy logic from my example:
$table = new MyTable($db);
$onlineOnly = $table->findBy(array('is_online' => true), null, null);
$over18Only = $table->findBy(array('is_over_18' => true), null, null);
$onlineOver18 = $table->findBy(array('is_over_18' => true, 'is_online' => true), null, null);
If the query is more complex - for example to get over 18 clients you have to do:
select client.*, (YEAR(CURDATE()) - YEAR(client.birthdate)) as age
FROM client
WHERE age >= 18
Then its probably better to make this into a separate method or create methods to work on Query objects directly to add complex conditions for example - especially if you will need this condition in a few different queries in the app:
$table = new MyTable($db);
// creates a basic query defaulted to SELECT * FROM table_name
$query = $table->createQuery();
// adds the complex condition for over 18 resulting in
// SELECT table_name.*, (YEAR(CURDATE()) - YEAR(table_name.birthdate)) as age WHERE age >= 18
$over18 = $table->applyOver18Query($query)->execute();
This way you can apply your over 18 condition easily to any query with out manually manipulating the builder ensure that your over 18 condition is consistent. But for simplicity you could also have a convenience method like the following:
public function findOver18By(array $criteria, $limit = null, $offest = null) {
$query = $this->findBy($criteria, $limit, $offset);
$this->applyOver18Query($query);
return $query->execute();
}
Normally you would use some kind of query builder at the lower level like:
$query = $db->createQuery()
->select($fields)
->from($tableName)
->where($fieldName, $value);
$results = $query->execute();
Then you might have a class that makes use of this like:
class MyTable
{
protected $tableName = 'my_table';
protected $db;
public function __construct($db) {
$this->db = $db;
}
public function findBy(array $criteria, $limit = null, $offset = null) {
$query = $this->db->createQuery();
$query->select('*')->from($this->tableName);
foreach ($criteria as $col => $value) {
// andWhere would determine internally whether or not
// this is the initial WHERE clause or an AND clause
// something similar would happen with an orWhere method
$query->andWhere($col, $value);
}
if (null !== $limit) {
$query->limit($limit);
}
if (null !== $offset) {
$query->offset($offset);
}
return $query->execute();
}
}
Usage would look like:
$table = new MyTable($db);
$result = $table->findBy(array('column1' => 'foo'), null, null);
This is a lot to implement on your own. Most people use an ORM or a DBAL to provide these features and those are often included with a framework like Eloquent with Laravel, or Doctrine with Symfony.
I guess at start you should need some main data like
$main = [
'from' = '`from_table`',
]
Then you should add selects if had
$selects = ['fields1','field2'];
$where = ['some condition', 'other condition'];
Then you could
$query = "SELECT ".implode(',', $selects ." FROM ".$main['from']."
WHERE ".implode('AND ', $where .";";
That's some approaches for simple one table query.
If you need Joins, then $selects better would be make with aliasos, so no field will be lost if they are not different, like
select temp.id as temp_id , temp2.id temp2_id from temp
left join temp2 on temp2.temp_id = temp.id
Feel free to ask some questions, maybe i haven't told , but you should also check bound parameters with some functions to avoid sql injections
I suggest using a CLASS for your database which holds all your database accessing functions as it makes your code cleaner making it more easier to look through for errors or modifications.
class Database
{
public function connect() { }
public function disconnect() { }
public function select() { }
public function insert() { }
public function delete() { }
public function update() { }
}
sample connect function for connecting to a selected database.
private db_host = ‘’;
private db_user = ‘’;
private db_pass = ‘’;
private db_name = ‘’;
public function connect()
{
if(!$this->con)
{
$myconn = mysqli_connect($this->db_host,$this->db_user,$this->db_pass);
if($myconn)
{
$seldb = mysqli_select_db($this->db_name,$myconn);
if($seldb)
{
$this->con = true;
return true;
} else
{
return false;
}
} else
{
return false;
}
} else
{
return true;
}
}
with this approach will make creating CRUD functions easier. Heres a sample insert function.
public function insert($table,$values,$rows = null)
{
if($this->tableExists($table))
{
$insert = 'INSERT INTO '.$table;
if($rows != null)
{
$insert .= ' ('.$rows.')';
}
for($i = 0; $i < count($values); $i++)
{
if(is_string($values[$i]))
$values[$i] = '"'.$values[$i].'"';
}
$values = implode(',',$values);
$insert .= ' VALUES ('.$values.')';
$ins = #mysql_query($insert);
if($ins)
{
return true;
}
else
{
return false;
}
}
}
heres a quick view on using this.
;<?php;
$db->insert('myDataBase',array(3,"Name 4","this#wasinsert.ed")); //this takes 3 paramteres
$result = $db->getResult(); //Assuming you already have getResult() function.
print_r($result);
?>
EDIT
there are more purist approach to handling database operations. I highly suggest it because handling information is very delicate and should be fronted with many safety measures But it requires deeper php knowledge. Try PDO for php and this article by matt bango on prepared statements and its significance.
I need to make a fundamental decision of my database/web interaction and I am missing the knowledge to even find proper search terms.
Background:
I am building a family website which supports a forum, family tree, pvp games with rankings and more details, all from a datamodel. Technologies right now: Php, MySQL, javascript in object oriented fashion.
Requirement:
In a forum datamodel, process a written post as addition of a new forum topic (thread).
Current approach:
In my current datamodel this would imply and update on two tables: Post and Topic.
I would need to insert a row in the topic table, then get the newly generated topicId(sequence), and then use that in an insert to the post table.
Problem:
I feel this is too much work for what needs to happen, too much interaction.
But it will become a typical requirement if I stick with the current approach.
Question:
am I on the right track anyway or should I
restructure the datamodel or
pick another way of database interaction (e.g. stored procedures)
am I facing a typical example where you would use methodology/framework xyz.
Currently tables have following structure (loosely based on this one from erdiagrams.com)
TOPIC: ('thread')
id
Forum_ID (FK)
Person_ID (FK)(threadcreator)
IsLocked
IsSticky
Subject
ViewCount
DateCreated
Tc_post_id - trigger to last post_id in this thread
POST
id
topic_id(FK)
person_id(FK)
subject
message
timestamp
replyto
Then I have a view that collects the last post for each topic and displays some info on that as well (e.g. last poster image) over the trigger Tc_post_id.
Ad 1 and 2: Your data model is fine. Using foreign keys is crucial here. One more thing that you need to take care of is that the database should ensure there is a TOPIC record for each POST. This is done by setting POST.topic_id NOT NULL attribute. This is sufficient safety mechanism on the DB side, as it ensures that no POST will be left without TOPIC. No matter what you do now with your POST you are obligated to provide a TOPIC.
Ad 3: A trigger with stored procedure is not recommended here as you have additional data in your TOPIC table (IsSticky, IsLocked, etc), which you might want to provide upon TOPIC record creation. Also, if such a trigger would be applicable, the database design would be a subject to denormalization.
Ad 4: On the business logic side you can now aid yourself by writing a automated mechanism to create the TOPIC record every time a new POST record is created without specified topic_id. I recommend using some ORM for this or take advantage of the data models available in any MVC framework. The blueprint for such models would look like this:
abstract class AModel // this class should be provided by ORM or framework
{
/**
* #var PDO
*/
protected $_db_driver;
public function getLastInsertId()
{
$stmt = $this->_db_driver->prepare('SELECT LAST_INSERT_ID() AS id');
$stmt->execute();
return $stmt->fetch(PDO::FETCH_OBJ)->id;
}
public abstract function getFieldList();
}
class ForumTopicModel extends AModel
{
public function insert(array $data)
{
$sql = 'INSERT INTO topic VALUES (:id, :forum_id, :person_id, :is_locked, ...)';
$stmt = $this->_db_driver->prepare($sql);
return $stmt->execute($data);
}
public function getFieldList()
{
return array('id', 'forum_id', 'person_id', 'is_locked', /*...*/);
}
// ...
}
class ForumPostModel extends AModel
{
public function insert(array $data)
{
$sql = 'INSERT INTO post VALUES (:id, :topic_id, :person_id, :subject, ...)';
$stmt = $this->_db_driver->prepare($sql);
return $stmt->execute($data);
}
public function getFieldList()
{
return array('id', 'topic_id', 'person_id', 'subject', /*...*/);
}
public function insertInitialTopicPost(array $form_data)
{
$this->_db_driver->beginTransaction();
$result = true;
if ( empty($form_data['topic_id']) ) {
// no topic_id provided, so create new one:
$topic = new ForumTopicModel();
$topic_data = array_intersect_key(
$form_data, array_flip($topic->getFieldList())
);
$result = $topic->insert($topic_data);
$form_data['topic_id'] = $topic->getLastInsertId();
}
if ( $result ) {
$forum_post_data = array_intersect_key(
$form_data, array_flip($this->getFieldList())
);
$result = $this->insert($forum_post_data);
}
if ( $result ) {
$this->_db_driver->commit();
}
else {
$this->_db_driver->rollBack();
}
return $result;
}
// ...
}
Note: as a good MVC practice those models should be the only place to directly operate on the table rows. Otherwise you'll end up getting SQL errors (but the data model will remain coherent, so you don't have to worry that something will break).
Finally take advantage of your models in the controller layer:
class ForumPostController extends AController
{
public function createInitialTopicPostAction()
{
$form_data = $this->getRequest()->getPost(); /* wrapper for getting
the $_POST array */
// (...) validate and filter $form_data here
$forumPost = new ForumPostModel();
$result = $forumPost->insertInitialTopicPost($form_data);
if ( $result ) {
// display success message
}
else {
// display failure message
}
}
}
The way I understand it: topics are containers of posts.
Topics table would be rather minimal, and would perhaps only contain a topic id (PK) and topic title.
The posts themselves will contain post id (PK), topic id (FK), timestamps, author id, text.
I would utilize InnoDB and foreign keys, so a topic that is deleted could delete all of its child posts.
(edit:)
In this answer I posted a way to do it using mysql_insert_id(), which would be still a technically correct solution (correct me if wrong).
However instead I will now go for the PDO wrapper I guess. And also, this is not an answer to the general modeling/approach question.
Still, following would be a way to do it:
$sql = "INSERT INTO topic VALUES (NULL,'$forumId',<more parameters>)";
$result = mysql_query($sql);
# get the generated id
$topicId = mysql_insert_id();
# and insert into the post table
$sql = "INSERT INTO post VALUES (NULL,'$topicId',<more parameters>)";
$result = mysql_query($sql);
mysql_free_result($result);
Source: http://www.desilva.biz/mysql/insertid.html
Alright here's the situation, I have an application written in the Zend_Framework, that is compatible with both MySQL and MSSQL as the backend. Now, ZF is pretty good at solving a lot of the SQL discrepancies/differences between the two languages, but I still have yet to figure this one out.
The objective is to select 1 random record from the table, which is an extremely simple statement.
Here's a select statement for example:
$sql = $db->select()
->from("table")
->order("rand()")
->limit(1);
This works perfectly for the MySQL database tables, because the sql for MySQL is as follows:
SELECT `table`.* FROM `table` ORDER BY rand() ASC
Now MSSQL on the other hand, uses the newid() function to do randomizing.
Is there some sort of helper I can pass into the order() function in order to make it realize that it has to use the proper ordering? I searched the documentation and on the zfforums, found a few tips, but nothing solid.
One of the things I did find was:
ORDER BY RANDOM() not working - ZFForums.com
They are using the following:
$res = $db->fetchAll(
'SELECT * FROM table ORDER BY :random',
array('random' => new Zend_Db_Expr('RANDOM()')
);
It works... but I am not looking to build my select statement by typing it out and doing a replace on the string, I am trying to keep it in the same Zend_Db_Select object. I also have tried passing in the Zend_Db_Expr('RANDOM()') into the ->order() on the statement, and it fails. He also posts a theoretical solution to finding the answer, but I am not looking to rewrite the function this is within, modifying the $db->fetch() call.
Any ideas?
You could quickly abstract the function to a table - who knows which adapter it is using:
class MyTable extends Zend_Db_Table_Abstract {
public function randomSelect($select=null) {
if ($select === null) $select = $this->select();
if (!$select instanceOf Zend_Db_Select) $select = $this->select($select);
$adapter = $this->getAdapter();
if ($adapter instanceOf Zend_Db_Adapter_Mysqli) {
$select->order(new Zend_Db_Expr('RAND()'));
} else if ($adapter instanceOf Zend_Db_Adapter_Dblib) {
$select->order(new Zend_Db_Expr('NEWID()'));
} else {
throw new Exception('Unknown adapter in MyTable');
}
return $select;
}
}
$someSelect = $table->select();
// add it to an existing select
$table->randomSelect($someSelect);
// or create one from scratch
$select = $table->randomSelect();
Also, I found an article somewhere which I lost that recommended trying something like:
$select->order(new Zend_Db_Expr('0*`id`+RAND()));
to subvert MSSQL's query optimizer and trick it into calculating a new value for each row.
I would create class My_Db_Expr_Rand extends Zend_Db_Expr. Bassed on the adapter I would return either one or the other.
What is the best way of working with calculated fields of Propel objects?
Say I have an object "Customer" that has a corresponding table "customers" and each column corresponds to an attribute of my object. What I would like to do is: add a calculated attribute "Number of completed orders" to my object when using it on View A but not on Views B and C.
The calculated attribute is a COUNT() of "Order" objects linked to my "Customer" object via ID.
What I can do now is to first select all Customer objects, then iteratively count Orders for all of them, but I'd think doing it in a single query would improve performance. But I cannot properly "hydrate" my Propel object since it does not contain the definition of the calculated field(s).
How would you approach it?
There are several choices. First, is to create a view in your DB that will do the counts for you, similar to my answer here. I do this for a current Symfony project I work on where the read-only attributes for a given table are actually much, much wider than the table itself. This is my recommendation since grouping columns (max(), count(), etc) are read-only anyway.
The other options are to actually build this functionality into your model. You absolutely CAN do this hydration yourself, but it's a bit complicated. Here's the rough steps
Add the columns to your Table class as protected data members.
Write the appropriate getters and setters for these columns
Override the hydrate method and within, populate your new columns with the data from other queries. Make sure to call parent::hydrate() as the first line
However, this isn't much better than what you're talking about already. You'll still need N + 1 queries to retrieve a single record set. However, you can get creative in step #3 so that N is the number of calculated columns, not the number of rows returned.
Another option is to create a custom selection method on your TablePeer class.
Do steps 1 and 2 from above.
Write custom SQL that you will query manually via the Propel::getConnection() process.
Create the dataset manually by iterating over the result set, and handle custom hydration at this point as to not break hydration when use by the doSelect processes.
Here's an example of this approach
<?php
class TablePeer extends BaseTablePeer
{
public static function selectWithCalculatedColumns()
{
// Do our custom selection, still using propel's column data constants
$sql = "
SELECT " . implode( ', ', self::getFieldNames( BasePeer::TYPE_COLNAME ) ) . "
, count(" . JoinedTablePeer::ID . ") AS calc_col
FROM " . self::TABLE_NAME . "
LEFT JOIN " . JoinedTablePeer::TABLE_NAME . "
ON " . JoinedTablePeer::ID . " = " . self::FKEY_COLUMN
;
// Get the result set
$conn = Propel::getConnection();
$stmt = $conn->prepareStatement( $sql );
$rs = $stmt->executeQuery( array(), ResultSet::FETCHMODE_NUM );
// Create an empty rowset
$rowset = array();
// Iterate over the result set
while ( $rs->next() )
{
// Create each row individually
$row = new Table();
$startcol = $row->hydrate( $rs );
// Use our custom setter to populate the new column
$row->setCalcCol( $row->get( $startcol ) );
$rowset[] = $row;
}
return $rowset;
}
}
There may be other solutions to your problem, but they are beyond my knowledge. Best of luck!
I am doing this in a project now by overriding hydrate() and Peer::addSelectColumns() for accessing postgis fields:
// in peer
public static function locationAsEWKTColumnIndex()
{
return GeographyPeer::NUM_COLUMNS - GeographyPeer::NUM_LAZY_LOAD_COLUMNS;
}
public static function polygonAsEWKTColumnIndex()
{
return GeographyPeer::NUM_COLUMNS - GeographyPeer::NUM_LAZY_LOAD_COLUMNS + 1;
}
public static function addSelectColumns(Criteria $criteria)
{
parent::addSelectColumns($criteria);
$criteria->addAsColumn("locationAsEWKT", "AsEWKT(" . GeographyPeer::LOCATION . ")");
$criteria->addAsColumn("polygonAsEWKT", "AsEWKT(" . GeographyPeer::POLYGON . ")");
}
// in object
public function hydrate($row, $startcol = 0, $rehydrate = false)
{
$r = parent::hydrate($row, $startcol, $rehydrate);
if ($row[GeographyPeer::locationAsEWKTColumnIndex()]) // load GIS info from DB IFF the location field is populated. NOTE: These fields are either both NULL or both NOT NULL, so this IF is OK
{
$this->location_ = GeoPoint::PointFromEWKT($row[GeographyPeer::locationAsEWKTColumnIndex()]); // load gis data from extra select columns See GeographyPeer::addSelectColumns().
$this->polygon_ = GeoMultiPolygon::MultiPolygonFromEWKT($row[GeographyPeer::polygonAsEWKTColumnIndex()]); // load gis data from extra select columns See GeographyPeer::addSelectColumns().
}
return $r;
}
There's something goofy with AddAsColumn() but I can't remember at the moment, but this does work. You can read more about the AddAsColumn() issues.
Here's what I did to solve this without any additional queries:
Problem
Needed to add a custom COUNT field to a typical result set used with the Symfony Pager. However, as we know, Propel doesn't support this out the box. So the easy solution is to just do something like this in the template:
foreach ($pager->getResults() as $project):
echo $project->getName() . ' and ' . $project->getNumMembers()
endforeach;
Where getNumMembers() runs a separate COUNT query for each $project object. Of course, we know this is grossly inefficient because you can do the COUNT on the fly by adding it as a column to the original SELECT query, saving a query for each result displayed.
I had several different pages displaying this result set, all using different Criteria. So writing my own SQL query string with PDO directly would be way too much hassle as I'd have to get into the Criteria object and mess around trying to form a query string based on whatever was in it!
So, what I did in the end avoids all that, letting Propel's native code work with the Criteria and create the SQL as usual.
1 - First create the [get/set]NumMembers() equivalent accessor/mutator methods in the model object that gets returning by the doSelect(). Remember, the accessor doesn't do the COUNT query anymore, it just holds its value.
2 - Go into the peer class and override the parent doSelect() method and copy all code from it exactly as it is
3 - Remove this bit because getMixerPreSelectHook is a private method of the base peer (or copy it into your peer if you need it):
// symfony_behaviors behavior
foreach (sfMixer::getCallables(self::getMixerPreSelectHook(__FUNCTION__)) as $sf_hook)
{
call_user_func($sf_hook, 'BaseTsProjectPeer', $criteria, $con);
}
4 - Now add your custom COUNT field to the doSelect method in your peer class:
// copied into ProjectPeer - overrides BaseProjectPeer::doSelectJoinUser()
public static function doSelectJoinUser(Criteria $criteria, ...)
{
// copied from parent method, along with everything else
ProjectPeer::addSelectColumns($criteria);
$startcol = (ProjectPeer::NUM_COLUMNS - ProjectPeer::NUM_LAZY_LOAD_COLUMNS);
UserPeer::addSelectColumns($criteria);
// now add our custom COUNT column after all other columns have been added
// so as to not screw up Propel's position matching system when hydrating
// the Project and User objects.
$criteria->addSelectColumn('COUNT(' . ProjectMemberPeer::ID . ')');
// now add the GROUP BY clause to count members by project
$criteria->addGroupByColumn(self::ID);
// more parent code
...
// until we get to this bit inside the hydrating loop:
$obj1 = new $cls();
$obj1->hydrate($row);
// AND...hydrate our custom COUNT property (the last column)
$obj1->setNumMembers($row[count($row) - 1]);
// more code copied from parent
...
return $results;
}
That's it. Now you have the additional COUNT field added to your object without doing a separate query to get it as you spit out the results. The only drawback to this solution is that you've had to copy all the parent code because you need to add bits right in the middle of it. But in my situation, this seemed like a small compromise to save all those queries and not write my own SQL query string.
Add an attribute "orders_count" to a Customer, and then write something like this:
class Order {
...
public function save($conn = null) {
$customer = $this->getCustomer();
$customer->setOrdersCount($customer->getOrdersCount() + 1);
$custoner->save();
parent::save();
}
...
}
You can use not only the "save" method, but the idea stays the same. Unfortunately, Propel doesn't support any "magic" for such fields.
Propel actually builds an automatic function based on the name of the linked field. Let's say you have a schema like this:
customer:
id:
name:
...
order:
id:
customer_id: # links to customer table automagically
completed: { type: boolean, default false }
...
When you build your model, your Customer object will have a method getOrders() that will retrieve all orders associated with that customer. You can then simply use count($customer->getOrders()) to get the number of orders for that customer.
The downside is this will also fetch and hydrate those Order objects. On most RDBMS, the only performance difference between pulling the records or using COUNT() is the bandwidth used to return the results set. If that bandwidth would be significant for your application, you might want to create a method in the Customer object that builds the COUNT() query manually using Creole:
// in lib/model/Customer.php
class Customer extends BaseCustomer
{
public function CountOrders()
{
$connection = Propel::getConnection();
$query = "SELECT COUNT(*) AS count FROM %s WHERE customer_id='%s'";
$statement = $connection->prepareStatement(sprintf($query, CustomerPeer::TABLE_NAME, $this->getId());
$resultset = $statement->executeQuery();
$resultset->next();
return $resultset->getInt('count');
}
...
}