CakePHP 3: Best Practice for Temporary SQL Tables - mysql

Dear CakePHP 3 developers,
I'd like to use SQL's Temporary Tables in a CakePHP 3.4.13 project for a single run through a script. Going through Cake's documentation, there seems no direct way to tell CakePHP my desire. How would I best go about it, then?
I've prepared a Table in src/Model/Table/TempItemsTable.php:
namespace App\Model\Table;
use Cake\ORM\Table;
class TempItemsTable extends Table
{
public $fields = [
'id' => ['type' => 'integer'],
'con' => ['type' => 'string', 'length' => 255, 'null' => false],
'_constraints' => [
'primary' => ['type' => 'primary', 'columns' => ['id']]
]
];
public function initialize(array $config)
{
// $this->setTable(null);
}
}
The idea to use $fields to tell CakePHP the desired table schema comes from a possibly unrelated documentation for Test Fixtures.
But how do I tell CakePHP not to look for an actual table in the database?
The uncommented line $this->setTable(null); was my poor attempt at that, which is supposedly similiar to the right way in earlier versions of CakePHP, but according to version 3.x documentation, setTable() doesn't accept null, while table() does, but it's deprecated as of 3.4 and also didn't change anything.
Finally, of course, I get this exception as soon as I try to access this "table" in a controller via $temp = TableRegistry::get('TempItems');:
SQLSTATE[42S02]: Base table or view not found: 1146 Table 'mydatabase.temp_items' doesn't exist
Help, I'm stuck. :(

There's no need to tell it to not look for the table, actually that's the opposite of what you want to do, given that you eventually want to access it.
The table class should basically be configured as usual, and you should create the temporary database table before the application causes it to be accessed. You can either write the raw table creation SQL manually, or generate it from a \Cake\Database\Schema\TableSchema instance, which supports temporary tables.
You can either explicitly create the schema object:
$schema = new \Cake\Database\Schema\TableSchema('temp_items');
$schema
->addColumn('id', ['type' => 'integer'])
->addColumn('con', ['type' => 'string', 'length' => 255, 'null' => false])
->addConstraint('primary', ['type' => 'primary', 'columns' => ['id']])
->setTemporary(true);
$TableObject->setSchema($schema);
or let the table object generate it, using your fields definition array:
$TableObject->setSchema($TableObject->fields);
$schema = $TableObject->getSchema()->setTemporary(true);
You can then generate the table creation SQL from the schema object and run it against the database:
$connection = $TableObject->getConnection();
$queries = $schema->createSql($connection);
$connection->transactional(
function (\Cake\Database\Connection $connection) use ($queries) {
foreach ($queries as $query) {
$stmt = $connection->execute($query);
$stmt->closeCursor();
}
}
);
$queries would be an array of SQL commands required to create the table, something along the lines of:
[
'CREATE TEMPORARY TABLE `temp_items` (
`id` INTEGER AUTO_INCREMENT,
`con` VARCHAR(255) NOT NULL,
PRIMARY KEY (`id`)
)'
]
Note that if you do not assign the schema to the table object, you could run into caching problems, as the cached schema wouldn't match anymore when you change the table definition and do not clear the cache.
See also
Cookbook > Database Access & ORM > Schema System
Cookbook > Database Access & ORM > Database Basics

Related

Laravel Eloquent create method skipping IDs and returning them

I'm simply trying to create a new model through Eloquent's create method. However, we're seeing that the new model will sometimes contain an id that has been skipped in mysql. This id is designated as an autoincrement column in mysql.
We are also seeing this happen when using DB:getPdo()->lastInsertId() immediately after the model has been created.
$questionResponse = $response->questionResponses()->create([
'survey_question_id' => $question->id,
'response' => 5
]);
$response_id = $questionResponse->id; // weve also used DB::getPdo()->lastInsertId();
Queue::push('TheWorker', array('survey_id' => $survey->id, 'response_id' => $response_id, 'question_id' => $question->id, 'user_id' => $user->id, 'student_target_id' => $student_target_id));
TheWorker then fails due to receiving an incorrect $repsonse_id that doesn't exist in the DB.
We would expect that the newly created model would contain the id that matches the record created in mysql.
Thanks in advance!

Laravel: Store error messages in database

