I need to check if a table exists in a database. I currently develop using Yii2.
My case is a bit different from this question because the table to be checked is not (and can not be) a model.
I have tried (new \yii\db\Query())->select('*')->from($mysticTable)->exists());
The above throws a yii\db\Exception because, according to question linked above, the yii\db\Query() class tries to ->queryScalar() when asked if ->exists(). Invariably, this method checks if the result-set exists.
How do I check if a table exists?
For Yii2 you can use:
$tableSchema = Yii::$app->db->schema->getTableSchema('tableName');
If the table does not exist, it will return null, so you can check returned value for being null:
if ($tableSchema === null) {
// Table does not exist
}
You can find this method in official docs here.
Good that you get an exception. Simply parse the exception message. You would get a very very specific message and SQL error code for missing table.
That is what I do when checking e.g. IF an error was due to something that can be recovered, say a broken connection, versus some other error.
OR I see that many people have pointed out much more direct ways of getting that information.
A spin off #msfoster's answer got me closer to a solution in yii2
/**
* #param $tableName
* #param $db string as config option of a database connection
* #return bool table exists in schema
*/
private function tableExists($tableName, $db = null)
{
if ($db)
$dbConnect = \Yii::$app->get($db);
else
$dbConnect = \Yii::$app->get('db');
if (!($dbConnect instanceof \yii\db\Connection))
throw new \yii\base\InvalidParamException;
return in_array($tableName, $dbConnect->schema->getTableNames());
}
This also serves multiple databases.
Related
I've a mysql connection and a pgsql (postgres, set as default) connection.
I must read all rows from a table from mysql and then save all into postgres
I tried do this, remembering that my default connection is pgsql
$mysql_model = new Regioni();
$regioni = $mysql_model->setConnection("mysql")->all();
But it's still using pgsql connection.
I'm sure of this because i tried to insert a row into pgsql table, (it was empty), and I can dump this single row as results of all.
Also, I (only for debugging, do not hate me), modified vendor/laravel/framework/src/Illuminate/Database/Eloquent/Model.php, adding a debug echo into this method
/**
* Set the connection associated with the model.
*
* #param string|null $name
* #return $this
*/
public function setConnection($name)
{
echo "Model calling setConnection($name)" . PHP_EOL;
$this->connection = $name;
return $this;
}
As a result I can see
Model calling setConnection(mysql)
Model calling setConnection()
Model calling setConnection(pgsql)
The first one is the result of my explicit setConnection.
But why calling ->all() will reset connection to default one?
Main goal/question
What's the right way to dinamically change the connection of a model?
I post my actual solution.
It's ugly, but it works.
The requisite for this is that default connection is pgsql so I must set connection explicitly to read all table record from nysql
Aftert that, I loop throught and then save to pgsql simply doing an insert at time
$regioni = (new Regioni)->setConnection('mysql')->get();
(new Regioni)->truncate();
foreach($regioni as $r) {
(new Regioni)->insert($r->toArray());
}
Note that this is specifically for my case
I have an old mysql to be migrated to the new postgresql
I already did migrations on postgresql so all structures are ready
I read all data from source and simply write into target postgres
this must be done only one time
I truncate target before insert (to be repeatable in development)
If you can suggest something more efficient, please, write an answer
Laravel 5.5
I'm wondering how to properly handle the possible case of multiple updates to the same records by separate users or from different pages by the same user.
For example, if an instance of Model_1 is read from the database responding to a request from Page_1, and a copy of the same object is loaded responding to a request from Page_2, how best to implement a mechanism to prevent a second update from clobbering the first update? (Of course, the updates could occur in any order...).
I don't know if it is possible to lock records through Eloquent (I don't want to use DB:: for locking as you'd have to refer to the underlying tables and row ids), but even if it were possible, Locking when loading the page and unlocking when submitting wouldn't be proper either (I'm going to omit details).
I think detecting that a previous update has been made and failing the subsequent updates gracefully would be the best approach, but do I have to do it manually, for example by testing a timestamp (updated_at) field?
(I'm supposing Eloquent doesn't automatically compare all fields before updating, as this would be somewhat inefficient, if using large fields such as text/binary)
You should take a look at pessimistic locking, is a feature that prevents any update until the existing one its done.
The query builder also includes a few functions to help you do "pessimistic locking" on your select statements. To run the statement with a "shared lock", you may use the sharedLock method on a query. A shared lock prevents the selected rows from being modified until your transaction commits:
DB::table('users')->where('votes', '>', 100)->sharedLock()->get();
Alternatively, you may use the lockForUpdate method. A "for update" lock prevents the rows from being modified or from being selected with another shared lock:
DB::table('users')->where('votes', '>', 100)->lockForUpdate()->get();
Reference: Laravel Documentation
What I came up with was this:
<?php
namespace App\Traits;
use Illuminate\Support\Facades\DB;
trait UpdatableModelsTrait
{
/**
* Lock record for update, validate updated_at timestamp,
* and return true if valid and updatable, throws otherwise.
* Throws on error.
*
* #return bool
*/
public function update_begin()
{
$result = false;
$updated_at = DB::table($this->getTable())
->where($this->primaryKey, $this->getKey())
->sharedLock()
->value('updated_at');
$updated_at = \Illuminate\Support\Carbon::createFromFormat('Y-m-d H:i:s', $updated_at);
if($this->updated_at->eq($updated_at))
$result = true;
else
abort(456, 'Concurrency Error: The original record has been altered');
return $result;
}
/**
* Save object, and return true if successful, false otherwise.
* Throws on error.
*
* #return bool
*/
public function update_end()
{
return parent::save();
}
/**
* Save object after validating updated_at timestamp,
* and return true if successful, false otherwise.
* Throws on error.
*
* #return bool
*/
public function save(array $options = [])
{
return $this->update_begin() && parent::save($options);
}
}
Usage example:
try {
DB::beginTransaction()
$test1 = Test::where('label', 'Test 1')->first();
$test2 = Test::where('label', 'Test 1')->first();
$test1->label = 'Test 1a';
$test1->save();
$test2->label = 'Test 1b';
$test2->save();
DB::commit();
} catch(\Exception $x) {
DB::rollback();
throw $x;
}
This will cause abort as the timestamp does not match.
Notes:
This will only work properly if the storage engine supports row-locks. InnoDB does.
There is a begin and an end because you may need to update multiple (possibly related) models, and wish to see if locks can be acquired on all before trying to save. An alternative is to simply try to save and rollback on failure.
If you prefer, you could use a closure for the transaction
I'm aware that the custom http response (456) may be considered a bad practice, but you can change that to a return false or a throw, or a 500...
If you don't like traits, put the implementation in a base model
Had to alter from the original code to make it self contained: If you find any errors, please comment.
i have a phpunit question regarding dbunit and how to keep data created in the database by one test for use in the next. i'm new to phpunit (we've been using an in-house tester for years but are finally trying to get with the modern age), so i apologize if this is a trivial issue.
the desired effect
i have a mysql table that contains a column that is a unique key. if an attempt is made to insert a duplicate of this column, special things happen that i would like to be able to test. i have written a test to insert a value into this column (and test its success) and then written another test immediately afterwards to test how the class fails on attempting a duplicate value. i'd like to be able to catch that exception and test it. i am using dbunit to pre-fill my db with all the pre-filly stuff i need.
the problem
at the commencement of each test it appears as if getDataSet() is called and, as a result, the unique key data i insert in the first test is no longer there to test against. consequently, i can't test the anticipated failure of inserting duplicate unique keys.
what i'm looking for
well, obviously some way to persist the database data across tests; avoid calling getDataSet(), perhaps, at the beginning of the second test.
i certainly hope this is possible. i can't imagine why it wouldn't be; it seems like people should want to test duplicate insert! i am willing to entertain other solutions if they accomplish the task.
thanks in advance!
here's my test, stripped down to the relevant bits.
<?php
class UserPOSTTest extends \PHPUnit_Extensions_Database_TestCase
{
static private $pdo = null;
private $conn = null;
/**
* #return PHPUnit_Extensions_Database_DB_IDatabaseConnection
*/
public function getConnection()
{
if($this->conn === null) {
if (self::$pdo == null) {
self::$pdo = new \PDO('mysql:host=localhost;dbname=thedatabase', 'user', '*********');
}
$this->conn = $this->createDefaultDBConnection(self::$pdo, "db");
}
return $this->conn;
}
/**
* #return PHPUnit_Extensions_Database_DataSet_IDataSet
*/
public function getDataSet()
{
// this is returned at the beginning of every test
return $this->createFlatXmlDataSet(dirname(__FILE__) . '/some_data_set.xml');
}
/**
* test the insertion of the value "unique key value" into a column set as UNIQUE KEY in mysql
* since getDataSet() has cleared this table, it passes.
*/
public function uniqueKeyTest_passes()
{
$inserter = new Inserter("unique key value");
$this->assertEquals($inserter->one,1); // just some bogus assertion
} // uniqueKeyTest_passes
/**
* run the exact same insert as in uniqueKeyTest_passes() above. the purpose of this test is to
* confirm how the Inserter class fails on the attempt to insert duplicate data into a UNIQUE KEY column.
* however, the data inserted in uniqueKeyTest_passes() has been scrubbed out by getDataSet()
* this is the crux of my question
*/
public function uniqueKeyTest_should_fail()
{
try {
// exact same insert as above, should fail as duplicate
$inserter = new Inserter("unique key value");
}
catch(Exception $e) {
// if an exception is thrown, that's a pass
return;
}
// the insert succeeds when it should not
$this->fail("there should be an exception for attempting insert of unique key value here");
} // uniqueKeyTest_should_fail
}
The fact that each test runs independently from others is a feature and a design goal of unit testing.
In your case you can simply use this:
/**
* Confirm how the Inserter class fails on the attempt to
* insert duplicate data into a UNIQUE KEY column.
*
* #expectedException Exception
*/
public function uniqueKeyTest_should_fail()
{
$inserter = new Inserter("unique key value");
// exact same insert as above, should fail as duplicate
$inserter = new Inserter("unique key value");
}
Please note the usage of #expectedException which simplifies the test code a lot. However, I would write my code in a way that it throws a DuplicateKeyException, this would make the test more specific. In the current form, you would for example not being able to detect a database connection error anymore (and the test would succeed!).
I want to check if a table in the OpenCart database exist so i made this function
public function CheckCustomer(){
$query = $this->db->query('SELECT * FROM '.DB_PREFIX.'customer_online');
return $query->row;
}
and in my controller i test if it exists i set a variable to 1 or 0 depending.
The table does exist everything is fine, byt if i delete the c from customer just to simulate the table not being there my tpl page is not rendered instead i get this error :
Notice: Error: Table 'OpenCart-Test.oc_ustomer_online' doesn't exist
Error No: 1146
SELECT * FROM oc_ustomer_online in /home/justine/www/opencart-test/opencart-1.5.5.1/upload/system/database/mysql.php on line 50
Is there anyway of doing this without it throwing errors on screen as i need to know if the table exists before i display certain information in my tpl file.
Hope someone can shed some light on this.
Generally speaking dynamically-created tables are a bad idea. Any table you need should always exist, and you can simply use TRUNCATE TABLE mytable to quickly remove all rows rather than DROP TABLE.
edit: way better idea than below
Change the query to SHOW TABLES FROM my_database [or simply SHOW TABLES if the db is already selected] and check to see if the table name you're looking for exists in the result set.
That said, you should be able to suppress the error by prefixing the function with #, ie:
$query = #$this->db->query('SELECT * FROM '.DB_PREFIX.'customer_online');
Or you should be able to switch your mySQL database object to use Exceptions rather than PHP Errors, then enclose the function call in a try{ } catch() { } block.
Resolved .
Putting this in my controller file fixed it.
$this->data['checkcustomer'] = #mysql_query ('SELECT * FROM '.DB_PREFIX.'customer_online');
MySQL can list all tables in a database. By specifying a "like tablename" you can search for a specific table.
/**
* Check if the table 'customer_online' exists
* #return boolean TRUE if table exists, FALSE otherwise.
*/
public function CheckCustomer(){
$res = $this->db->query("SHOW TABLES LIKE '".DB_PREFIX."customer_online'");
return (boolean) $res->num_rows;
}
This does not generate any errors as it is not trying to access the table, just looking for the table name in the list of tables in the database.
In Doctrine2.0.6, I keep getting an error: "Column VoucherId specified twice".
The models in question are:
Basket
BasketVoucher
Voucher
Basket links to BasketVoucher.
Voucher links to BasketVoucher.
In Voucher and BasketVoucher, there is a field called VoucherId. This is defined in both models and exists with the same name in both DB tables.
The error occurs when saving a new BasketVoucher record:
$basketVoucher = new BasketVoucher;
$basketVoucher->setVoucherId($voucherId);
$basketVoucher->setBasketId($this->getBasket()->getBasketId());
$basketVoucher->setCreatedDate(new DateTime("now"));
$em->persist($basketVoucher);
$em->flush();
I've checked the models and VoucherId is not defined twice. However, it is used in a mapping. Is this why Doctrine thinks that the field is duplicated?
Here's the relevant code - I haven't pasted the models in their entirety as most of the code is get/set.
Basket
/**
* #OneToMany(targetEntity="BasketVoucher", mappedBy="basket")
* #JoinColumn(name="basketId", referencedColumnName="BasketId")
*/
private $basketVouchers;
public function getVouchers()
{
return $this->basketVouchers;
}
BasketVoucher
/**
* #ManyToOne(targetEntity="Basket", inversedBy="basketVouchers")
* #JoinColumn(name="basketId", referencedColumnName="BasketId")
*/
private $basket;
public function getBasket()
{
return $this->basket;
}
/**
* #OneToOne(targetEntity="Voucher", mappedBy="basketVoucher")
* #JoinColumn(name="voucherId", referencedColumnName="VoucherId")
*/
private $voucherEntity;
public function getVoucher()
{
return $this->voucherEntity;
}
Voucher
/**
* #OneToOne(targetEntity="BasketVoucher", inversedBy="voucherEntity")
* #JoinColumn(name="voucherId", referencedColumnName="VoucherId")
*/
private $basketVoucher;
public function getBasketVoucher()
{
return $this->basketVoucher;
}
Any ideas?
EDIT: I've found that the same issue occurs with another model when I save it for the first time. I am setting the primary key manually. The main issue appears to be saving a relationship within an entity.
In this case, I have a field - DraftOrderId - which is used as the primary key on three models. The first model - DraftOrder - has DraftOrderId as a primary key, which is an auto incrementing value. The other two models - DraftOrderDeliveryAddress, and DraftOrderBillingAddress - also use DraftOrderId as a primary key, but it isn't auto incremented.
What's happening is one of the following issues:
If I save the delivery address entity with a draft order id and set it to persist, I get an error: Column DraftOrderId specified twice. Code:
try {
$addressEntity->getDraftOrderId();
} catch (\Doctrine\ORM\EntityNotFoundException $e) {
if ($addressType == "delivery") {
$addressEntity = new Dpp\DraftOrderDeliveryAddress;
} elseif ($addressType == "billing") {
$addressEntity = new Dpp\DraftOrderBillingAddress;
}
$addressEntity->setDraftOrderId($draftOrder->getDraftOrderId());
$em->persist($addressEntity);
}
(It would also help to know if there's a better way of checking if a related entity exists, rather than trapping the exception when trying to get a value.)
If I remove the line that sets the draft order id, I get an error: Entity of type Dpp\DraftOrderDeliveryAddress is missing an assigned ID.
If I keep the line that sets the draft order id but I remove the persist line, and I also keep the lines later on in the code that sets the name and address fields, I don't get an error - but the data is not saved to the database. I am using flush() after setting all the fields - I'm just not using persist(). In the previous examples, I do use persist() - I'm just trying things out to see how this can work.
I can paste more code if it would help.
I think I've fixed it! A couple of findings:
For a primary key that is not an auto-incrementing value, you need to use:
#generatedValue(strategy="IDENTITY")
You also have to explicitly set the mapped entities when creating them for the first time. At first, I was trying to create the address entity directly, but I wasn't setting the mapped entity within the parent model to reference the address entity. (if that makes any sense)
I'm fairly sure it was mostly due to the lack of the IDENTITY keyword, which for some reason was either saying the key wasn't set, or saying it was set twice.