CakePHP: Can I ignore a field when reading the Model from the DB? - cakephp

In one of my models, I have a "LONGTEXT" field that has a big dump of a bunch of stuff that I never care to read, and it slows things down, since I'm moving much more data between the DB and the web app.
Is there a way to specify in the model that I want CakePHP to simply ignore that field, and never read it or do anything with it?
I really want to avoid the hassle of creating a separate table and a separate model, only for this field.
Thanks!
Daniel

As #SpawnCxy said, you'll need to use the 'fields' => array(...) option in a find to limit the data you want to retrieve. If you don't want to do this every time you write a find, you can add something like this to your models beforeFind() callback, which will automatically populate the fields options with all fields except the longtext field:
function beforeFind($query) {
if (!isset($query['fields'])) {
foreach ($this->_schema as $field => $foo) {
if ($field == 'longtextfield') {
continue;
}
$query['fields'][] = $this->alias . '.' . $field;
}
}
return $query;
}
Regarding comment:
That's true… The easiest way in this case is probably to unset the field from the schema.
unset($this->Model->_schema['longtextfield']);
I haven't tested it, but this should prevent the field from being included in the query. If you want to make this switchable for each query, you could move it to another variable like $Model->_schemaInactiveFields and move it back when needed. You could even make a Behavior for this.

The parameter fields may help you.It doesn't ignore fields but specifies fields you want:
array(
'conditions' => array('Model.field' => $thisValue), //array of conditions
'fields' => array('Model.field1', 'Model.field2'), //list columns you want
)
You can get more information of retrieving data in the cookbook .
Another idea:
Define your special query in the model:
function myfind($type,$params)
{
$params['fields'] = array('Model.field1','Model.field2',...);
return $this->find($type,$params);
}
Then use it in the controller
$this->Model->myfind($type,$params);

Also try containable behaviour will strip out all unwanted fields and works on model associations as well.
Containable
class Post extends AppModel { <br>
var $actsAs = array('Containable'); <br>
}
where Post is your model?

You can add a beforeFilter function in your Table and add a select to the query
Excample:
public function beforeFind(Event $event, Query $query){
$protected = $this->newEntity()->hidden;
$tableSchema = $event->subject()->schema();
$fields = $tableSchema->columns();
foreach($fields as $key => $name){
if(in_array($name,$protected)){
unset($fields[$key]);
}
}
$query->select($fields);
return $event;
}
In this excample I took the hidden fields from the ModelClass to exclude from result.
Took it from my answer to a simular question here : Hidden fields are still listed from database in cakephp 3

Related

Add a new translatable field to an existing translatable table in CakePHP 2.2

I'm using CakePHP's translatable behavior. I have a few existing fields working fine, but I'm having trouble adding a new translatable field to my model.
CakePHP uses an INNER JOIN to fetch all translatable fields from the database.
Now, if I add an extra translatable field to my model, all the translation records for that field won't exist in the database. And because of the inner join, whenever it tries to fetch ANY existing records from the database, it will return blank - because the INNER JOIN on the new field fails, and so the entire query returns nothing.
Surely people must have come accross this situation before. Is there an easy solution?
One solution would be to edit/override the core and make all the INNER JOIN's into LEFT OUTER JOIN's. Is there anything wrong with that?
Another solution would be to run an update on the translations table to create all the extra records for the new field, every time you add a new translatable field - but I hate that solution.
Is there a better solution? How have others dealt with this problem?
Thanks in advance.
OK, here's a way of making sure the records exist after each time you add a new translatable field. If you've got a better answer, add it, and I'll mark yours as correct.
PS - this is tested for my purposes. I'm using multiple translation tables (http://book.cakephp.org/2.0/en/core-libraries/behaviors/translate.html#multiple-translation-tables). I think it should work for most situations, but if not, it should at least be a good starting point.
In your model (the model that actsAs Translatable), add the following method. What it does is takes an array of locales, and then for every record in the table, and for every translatable field, and for every locale (ie, 3 loops), it checks that a translation record exists. If a translation doesn't exist, it adds a blank one, so at least the INNER JOIN won't fail.
It returns an array of all the records it added, so you can then go through and check them or change their content or whatever.
Here's the model method:
function ensureTranslationIntegrity($localesToCheck){
$allRows = $this->find('all', array('fields' => array('id')));
$fieldsToCheck = array();
$translatableFields = $this->actsAs['Translate'];
foreach($translatableFields as $key => $value){
// actsAs Translatabe can take field names only, or Key => Value pairs - see http://book.cakephp.org/2.0/en/core-libraries/behaviors/translate.html#retrieve-all-translation-records-for-a-field
if(is_numeric($key)){
$field = $value;
} else {
$field = $key;
}
array_push($fieldsToCheck, $field);
}
$translateModel = $this->translateModel();
$addedRows = array(); // This will contain all the rows we have to add
foreach ($allRows as $row){
foreach($fieldsToCheck as $field){
foreach($localesToCheck as $locale){
$conditions = array(
'model' => $this->name,
'foreign_key' => $row[$this->name]['id'],
'field' => $field,
'locale' => $locale
);
$translation = $translateModel->find('first',array('conditions' => $conditions));
if(!$translation){
$data = $conditions; // The data we want to insert will mostly just match the conditions of the failed find
$data['content'] = ''; // add it as empty
$translateModel->create();
$translateModel->save($data);
array_push($addedRows, $data);
}
} // END foreach($localesToCheck as $locale){
} // END foreach($fieldsToCheck as $field){
} // END foreach ($allRows as $row){
return $addedRows;
}
And in your controller, you'd call it something like this:
public function ensure_translation_integrity(){
$locales = array('en_au','en_gb','en_nz','pt_br','xh_za');
$addedRows = $this->YourModel->ensureTranslationIntegrity($locales);
debug($addedRows);
}
Hope that helps someone, but like I said, I'd love to see a better solution if someone has one.