Any one know how to send error messages to database in laravel which generate from app/exceptions/handler.php ?
I need to send what error massages generated in report() method to database.
If you are interested doing this manually, you can do something as following.
Step 1 -
Create a model to store errors that has a DB structure as following.
class Error extends Model
{
protected $fillable = ['user_id' , 'code' , 'file' , 'line' , 'message' , 'trace' ];
}
Step 2
Locate the App/Exceptions/Handler.php file, include Auth, and the Error model you created. and replace the report function with the following code.
public function report(Exception $exception) {
// Checks if a user has logged in to the system, so the error will be recorded with the user id
$userId = 0;
if (Auth::user()) {
$userId = Auth::user()->id;
}
$data = array(
'user_id' => $userId,
'code' => $exception->getCode(),
'file' => $exception->getFile(),
'line' => $exception->getLine(),
'message' => $exception->getMessage(),
'trace' => $exception->getTraceAsString(),
);
Error::create($data);
parent::report($exception);
}
(I am demonstrating this using laravel 5.6)
Because Laravel uses Monolog for handling logging it seems that writing Monolog Handler would be the cleanest way.
I was able to find something that exists already, please have a look at monolog-mysql package. I did not use it, so I don't know whether it works and if it works well, but it's definitely good starting point.

CakePHP 3.x UnitTest "Base table or view not found"

I get an Error-Message in a UnitTest in CakePHP 3.2 and the official documentation doesn't help me here anymore. I think the error has something todo with the SQL-Joins I try to use.
The Error-Message is the following:
`1) App\Test\TestCase\Controller\GetContentControllerTest::testIndex
PDOException: SQLSTATE[42S02]: Base table or view not found: 1146 Table 'contentmapper_test.CmDeviceclasses' doesn't exist`
In my Testclass GetContentControllerTest I load my fixtures that I need and that creates my Database-Tables on start:
`public $fixtures = [
'app.cm_content_options',
'app.cm_content_addresses',
'app.cm_deviceclasses',
'app.cm_properties'
];`
In the setUp()-Method I load the Main-Table:
`public function setUp()
{
parent::setUp();
$this->CmContentOptions = TableRegistry::get('CmContentOptions');
}`
My Test-Method testIndex() looks like this:
public function testIndex()
{
//find the belonging ContentOption to address data
//submitted by the client
$this->testFindAllByUriAndDeviceclassAndBoxId();
assert($this->arrObjContentOptions->count() == 1);
}
The testFindAllByUriAandDeviceclassAndBoxID() looks like shown in the following Image (the Editor is not able to prettyprint it correctly):
testFindAllByUriAandDeviceclassAndBoxID()
It's hard to describe the whole Context; I hope it is possible to understand.
The Error happens exactly on the statement shown in the image:
$result = $query->toArray()
I think I just forgot something to add in the setUp() Method or something like that.
I hope anyone can help.
You joins are set up incorrectly, you're mixing up aliases and table names.
The alias is the key of the join array, and the table key should hold the actual database table name, not the table class name.
Given that you are following CakePHPs naming conventions for your database table names, your join setup should look more like this
[
'CmDeviceclasses' => [ /* < this is the SQL alias */
'table' => 'cm_deviceclasses', /* < this is the database table name */
'type' => 'LEFT',
'conditions' => [
'CmDeviceclasses.classname' => $this->deviceclass
]
],
'CmContentAddresses' => [
'table' => 'cm_content_addresses',
'type' => 'INNER',
'conditions' => [
'CmContentAddresses.uri' => $this->uri,
'CmContentAddresses.boxid' => $this->boxid,
]
],
],
[
'CmDeviceclasses.classname' => 'string',
'CmContentAddresses.uri' => 'string',
'CmContentAddresses.boxid' => 'string'
]
There is no technical need to follow the CamelCase conventions for the aliases, but for sure it doesn't hurt to generally stick to the conventions.
ps, if you setup the associations properly, then there should be no need to use manual joins, you could just use Query::contain() and Query::innerJoinWith() or Query::matching().
See
Cookbook > Database Access & ORM > Associations - Linking Tables Together
Cookbook > Database Access & ORM > Retrieving Data & Results Sets > Retrieving Associated Data
Cookbook > Database Access & ORM > Retrieving Data & Results Sets > Filtering by Associated Data

CakePHP 1.3 not saving to database but sql statement is correct and insertID is increased correctly