Can a virtualfield be based upon linked data in cakephp?

Is it possible for a virtualFields var to be the sum of a field from a linked table?
For example, in, say, an Invoice model, could you have
public $virtualFields = array(
'invoiceNett' => 'SUM(InvoiceLine.nett)'
);
but obviously only SUMming the lines that belong to that invoice?
Thanks.
== Using CakePHP 2.0
You could use the afterFind callback to get the sum. This avoids storing the calculated values​​, which should be avoided when possible.
function afterFind($results)
{
foreach($results as &$result)
{
/*
Use something like:
$this->InvoiceLine->find('all', array('fields' => array('SUM(InvoiceLine.nett) as total'),
'conditions' => array('invoice_id' => $result['Invoice']['id'])));
*/
}
unset($result);
}
As far as I know, the best way to do that would be to have an actual total field, and update it anytime the data is saved (likely with a afterSave callback method).
So - anytime an InvoiceLine is saved, you run some code to update it's associated Invoice with a new total.
//InvoiceLine model
public function beforeSave() {
//code to update Invoice's "total" field
}
Theoretically, yes, if that linked table is an associate that is joined (belongsTo and hasOne).
However this would be a poor idea because if you decide not to include that table you would generate a SQL error.
You'd be better off having a separate function grab the data or creating a virtual field that was a nested SQL query.
Defining your virtual field in the respective Model would make more sense.
If not done so, you will be breaking the MVC pattern.
You can use that virtual field from other related models.
If you don't want using them in all related models, you can always use the field
attribute whilst defining relations.
public $hasMany = array(
'IwantVirtualField' => array(
'className' => 'MyModel',
...
)
);
In a model where you don't want virtual field
public $belongsTo = array(
'IwantVirtualField' => array(
'className' => 'MyModel1',
'fields' => array('MyModel1.id', 'MyModel1.name')
...
)
);

Paginate from within a model in CakePHP

I have a function in my Event model called getEvents - you can pass limit, start and end dates, fields, event types, and event subtypes.
I read that paginate can accept all the parameters I'm using like joins, conditions, limit...etc just like a normal find can.
It returns data just fine when I don't try to paginate. But - I'd like to be able to pass it a paginate variable to tell it instead of doing this:
$this->recursive = -1;
$data = $this->find('all', $qOptions);
to do this:
$this->recursive = -1;
$data = $this->paginate($qOptions);
When I try that, though, it gives me lots of errors. I can specify the errors later if needed - for now, I guess I'm looking for - is this something that can be done? If so, how?
Is there another better way to do something like this? I spent enough time making this function do just what I want, and allowing all the options passed...etc - it just seems like a waste if I can't also use it for pagination. But - if it's not ideal, I'm ok hearing that too. Thanks in advance.
EDIT:
I'm reading other things online that say you shouldn't use paginate in your model, because it draws from URL variables, which defeats the MVC structure purpose. This makes sense, but does that mean I have to write the same joins/queries in both model and controller? And in every action that it's needed?
The way I figured out how I can keep my complex find in my model without having to rewrite it a second time in the controller is by passing a $paginate boolean variable.
If $paginate is true, it returns just the options created, which can then be used in the controller's pagination. If it's false (meaning we don't want to paginate), it returns the actual event results. So far this seems to be working.
In my getEvents() function (this method is in the Events model)
if($paginate) {
return $qOpts; // Just return the options for the paginate in the controller to use
} else {
$data = $this->find('all', $qOpts); // Return the actual events
return $data;
}
Then, in my Events/Index (events controller, index action - where I know I want pagination):
$this->Event->recursive = -1; // (or set recursive = -1 in the appModel)
$opts['paginate'] = true;
$paginateOptions = $this->Event->getEvents($opts);
$this->paginate = $paginateOptions; // Set paginate options to just-returned options
$data = $this->paginate('Event'); // Get paginate results
$this->set('data', $data); // Set variable to hold paginated results in view
The paginate() model method does not accept the same parameters as a find(). Specifically, find() wants an array of options, but paginate() wants every option passed individually. See Custom Query Pagination in the CakePHP book.
So, instead of:
$data = $this->paginate($qOptions);
You want something like:
$data = $this->paginate($qOptions['conditions'], $qOptions['fields'], ...);
EDIT
Custom model pagination isn't a function that you call. It's a function that you need to implement and will be called by the CakePHP framework. In the example in your question you are trying to manually call $this->paginate(...) from somewhere in your model. That doesn't work. Instead, do this.
In your model, implement the paginate and paginateCount methods.
function paginate($conditions, $fields, ...)
{
// return some data here based on the parameters passed
}
function paginateCount($conditions, ...)
{
// return some rowcount here based off the passed parameters
}
Then, in your controller you can use the standard pagination functions.
function index()
{
$this->paginate = array('MyModel' => array(
'conditions' => array(...),
'fields' => array(...),
));
$this->set('myobjects', $this->paginate('MyModel'));
}
Now, the Controller::paginate() function will grab the conditions and other data from the Controller::paginate parameter and, instead of passing it to your Model::find it will pass it to your custom Model::paginate() and Model::paginateCount() functions. So, the data that is returned is based on whatever you do in those two methods and not based on a standard find().
}
you can use this one which is working fine for me.
$condition="your where condition";
$this->paginate = array(
'fields' => array('AsinsBookhistory.id', 'AsinsBookhistory.reffer_id', 'AsinsBookhistory.ISBN','AsinsBookhistory.image','AsinsBookhistory.title','AsinsBookhistory.last_updatedtime'),
'conditions' => $condition,
'group' => array('AsinsBookhistory.ISBN'),
'order' => array('AsinsBookhistory.last_updatedtime' => 'desc')
);
$this->set('lastvisitedbooks', $this->paginate('AsinsBookhistory'));
$paginate array are similar to the parameters of the Model->find('all') method, that is: conditions, fields, order, limit, page, contain, joins, and recursive.
So you can define your conditions like this :
var $paginate = array(
'Event' => array (...)
);
Or you can also set conditions and other keys in the $paginate array inside your action.
$this->paginate = array(
'conditions' => array(' ... '),
'limit' => 10
);
$data = $this->paginate('Event');
http://book.cakephp.org/2.0/en/controllers.html
http://book.cakephp.org/2.0/en/core-libraries/components/pagination.html
R u using $name = 'Event' in your controller ?
If we wont mention model name in $this->paginate() , it will use model as mentioned in $name otherwise look in var $uses array and in that will get Model name (first one )
for e.g var $uses = array('Model1','Model2'); // $name != mentioned
n you want pagination with respect to Model2 then you have to specify ModelName in paginate array like $this->paginate('Model2') otherwise Model1 will be considered in pagination.

Using existing field name as different name