I already searched many forums for my really strange issue, but I still can't figure out whats going wrong during my save process... The issue: Cake says, my data was saved, creates an autoincrement-ID but no record is stored in the database.
The environment
I have a cake-1.3.13 app running for some time and now needed to add another database table, which is of course related to other tables. My problem is saving records for the habtm-relation table, which looks like this:
CREATE TABLE IF NOT EXISTS `employees_projects_rejectreasons` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`employees_project_id` int(10) unsigned NOT NULL,
`rejectreason_id` int(10) unsigned NOT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `employees_project_id` (`employees_project_id`,`rejectreason_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 AUTO_INCREMENT=6;
I scaffolded the simple model only with basic validation criteria.
<?php
class EmployeesProjectsRejectreason extends AppModel {
var $name = 'EmployeesProjectsRejectreason';
var $validate = array(
'employees_project_id' => array(
'numeric' => array(
'rule' => array('numeric'),
//'message' => 'Your custom message here',
//'allowEmpty' => false,
//'required' => false,
//'last' => false, // Stop validation after this rule
//'on' => 'create', // Limit validation to 'create' or 'update' operations
),
),
'rejectreason_id' => array(
'numeric' => array(
'rule' => array('numeric'),
//'message' => 'Your custom message here',
//'allowEmpty' => false,
//'required' => false,
//'last' => false, // Stop validation after this rule
//'on' => 'create', // Limit validation to 'create' or 'update' operations
),
),
);
//The Associations below have been created with all possible keys, those that are not needed can be removed
var $belongsTo = array(
'EmployeesProject' => array(
'className' => 'EmployeesProject',
'foreignKey' => 'employees_project_id',
'conditions' => '',
'fields' => '',
'order' => ''
),
'Rejectreason' => array(
'className' => 'Rejectreason',
'foreignKey' => 'rejectreason_id',
'conditions' => '',
'fields' => '',
'order' => ''
)
);
I created several records for Rejectreasons and EmployeesProjects, so I have some valid entries here in the database. Now I want to link them together by creating a new record in the given employees_projects_rejectreasons table. I try to do this from another controller (the EmployeesProjectsController). Here is my latest attempt to save the data:
$this->EmployeesProject->EmployeesProjectsRejectreason->create();
$eprData = array(
'EmployeesProjectsRejectreason' => array(
'employees_project_id' => (int)$id,
'rejectreason_id' => (int)$rrId
)
);
if($this->EmployeesProject->EmployeesProjectsRejectreason->save($eprData)) {
debug('successfully saved EPR with ID '.$this->EmployeesProject->EmployeesProjectsRejectreason->__insertID);
} else {
debug('could not save EPR with employees_project_id='.$id.' and rejectreason_id='.$rrId);
}
Now what happens
After I make an attempt to save a record, my debug gives me the following success report:
successfully saved EPR with ID 4
So the save() call returned true, a new ID was created by the auto_increment function of mySQL. So far so good. But when I check my database, there was no record created. But the auto_increment_counter was increased by 1, as if a record was stored, but it wasn't.
Running the app with debug-level 2, I can see the generated SQL-statement from cake, which looks perfectly fine to me:
INSERT INTO `employees_projects_rejectreasons` (`employees_project_id`, `rejectreason_id`) VALUES (3, 3)
If I run this statement directly on the sql server, the record ist inserted correctly.
What I already tried
I already tried different approaches with the save procedure. I tried working with setters instead of a data-array:
$this->EmployeesProject->EmployeesProjectsRejectreason->set('employees_project_id', $id);
as well, but it made no difference. After I wrote a custom save-method in the EmployeesProjectsRejectreason-Model, calling it from the controller, but it always produced the same result.
I tried
deleting the model-cache
restarting the server-instances and the server itself
Deleting the table and creating it again
disabling validation in the model
removing the unique foreign-key index
Saving with hard-coded and existing ids as foreign key
Some more strange behaviour
The last tests with hard-coded IDs in my controller code confronted me with more riddles: If I try storing existent foreign_key-IDs, the data is not saved as before. But if both IDs are hardcoded and NOT EXISTING (I used invented IDs 345 AND 567, which are definetely not existing in the database) a record was finally inserted!
Moreover I scaffolded Models, Views and Controllers for the new tables. When I run the scaffolded view "myApp/employees_projects_rejectreasons/add" and add a new record, everything works just fine.
I'm just not able to save the record from other controllers. Since I already have a huge headache, solving this problem, I highly appreciate any hint for a solution!!
Thanks in advance guys!
I finally found a solution to solve the issue. I still don't know, why the save code before did not work, but here is how I changed my code to make it work:
From my form, the data array comes in the following format:
Array
(
[EmployeesProject] => Array
(
[id] => 10
[user_id] => 0
[additional_information] => some comment text
[state] => absage
[Rejectreason] => Array
(
[0] => 1
[1] => 8
)
)
)
I searched for some solutions to save habtm relations in cakePHP directly with one call, but that does not seem to be possible in cake-1.3. So I created this pretty simple save routine in my EmployeesProjectController, which works perfectly fine for me:
if (!empty($this->data)) {
if ($this->EmployeesProject->save($this->data)) {
if(array_key_exists('Rejectreason', $this->data['EmployeesProject'])) {
foreach($this->data['EmployeesProject']['Rejectreason'] as $key => $rrId) {
$this->EmployeesProject->EmployeesProjectsRejectreason->create();
$this->EmployeesProject->EmployeesProjectsRejectreason->set('rejectreason_id', $rrId);
$this->EmployeesProject->EmployeesProjectsRejectreason->set('employees_project_id', $this->data['EmployeesProject']['id']);
if($this->EmployeesProject->EmployeesProjectsRejectreason->save()) {
}
}
}
}
}
Thanks #Yoggi for supporting me solving this issue!

WordPress Custom Select Query

I really don't know enough about MySQL queries and it's showing.
I have a custom field set for every post. The custom field stores the posts source URL in a key called "source_url".
I have it working with the below WP_Query parameters, but it's incredibly slow. Keep in mind it's possible to 50+ urls to search for.
So, given an array of source URL's, I want to fetch the matching posts.
For example, here is what I currently have that's slow in WP_Query:
// var_dump of $urls array (this could be 50+ urls)
array(7) {
[0]=>
string(42) "http://www.youtube.com/watch?v=FMghvnqDhT8"
[1]=>
string(42) "http://www.youtube.com/watch?v=RY-yUFpXTnM"
[2]=>
string(58) "http://www.youtube.com/watch?v=nIm2dnyJ1Ps&feature=related"
[3]=>
string(42) "http://www.youtube.com/watch?v=NoCtRQlJAqM"
[4]=>
string(57) "http://holidaycustoms.blogspot.com/2012/08/busy-week.html"
[5]=>
string(42) "http://www.youtube.com/watch?v=DcZvg197Ie4"
[6]=>
string(42) "http://www.youtube.com/watch?v=7P3UEbLmLuo"
}
// Build Media Query
$meta_query = array(
'relation' => 'OR'
);
foreach( $urls as $url ) {
$meta_query[] = array(
'key' => 'source_url',
'value' => $url
);
}
// Get 20 matching posts from a category set by a variable
$args = array(
'post_type' => 'post',
'posts_per_page' => 20,
'orderby' => 'rand',
'cat' => $cat_ID,
'meta_query' => $meta_query
);
$posts = get_posts($args);
What I'm looking to do is replace the above code with a custom query select, which I have read is much faster than WP_Query.
But I don't know enough about MySQL or the WP database to build the custom select query. Can anyone help? Thanks in advance!
In the post you linked yourself, the first reply already states that
[...] the default schema doesn't even have an index on the value column
Which is far more severe a problem than any you would have with a query generator, because without an index the DBMS has to traverse the whole table and compare strings of each field.
Adding an index is fairly easy with an appropriate management tool like PHPMyAdmin. The offending table you will need to add an index to is called wp_postmeta and the field that needs an index is meta_value, and the index type should be INDEX.
Adding an index is transparent and does not affect wordpress other than in performance. It could take some time though since, well MySQL needs to traverse the whole table. Also, because you are indexing string data, the index will be quite big.
You should also try using appropriate structures for your query. You are currently using a big ORed selection with different values but always the same field. There is a construct for just that, and it's called IN.
...
// Build Media Query
$meta_query = array();
$meta_query[] = array(
'key' => 'source_url',
'value' => $urls,
'compare' => 'IN'
);
// Get 20 matching posts from a category set by a variable
..
(Untested. I actually never did this, Reference)
The performance gain would be negligible compared to adding an index I assume, but your code would become a lot simpler.