i have existing website.
and i write the new back-end (in cakephp) without changing front-end programm
the discomfort that
db table has field names as
id
news_date
news_title
news_content
is it possiable to do something in cakephp model file (reindentify the field names)
so i can use model in controller as
News.date
News.title
News.content
What you need to do is setup some very basic virtual fields in your news model. Something like this should suit your needs.
public $virtualFields = array(
'title' => 'news_title',
'date' => 'news_date',
'content' => 'news_content'
);
Also do yourself a favour by checking out the other model attributes that could help you out, you'll want to set displayType as new_title I'd imagine.
Is said by Dunhamzz, virtualFields are a good solution until you want to work with these new field-names.
Since I assume your frontend needs to use the old names from the database I would go with the afterFind-callback in your model.
Let's say you've got the model news.php:
# /app/model/news.php
function afterFind($results) {
foreach ($results as $key => $val) {
if (isset($val['News']['title'])) {
$results[$key]['News']['news_title'] = $val['News']['title']);
# unset($results[$key]['News']['title']); //use this if you don't want the "new" fields in your array
}
if (isset($val['News']['date'])) {
$results[$key]['News']['news_date'] = $val['News']['date']);
# unset($results[$key]['News']['date']); //use this if you don't want the "new" fields in your array
}
if (isset($val['News']['content'])) {
$results[$key]['News']['news_content'] = $val['News']['content']);
# unset($results[$key]['News']['content']); //use this if you don't want the "new" fields in your array
}
}
return $results;
}
You need to rename the database-fields to your new wanted value. You then can use these within conditions like every other field.
Only difference is, that you get back an array where all your fields have been renamed to your frontend-fields.
For more information about the available callback-methods have a look here: Callback Methods

CakePHP HABTM: Editing one item casuses HABTM row to get recreated, destroys extra data

I'm having trouble with my HABTM relationship in CakePHP.
I have two models like so: Department HABTM Location. One large company has many buildings, and each building provides a limited number of services. Each building also has its own webpage, so in addition to the HABTM relationship itself, each HABTM row also has a url field where the user can visit to find additional information about the service they're interested and how it operates at the building they're interested in.
I've set up the models like so:
<?php
class Location extends AppModel {
var $name = 'Location';
var $hasAndBelongsToMany = array(
'Department' => array(
'with' => 'DepartmentsLocation',
'unique' => true
)
);
}
?>
<?php
class Department extends AppModel {
var $name = 'Department';
var $hasAndBelongsToMany = array(
'Location' => array(
'with' => 'DepartmentsLocation',
'unique' => true
)
);
}
?>
<?php
class DepartmentsLocation extends AppModel {
var $name = 'DepartmentsLocation';
var $belongsTo = array(
'Department',
'Location'
);
// I'm pretty sure this method is unrelated. It's not being called when this error
// occurs. Its purpose is to prevent having two HABTM rows with the same location
// and department.
function beforeSave() {
// kill any existing rows with same associations
$this->log(__FILE__ . ": killing existing HABTM rows", LOG_DEBUG);
$result = $this->find('all', array("conditions" =>
array("location_id" => $this->data['DepartmentsLocation']['location_id'],
"department_id" => $this->data['DepartmentsLocation']['department_id'])));
foreach($result as $row) {
$this->delete($row['DepartmentsLocation']['id']);
}
return true;
}
}
?>
The controllers are completely uninteresting.
The problem:
If I edit the name of a Location, all of the DepartmentsLocations that were linked to that Location are re-created with empty URLs. Since the models specify that unique is true, this also causes all of the newer rows to overwrite the older rows, which essentially destroys all of the URLs.
I would like to know two things:
Can I stop this? If so, how?
And, on a less technical and more whiney note: Why does this even happen? It seems bizarre to me that editing a field through Cake should cause so much trouble, when I can easily go through phpMyAdmin, edit the Location name there, and get exactly the result I would expect. Why does CakePHP touch the HABTM data when I'm just editing a field on a row? It's not even a foreign key!
From the CookBook the 1st problem is:
By default when saving a
HasAndBelongsToMany relationship, Cake
will delete all rows on the join table
before saving new ones.
I am not quite sure why Cake is trying to save the HABTM data even though you don't have a foreign key in your data, but there is an easy solution for that. Simply destroy the association for the save call:
$this->Location->unbindModel(
array('hasAndBelongsToMany' => array('Department'))
);
I'm thinking of one reason why this might be happening. When you retrieve Location, you also retrieve locations_departments data. And when you do a save($this->data) it looks for models in the array and saves them.
A way to solve this is setting the recursive attribute (of a model) to -1 or 0 (try, I'm not sure, just print out the data to see what comes out). You can set it in the model: var $recursive = -1; or in the controller method (action): $this->ModelName->recursive = -1;
More about recursive: http://book.cakephp.org/view/439/recursive
It's really similar to what harpax suggested, just if you don't need that data, tell it to Cake, so that it won't fetch it.
Trouble is that when saving your Location, you gave the save method an array containing all the DepartmentsLocations too. Thus CakePHP destroys everything and try to recreate it.
This is a common mistake with cake since it will often pull far too many results for you.
Be sure to pass only the data that needs to be saved, or better to fetch only the datas you need.

Resources